Basically, entities consist of components. Components should ideally just be data (a struct). Systems work on components (data), they are the code / functions. A system only cares about it's component (but for all entities, which have this component).
A entity "player" consists of the components like "texture", "input". A "monster" entity has components "texture", and "ai". The graphic processing done by the texture system doesnt care how the changes in x/y position is happening (via AI statemachine, or keyboard input).
Player and Monster should shoot too, so attach a "weapon" component to it. But then it's also possible to also create an entity with components "texture:door" and "portal" (portal system will teleport you to something, e.g. next level). No one stops you to attach the "ai" component too, making the door move around. And "weapon", which makes the door shoot too (then maybe call the entity something else, like portaling-monster).
As the components are just data stored outside your code, e.g. in files or a DB, you can change them at will. And load it, without recompiling or even restarting your game! This allows a large amount of creativity and innovation, without refactoring your code all the time.
I like to connect my systems with a message bus, where they send messages to each other. Messages change components, data, of entities. These may generate more messages. E.g. player clicking left mouse button creates a message, which gets handled by input system. This creates a message for the weapon system, reducing ammo count by one, and change its texture (-index) to like "shootingstance:1", and creating a new entity "bullet" with the source x/y and destination angle.
The advantage of this is that this requires less code, permits very good separation of concerns and is very transferrable even between engines. Unity, for example, uses something similar where components and systems are in the same class together. For self-coded games, there's several libraries too.
There's lots of great example code in the docs and within the repo itself, for what it's worth!
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).