Over on my YouTube channel I’ve been trying to write a game from scrach for the past year. There’s been some success, and I’ve managed to make a moving character with a tile-based world to explore. The whole process has been quite unstructured and mildly chaotic, the code just evolving as I’ve needed more functionality.
As it was getting more and more difficult to add new features, and the code seeming to grow exponentially longer I’ve gone back to the drawing board and started applying some computer science techniques.
This has now become less about making a game from scratch, and more about engineering better code and being more thoughtful with my approach. I started by creating UML diagrams to organise the code before writing any. Now I’m working on making parts of my code modular and isolated from each other.
Enter the ECS pattern
My last video was about software design patterns - ways of thinking and structuring software to solve common issues. There are algorithms we can reuse if we want to do a common task like sorting or searching through data. Equally there are ways of thinking and structuring our code if we wanted to do higher level things.
They’re all explained in a well known computing book called Design Patterns Elements of Reusable Object-Oriented Software, and then discussed and repeated endlessly throughout the Internet. 30 seconds of Google or a brief conversation with ChatGPT will tell you everything. You’re probably even using some of the patterns without realising.
And I can apply these patterns to my game.
For example, my game has the player, enemies and all sorts of moving objects to manage. Clearly I need some kind of “manager” to organise this.
When I first managed game objects, I used a manager class. The manager created objects, stored them, and let them interact. The problem was that this made everything tightly coupled:
- Each object needed to know about the manager.
- The manager needed to know about every object.
- Objects often ended up knowing about each other through the manager.
This made the code harder to change, extend, or reuse, because changing one class often meant updating several others.
On one of my previous videos someone left this comment that got me thinking about using a pattern to solve my problem.
![[Pasted_image_20250930202728.png]]
The Entity Component System (ECS) solves this by separating responsibilities:
- Entities are just IDs.
- Components hold data.
- Systems act on components.
Now objects don’t depend on each other or on a central manager. They just hold data, and systems define behavior. This makes the engine more flexible and decoupled.
Instead of there being large objects that contain functions, logic and data things are split up and everything becomes more data oriented. This fits C really well as trying to make an object oriented manager work without classes leads to a mess of structs and function pointers.
How others have done it
Knowing I wanted to make an ECS I did what any programmer does - hits ChatGPT and asks it to write all the code by itself. No, I went and did a bit of research first, just to see if this was a legitimate idea and whether others had done it before me.
There’s always a risk when approaching new concepts that you might accidentally be using a 30 year old idea that nobody uses, that’s been replaced by something much better.
I found these links to others trying to do the same thing. It seems there’s no specifc way to do this. That’s one of the good things about design patterns, the implementation is up to you and your specific problem.
https://denyskryvytskyi.github.io/event-system
https://gamedev.stackexchange.com/questions/172584/how-could-i-implement-an-ecs-in-c
https://www.reddit.com/r/C_Programming/comments/1fs9lj5/general_ecs_in_c/
Turns out it’s a valid idea and has been used in various ways. I was still a bit unsure on how to actually implement an ECS, so I went off for a bit of a chat with one of our friendly AI overlords. Not to write all the code for me, but more to explore the idea.
Me: I am designing an entity management system for my game. It needs to track multiple items and the data about them - where they are, animation state, etc. What software design patterns might help me create a well organised system that can be expanded later. For example later I need to add in collision detection
ChatGPT: Good question — the design of your entity management system will shape how easy it is to add things like collision, AI, and rendering later. A few well-known software design patterns are particularly useful for game entities:
- Component-Based (Entity-Component System, ECS)
Idea: Instead of a rigid inheritance tree (e.g.,
Player -> Enemy -> MovingEnemy -> FlyingEnemy
), entities are just IDs with components attached (e.g.,Position
,Sprite
,Animation
,Physics
).Benefits:
- Easy to extend (just add a new component type).
- Systems (e.g., CollisionSystem, RenderSystem) operate on entities with the right components.
- Promotes separation of concerns — position logic isn’t tangled with animation logic.
When to use: If you want flexibility and expect lots of entity types with overlapping features.
Me: I am writing in C, not C++ so does that make some of these patterns unusable, and others more relevant?
ChatGPT: Yes — writing in C changes things quite a bit. Some object-oriented patterns assume inheritance, virtual methods, or automatic polymorphism, which you don’t have in C. But you can still implement the ideas of those patterns with C techniques: structs, function pointers, arrays, and modules. Here’s how the patterns translate:
Patterns that map well to C ✅ Entity–Component System (ECS)
- Very natural in C:
- Entities are just integer IDs.
- Components are arrays of structs (
Position[ENTITY_MAX]
,Animation[ENTITY_MAX]
).- Systems are plain functions that loop over those arrays.
- No need for inheritance → data-oriented design fits C well.
Read the rest of it on this archived page of the chat
My plan
I made entities responsible for their own collisions. Each entity held pointers to other entities and a collision callback. This caused problems because:
- Every entity needed to know about others.
- Adding new entity types required editing existing ones.
- Collision logic was spread out and hard to maintain.
My code looked like this
typedef struct Entity {
int x, y, w, h;
struct Entity* other; // pointer to another entity
void (*onCollision)(struct Entity* self, struct Entity* other);
} Entity;
void checkCollision(Entity* a, Entity* b) {
if (a->x < b->x + b->w && a->x + a->w > b->x &&
a->y < b->y + b->h && a->y + a->h > b->y) {
// Each entity calls its own collision handler
if (a->onCollision) a->onCollision(a, b);
if (b->onCollision) b->onCollision(b, a);
}
}
The manager would run checkCollision
on every pair of entities in the game, then the entities would be responsible for dealing with collision.
Instead, I am breaking the systems out into different parts, each responsible for one piece of functionality and nothing else.
Entity Manager
The Entity Manager no longer maintains everything. Its sole purpose is to track and create new entities as needed.
And entities themselves don’t actually exist. An entity
is nothing more than an index into the entity_registry
array which merely stores which entities exist.
int em_get_unused()
{
for (int i = 0; i < MAX_ENTITIES; i++) {
if (entity_registry[i].enabled == false) {
entity_registry[i].enabled = true;
return i;
}
}
return -1;
}
int em_create(EntityType_e type, int x, int y)
{
int id = em_get_unused();
if (id == -1) return -1;
entity_registry[id].type = type;
entity_registry[id].sprite_def = sprite_defs[type];
switch (type) {
case ENTITY_TYPE_PLAYER:
entity_player_create(id, x, y);
break;
case ENTITY_TYPE_TEST_ENEMY:
entity_test_enemy_create(id, x, y);
break;
case ENTITY_TYPE_MAX:
default:
return -1;
}
}
Entities
Despite the point of the ECS being all about decoupled systems, it’s hard to explain entities without also explaining components, but explaining components also requires explaining entities. So let’s start with entities.
In the ECS an entity doesn’t exist as a specific object or collection of data. Instead it’s an index. The entity manager has a function to find an unused entity index from a list it maintains. This index is passed into a factory method that actually creates the required entity.
For example a player entity can be created that uses a position and velocity component. If the next free entity in the entity manager is “3” then element 3 in all component arrays now belong to the player, if they are being used. If not, they get ignored.
int entity_player_create(int entity_id, int x, int y)
{
positions[entity_id].active = true;
velocities[entity_id].active = true;
behaviours[entity_id].active = true;
positions[entity_id].x = x;
positions[entity_id].y = y;
velocities[entity_id].xv = 0;
velocities[entity_id].yv = 0;
behaviours[entity_id].type = BEHAVIOUR_PLAYER;
}
Components
If an entity is nothing more than an index into an array, a component is one of those arrays. Each array holds structs that describe the data being used by that component.
For example there is a position component to store x and y position, and a velocity component to store x and y velocity.
typedef struct {
bool active;
int x;
int y;
} Position_t;
void positions_init();
extern Position_t positions[MAX_ENTITIES];
the ‘active’ field is used when the system iterates through the array.
Systems
The final part of this is the system. A system is a collection of logic that does something with the components.
For example the physics system uses the position and velocity components to compute where the player will move. And then updates the position component for each entity.
If a particular entity doesn’t have a position or velocity component, its active
field will be false, and that entry in the arrays will be skipped.
void sys_fake_physics()
{
// Move with velocity
for (int i = 0; i < MAX_ENTITIES; i++) {
if (entity_registry[i].enabled == true) {
if (positions[i].active == true && velocities[i].active == true) {
positions[i].x += velocities[i].xv * 5;
positions[i].y += velocities[i].yv * 5;
}
}
}
}
The design of this specifically keeps all systems and components separated. The components don’t know they’re part of a system, and a system only needs to access the specific components it requires. And because these are just arrays in C, there is no complex getting and setting logic, or passing pointers to data structures deep into the code.
Behaviours
Game logic is also a component, I decided to call it “behaviour” and there is a corresponding behaviour system.
The system looks at the behaviour type to work out what code to run.
void sys_behaviour()
{
for (int i = 0; i < MAX_ENTITIES; i++) {
if (ENTITY_ENABLED(i)) {
SKIP_IF_MISSING(behaviours,i);
switch (behaviours[i].type) {
case BEHAVIOUR_PLAYER: behaviour_player(i); break;
case BEHAVIOUR_TEST_ENEMY: behaviour_test_enemy(i); break;
case BEHAVIOUR_NONE:
default: break;
}
}
}
}
Currently the player’s behaviour is to confine itself to the screen boundaries. Later I can add more complex logic here.
void behaviour_player(int entity_id)
{
helper_screenbounds(entity_id);
}
Helpers
Some code - like bouncing off the screen edges - might be used in more than one system or behaviour, so it’s separated into a helper function. This makes writing game logic more modular as I can include helpers into the behaviour logic where needed.
void helper_screenbounds(int entity_id)
{
if (positions[entity_id].x < 16)
positions[entity_id].x = 16;
else if (positions[entity_id].x + entity_registry[entity_id].sprite_def->width > 304)
positions[entity_id].x = 304-entity_registry[entity_id].sprite_def->width;
if (positions[entity_id].y < 16)
positions[entity_id].y = 16;
else if (positions[entity_id].y + entity_registry[entity_id].sprite_def->height > 224)
positions[entity_id].y = 224-entity_registry[entity_id].sprite_def->height;
}
Main game loop
The main game loop now becomes incredibly simple. Instead of complex logic to manage the running of the game, the code is reduced to simple declarative instructions or function calls.
void game_init(void)
{
em_create(ENTITY_TYPE_PLAYER, 100, 100);
em_create(ENTITY_TYPE_TEST_ENEMY, 50, 150);
em_create(ENTITY_TYPE_TEST_ENEMY, 200, 50);
}
GameStates_e game_update(void)
{
sys_key_control();
sys_fake_physics();
sys_behaviour();
if (IS_KEY_PRESSED(KEY_SPACE))
return GAME_STATE_TITLE;
return GAME_STATE_REMAIN;
}
void game_draw(void)
{
sys_debug_draw();
}