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
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:
- alecthomas’ entityx
- redxdev’s ECS
- google’s corgi (TIL google has an ECS implementation)
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:
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.
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:
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.
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.
Or you can make a helper is a bit more compact and allows for more efficient data access through iteration instead of lookups.
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.