Many companies, like Unity and Apple are using Entity Component Systems (ECS) to make games because having tightly packed data leads to cache efficiency and great performance. ECS isn’t just great for performance though: it leads to good code-decoupling, easy composition, and lends itself to TDD and automated testing.
Entity Component Systems are Easy to Compose
In the standard Object Oriented Programming (OOP) approach to writing games, you often create extremely complex and deep inheritance trees; a Player
class might inherit from a SceneObject
, and that SceneObject
might inherit from a Transform
class, and that Transform
class might rely on a PlayerManager
class to even be instantiated. This can be quite complex to understand and write tests for.
In ECS this is modeled by using composition. The entity (E) is the ID of an entity, components (C) are data associated with that entity, and then systems (S) operate on and modify groups of components. You then build up functionality by adding multiple components to an entity, and creating systems that look for groups of components.
Example pseudocode:
PositionComponent {
x, y, z
}
WindComponent {
//No data - just used as a tag
}
WindSystem {
update() {
world.each(position: PositionComponent, wind: WindComponent) {
position.x++;
}
}
}
//Add entities to the world
playerEntity = world.addEntity(PositionComponent());
enemyEntity = world.addEntity(PositionComponent(), WindComponent());
//Add systems
world.addSystem(WindSystem());
while(true) {
//Update all the systems each frame
world.systems(system) {
system.update();
}
}
In this example code above the enemy entity will continuously move every frame while the player entity will not. This is because the WindSystem is looking for entities that have both a PositionComponent
and a WindComponent
. The great part about using composition is it's easy to add and remove functionality from entities.
So, if something happened that caused us to want the player to be affected by wind as well, we could simply add the WindComponent
to the player entity.
Example pseudocode:
world.addComponent(playerEntity, WindComponent());
We can also make the system itself slightly more complex by removing the WindComponent if a component reaches the side of the screen.
Example pseudocode:
WindSystem {
update() {
world.each(position: PositionComponent, wind: WindComponent) {
if (position.x > 100) {
world.removeComponent(wind)
} else {
position.x++;
}
}
}
}
These examples are slightly simplified, however even real systems tend to be small and focused because they only have one job to perform. This makes them easy to understand and reason about.
Entity Component Systems are Easy to Test
ECS also helps when it comes to automated testing. In OOP our class might have a model, animation, and all sorts of other data attached to it that we don’t necessarily want to load for every test. Whereas with composition we can just create the components and systems we are currently testing.
- Example tests that we could write for our WindSystem include:
- Entities with PositionComponent and WindComponent move the expected amount per update
- Entities without the WindComponent don't move
- Entities lose the WindComponent after x > 100
These tests are easy to write and fast to run since they don’t load or run any unnecessary logic.
Example pseudocode:
test(‘Entities without the WindComponent dont move’) {
position = PositionComponent();
world.addEntity(position);
world.addSystem(WindSystem());
startingPosition = position;
world.update();
expect(position == startingPosition);
}
Conclusion
ECS has a lot of benefits outside of performance that can really help create better code that is easier to reason about and modify. We’ve started using it at ClassDojo for some projects and are really enjoying it.
Does this sound like a better way to create games? Come join us in ClassDojo Engineering, we’re hiring!