ECS stands for Entity – Component – System, it’s a paradigm introduced in the industry more than a decade ago but even today it’s still being one of the most used options while designing games.
When building a game your code goes messy really fast, even faster if you haven’t defined everything clearly beforehand, games have interdependencies, connections, interactions and states that makes them really hard and tricky to design, and, when they grow they tend to be a messy hank of code and classes, here’s where ECS comes to the rescue.
The concept of ECS is pretty simple, it assumes that every single “entity” in the game (npcs, player, items, etc) is formed by common models or behaviours, so, if an NPC has a “movement” behaviour that takes the target location from an AI mechanism, the player has the same behivour but it’s target location is taken from the user input.
assuming that this hypothesis is right (and it’s right, almost), if you had enough pieces for storing the data (components) and enough mechanisms to execute actions based on the data of each entity (systems) you could build every single entity needed for your game.
How does it work?
The main idea is simple, of course, there are thousands of variations and each of them is used and has it’s own pros and cons, but the root is the same: You have an entity that stores components, then you have components that stores data, then you have some systems that runs entity by entity executing tasks depending on the component that entity have. Let’s see some pseudocode:
obj Component_position { x, y } obj Entity { list Components[Component_position] } fn System_movement (Entity){ if (Entity have Component_position) { Entity -> Components -> Component_position -> x ++ } } fn System_draw (Entity){ if (Entity have Component_position) { DrawRect(Entity -> Component_position) } } define Entity; gameloop( System_movement(Entity) System_draw (Entity) )
Now in C99
Well, first of all, this system is based in a proof of concept implementation that i wrote to check if an ECS could fit in my current project, a PSP rpg (“mount and blade”-like) game, so i had to have in mind some preconditions.
- The PSP has 32Mb RAM
- The PSP has 333MHz CPU
- I want my game to manage ~200 different items
I don’t care too much about CPU tho, indeed my main objective is to fit all the data models of my entities in just 1Mb of ram. Also, my implementation had to have near to 0 boilerplate. Let’s see.
First, the Components. I want my components to act as partial models for the entities, the Entity model will be built with components, components won’t have any logic or other components by themselves.
typedef struct { float x; float y; } ComponentPosition; typedef struct { int color; int size; } ComponentDraw;
Once we have the components we need a list, in C++ we might want to use a vector<> and, of course i could have used an array, but i instead used a data structure that holds pointers to all possible components, why?
This is a really bad idea, if the components list grows bigger it will be unmantainable, but i don’t want boilerplate.
Other issue with this design is that the CPU cache is almost not used, i’m storing an object that stores pointers, and those objects (Component structure) are stored inside the Entities, no cache no party
Serious ECS systems uses an UID correlation where the Entity is refered from the components that are in an array, this is pretty similar to data oriented design, it’s leveraged by CPU cache, is easier to mantain and doesn’t waste that much memory.
typedef struct { ComponentPosition *componentPosition; ComponentDraw *componentDraw; } Components;
Now the Entity itself, as i said before, the entity is just the data model for the element it represents in the game, in this case, that model is built by the components.
typedef struct { unsigned int uuid; Components components; } Entity;
Components has to be added to the entity so the model can be built, this is done programatically. As stated before, the component container have pointers so we need to “malloc” the components, is this a good idea? well, as long as the component container exists, this is the less memory-intensive way to do it.
void addComponentPosition(Entity *entity, int x, int y) { entity -> components.componentPosition = malloc(sizeof(ComponentPosition)); entity -> components.componentPosition -> x = x; entity -> components.componentPosition -> y = y; } void addComponentDraw(Entity *entity, int color, int size) { entity -> components.componentDraw = malloc(sizeof(ComponentDraw)); entity -> components.componentDraw -> color = color; entity -> components.componentDraw -> size = size; }
And of course, we need a way to free the memory.
void freeEntity(Entity *entity) { if (entity -> components.componentPosition) free(entity -> components.componentPosition); if (entity -> components.componentDraw) free(entity -> components.componentDraw); }
Now we can define our entity. Entity definitions are like recipes.
Entity npc; addComponentPosition(&npc, 0, 0); addComponentDraw(&npc, 0xFF00FF, 20);
Then the systems, in my case i wanted them to be as granular as possible, even if it’s not good for my CPU performance, i wanted to be able to test the architecture as a hybrid solution, so i wanted tiny specialized systems. As we are replicating the example above we will need two systems, one to update the position and the other to draw the entity.
I want my game state to have as little logic as possible, so the systems have to be able to run on every single entity, even if that entity doesn’t have the needed data.
int updatePositionSystem(Entity *entity) { if (!entity -> Components.componentPosition) return 1; entity -> Components.componentPosition -> x++; } int DrawSystem(Entity *entity) { if (!entity -> Components.componentPosition || ! entity -> Components.componentDraw) return 1; drawRectangle( entity -> Components.componentPosition -> x, entity -> Components.componentPosition -> y, entity -> Components.componentDraw -> size, entity -> Components.componentDraw -> color ); }
Then those systems are called in the game loop, if there are more entities, the systems are called inside the entities loop, so each system is called once for each entity. In a real-world example we would see some systems running in threads and iterating the components at it’s own, in my case, systems are used inside the loop for simplicity.
Cool, right? This is just an approach, it’s not even the best approach, it’s not even a good one but i think it helps to clarify what a ECS is and how it works.
Conclusion
Is ECS the final perfect answer for all our game-development problems?
The answer is simple, NO. ECS is just an architecture, and, as always, it’s good for some projects and bad for others. As you can see, it might be good when you are building something big, with tons of different entities that have to interact with each other or with the game world, but the boilerplate of a proper ECS system makes it unsuitable for little projects.
On the other hand, even with large code bases, once the ECS is implemented and stable the development is much more fluid and easy, entity definition is just a recipe as you can notice in the examples, so you can build new ones in a couple lines. It also helps with projects where live changes are key, do you want to change the controllable character? just delete the UserInputComponent from the character and add it to other entity.
Hope it helped