Hacker News new | ask | show | jobs
by steve_coral 1581 days ago
Could you possibly post a pseudo-code example of how the above would work with ECS? How do you attach the components to the rock object if not inheritance? Would you just have them be functions that you call to include within the rock via a header file or something? Or a separate code module you import, if using Python?
4 comments

To give a slightly different example.

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.

ECS is sort of an in-memory database. The rock is not a class in the code: it's just an entity in the game that has X, Y and Z components "attached" to it, dynamically. Components are just structs. And finally Systems are just functions that runs once-per-frame doing something on entities that have a certain components on them. Normally, entities/components are serialised into some format so it's not static in the code.

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.

I'm currently building a game engine using flecs as my ECS: https://github.com/SanderMertens/flecs

There's lots of great example code in the docs and within the repo itself, for what it's worth!

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.

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.