| Sure, I'll give a simple example. Things can be done differently and must be more complex sometimes depending on the requirements, but it all basically boils down to this. So, ECS - Entity Component System. The entity itself can be literally just an int id. To make a new entity, request a new id. Since everything about an entity is contained in what components it has, you don't need more information. Components: they're just data. You can model them as structs: struct c_actor {
int entity_id;
// action to execute once enough energy is accrued
struct action *action;
// Angband style energy level, to take a turn must be >=100 (ACTOR_ENERGY_TO_ACT)
int energy;
// amount of energy gained per game tick
int energy_gain_per_tick;
};
I hope the comments explain. This is all the component is, data. You can have a helper function which stores it in some data structure where you keep each entity's components, indexed by id.Systems. They're just function which operate on entities which have their component. int s_actor_do_acts(struct component_systems *c_systems)
{
// loop through your tracked entities
for (for (int i = 0; i < ACTOR_ENTITIES_NUM; i++) {
...
int *energy = &c_systems->actors[entity_id].energy;
if ((*energy) < ACTOR_ENERGY_TO_ACT) {
goto add_energy;
}
(*energy) -= ACTOR_ENERGY_TO_ACT;
...
// actually do action
if (c_actor.action != NULL) {
switch(c_actor.action->action_type) {
case ACTION_WALK:
action_walk_do((struct a_walk*)c_actor.action, &c_systems->positions[entity_id], c_systems->lmap);
break;
...
You call the system function in the main game loop.Note: something a bit "unorthodox" here is the fact that the Actor system here also directly gets a handle on the position component. Others prefer event systems, where instead you would fire off an event. My RL is single-threaded (I can definitely wring enough CPU perfomance without needing to overcomplicate), so I've opted to just pass handles around and have each system call each other directly via passing handles. In this example, the Walk action would call into the position system's function via the handle passed. This greatly simplifies code, and allows me to do partial updates of the game state outside the main loop. edit: Also, in case you're wondering how the data about what each entity is stored (since that seemed to be the crux of your questions). You can have a data file: ... many other components ..
[components.actor]
energy_gain_per_tick = 20
[components.fov]
radius = 10
...
Then you read this in and dynamically build an entity: toml_table_t *actor = toml_table_in(comp, "actor");
if (actor) {
toml_datum_t energy_gain_per_tick = toml_int_in(actor, "energy_gain_per_tick");
if (!energy_gain_per_tick.ok) {
UNT_ERROR("energy_gain_per_tick not defined for actor");
return -1;
}
struct c_actor c_actor = {
.entity_id = entity_id,
.energy = 0,
.energy_gain_per_tick = energy_gain_per_tick.u.i,
.action = NULL
};
c_actor_add(c_systems, c_actor);
s_actor_track_entity(entity_id);
}
I hope the real code is readable enough to serve as pseudo code alternative.One advantage is you can generate semi-randomized entities, and dynamically change their components at runtime. You can't do that with OOP. |
Even more, in networked games it can be used to roll-back actions you did (e.g. because someone with a 100ms ping shot you 50ms before launching a rocket, to roll-back rocket-launching animation). Or in the simulations of the behaviour of the other players (peeking around the wall).