Hacker News new | ask | show | jobs
by dj_mc_merlin 1587 days ago
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.

1 comments

The nice thing about using an event system is that you can just record all events. This allows you to record gameplay directly. This can be used as a demo, scripted scenes, and especially for debugging! Encountering a bug during playing? The recording can be used to reproduce the bug, as it will be a "pixel perfect" playback. Code until it is gone. Debugging is suddenly way easier, as you can just check the events wireshark/tcpdump style, and see what went wrong.

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).

Yep, I wasn't attacking event systems, they definitely allow a lot of cool behaviour. My approach was chosen since it was simpler, and for a solo project where I will have to dedicate months/years anyway, I choose to cut on non essential stuff that I might never use. I'd rather have something playable in a couple months to be able to prototype more, I can figure out what I need to add after.