Intro

Entity Component Systems (ECS) are all the rage these days as an architectural alternative that emphasises composition over inheritance. In this post I won’t be explaining the concept much as there are plenty other resources for that. Implementing an ECS can be done in a number of ways, and is often done in somewhat complex ways which as far as I have seen often confuse newcomers to the concept and also take time to implement.

In this post I will describe a super simple way of implemeting an ECS that requires almost no code for a functional version but stays true to the concept.

ECS

People seem to sometimes refer to slightly different things when they say ECS. When I talk about ECS, I mean a system that lets you define entities that have zero or more pure data components belonging to them. These components are selectively processed by systems that are pure logic. For example, an entity E could have the components position, velocity, hitbox and health attached to it, and these do nothing more than storing the data. For example the health component might store two integers, one for current health and one for max health. A system could be a health regeneration system that finds all health component instances and increment them by 1 every 120 frames for example.

Typical Implementations in C++

There are many libraries that provide ECS implementations, and from what I’ve seen, most of the time implementations often involve one or more of:

  • Inheritance of a base Component/System class GravitySystem : public ecs::System
  • Quite heavy template usage
  • Both of the above in some CRTP fashion
  • An EntityManager class that manages the creation/storage of entities in an opaque way

Some examples from quick googling:

While these are completely valid approaches, they also have drawbacks. The way that they opaquely handle things means that it can be very hard to reason about what exactly is going on underneath and how much of a performance hit it is. It also means that you have to learn the whole abstraction layer and make sure that it fits well into your existing code. There might also be bugs hiding there since there seems to be quite a bit of code involved that you have to spend time debugging.

Strongly template based approaches might affect compile times and how often you need to rebuild, while inheritance based concepts might potentially hurt performance.

The biggest reason why I think that such approaches are too much is that the problem they solve is actually very very simple. In the end it is just optional data components associated with an entity, and selective processing on those. Below I will show a very simple way of achieving this.

My Simple Approach

The Entity

Some approaches define an Entity class, others just work with entities as an ID/handle. With a component based approach, an entity is nothing else than the components associated to it and we don’t need a class for that. An entity will implicitly exist based on the components associated with it. For this we define:

using EntityID = int64_t; //for this post's purpose, int64_t is an arbitrary choice

Entity Components

Components are meant to be different kinds of data associated with existing entities. You could say that for every entity e, e is going to have zero or more of the available component types. This is basically a per-component key-value relationship and fortunately there are tools for this in the standard library in form of maps.

With this, I define our components as follows.

struct Position
{
    float x;
    float y;
};

struct Velocity
{
    float x;
    float y;
};

struct Health
{
    int max;
    int current;
};

template <typename Type>
using ComponentMap = std::unordered_map<EntityID, Type>;

using Positions = ComponentMap<Position>;
using Velocities = ComponentMap<Velocity>;
using Healths = ComponentMap<Health>;

struct Components
{
    Positions positions;
    Velocities velocities;
    Healths healths;
};

This is enough to represent entities using components - just as expected from an ECS. For example, to spawn an entity with a position and health but no velocity, we can do:

//given a Components instance c
EntityID newID = /*obtain new entity ID*/;
c.positions[newID] = Position{0.0f, 0.0f};
c.healths[newID] = Health{100, 100};

To destroy an entity of a given ID, we just .erase() it from every map.

Systems

The last part that we need is the systems part. This is the logic that operates on the components to attain game specific behaviour. Since I like to keep things simple, I just use normal functions. The health regeneration system mentioned above could simply be the following function.

void updateHealthRegeneration(int64_t currentFrame, Healths& healths)
{
    if(currentFrame % 120 == 0)
    {
        for(auto& [id, health] : healths)
        {
            if(health.current < health.max)
                ++health.current;
        }
    }
}

We can put a call to this function at a fitting place in our main loop and pass it the health component storage. Since the health storage only contains entries for entities that actually do have health, it can process these in isolation. This also means that this function takes only the data it needs and doesn’t touch any irrelevant data.

What about if a system operates on more than one component? Say a physics system that moves the position based on velocity. For this we need to do an intersection of the keys of all component types involved and iterate the values of those. At this point, the standard lib is a bit lacking but it’s not difficult to write helpers for this. For example.

void updatePhysics(Positions& positions, const Velocities& velocities)
{
    //this is a variadic function template taking N maps, and returning
    //a set of IDs that are present in all maps given
    std::unordered_set<EntityID> targets = mapIntersection(positions, velocities);

    //now targets will only contain entries that have both
    //position and velocity so it is safe to access the maps
    for(EntityID id : targets)
    {
        Position& pos = positions.at(id);
        const Velocity& vel = velocities.at(id);

        pos.x += vel.x;
        pos.y += vel.y;
    }
}

Or you can make a helper is a bit more compact and allows for more efficient data access through iteration instead of lookups.

void updatePhysics(Positions& positions, const Velocities& velocities)
{
    //this is a variadic function template that will work out
    //the intersection of the keys of the maps. It will then iterate these
    //keys and pass the data from the maps directly to the given functor
    intersectionInvoke<Position, Velocity>(positions, velocities,
        [] (EntityID id, Position& pos, const Velocity& vel)
        {
            pos.x += vel.x;
            pos.y += vel.y;
        }
    );
}

At this point we have covered the basic functionality of a typical ECS.

Benefits

This approach is very powerful since it builds from the ground-up without restricting abstractions. You don’t need to integrate any external libs or adapt your codebase around pre-defined ideas of how Entities/Components/Systems should be.

Since it’s completely transparent you can also build whatever utilities and helpers around this, so it is an approach that can grow alongside your project’s needs. For simple prototypes or game jam games, chances are that you don’t even need any further functionality than the above at all.

Furthermore, if you are new to the whole ECS thing, I hope that you can more easily understand the idea through this approach, as it is very straight-forward.

Limitations

As with all approaches there are limitations. In my experience, this particular implementation using unordered_map will for any non-trivial game quickly run into performance issues.

Doing a key intersection iteration accross several unordered_map instances with many entities will scale badly since you will essentially do N*M lookups where N is the amount of components you are intersecting and M is the amount of matching entities, and unordered_map is not very cache friendly. This can be remedied by replacing unordered_map with a more iteration-friendly key-value storage.

Another limitation is boilerplating. Depending on what you do, it might become tedious to define new components. You might need to add not only the declaration in the Components struct, but maybe you need to add them to spawning functions, serialisation functions, debug utility functions, and so on. I myself ran into this and I solved it by using code generation, where I define my components in external json files and then generate the C++ components along with helper functions as a build step. You can probably also figure out various templating based approaches to remedy whatever boilerplate issues you run into if any.

More Reading

Will contain links to future posts.