As a game grows, the hardest question is not about graphics or physics, but about how the code is organised. How do you represent an enemy that flies, one that shoots, one that flies and shoots, a crate that can be destroyed and one that can also be pushed? The Entity Component System (ECS) model was created precisely to answer this question flexibly.
The problem with deep inheritance
The intuitive approach, especially for people coming from classic object-oriented programming, is to build a class hierarchy:
Object
└─ Character
├─ Player
└─ Enemy
├─ FlyingEnemy
└─ ShootingEnemy
Trouble arrives when you need an enemy that flies and shoots. You have to duplicate code, resort to multiple inheritance, or move everything into the base class, which becomes huge and full of behaviour that most subclasses never use. After a few months of development the hierarchy becomes rigid: every new idea from the game designer requires restructuring it.
Composition instead of inheritance
ECS turns the perspective around. Instead of asking «what is» an object, you ask «what does it have». The model rests on three concepts.
Entities
An entity is just an identifier, often an integer. It contains no data and no logic: it is a label to which components are attached.
Components
A component is a small container of pure data, with no behaviour:
Position { x, y }Velocity { vx, vy }Sprite { image, frame }Health { points }Flying { altitude }Weapon { rate, damage }
An enemy that flies and shoots is simply an entity with Position, Velocity, Sprite, Health, Flying and Weapon. A destructible crate has Position, Sprite and Health. No hierarchy, no duplication.
Systems
A system contains the logic and operates on every entity that has a certain combination of components. The movement system, for example, processes all entities with Position and Velocity:
for each entity with (Position p, Velocity v):
p.x += v.vx * dt
p.y += v.vy * dt
The rendering system considers entities with Position and Sprite; the damage system those with Health. Each system does one thing and does it for every object concerned.
The practical advantages
Flexibility
Adding a behaviour means adding a component. If the game designer wants crates to be pushable, you just give them Velocity and a Pushable component, then write the matching system. Components can also be added or removed during play: a frozen enemy temporarily loses Velocity, and the movement system automatically stops moving it.
Performance
In data-oriented implementations, components of the same type are stored in contiguous arrays. When the movement system iterates over all positions and velocities, the processor reads sequential memory and makes the best use of its cache. With thousands of entities the difference compared with objects scattered in memory and reached through pointers can be significant.
Separation of concerns
Data and logic are cleanly separated. Saving the game, for instance, becomes simpler: you only need to serialise the components, which are pure data. Testing is easier too, because a system can be checked by feeding it a few hand-built entities.
Where ECS is less convenient
It is not a universal solution. Some aspects require care:
- Relationships between entities: a weapon held by a character, a carriage coupled to a train. These are handled with components holding another entity's identifier, but you must check that the linked entity still exists.
- System order: the input system must run before movement, movement before collisions. The order has to be defined explicitly.
- Very specific logic: a final boss with unique behaviour may be more readable as a dedicated script than as a set of generic components.
- Learning curve: for a small game with a few dozen objects, ECS can feel like needless complication.
ECS in popular engines
Engines adopt similar ideas in different forms. Unity offers the classic GameObject and MonoBehaviour model, based on components that contain logic, plus a separate data-oriented architecture for projects that need it. Godot is built on a tree of nodes, a different form of composition with similar goals. There are also standalone ECS libraries for many languages, useful if you work with a lightweight framework.
A design example
Suppose you need to build a small scrolling shooter. One possible breakdown:
| System | Required components |
|---|---|
| Input | PlayerControlled, Velocity |
| Movement | Position, Velocity |
| Shooting | Weapon, Position |
| Collisions | Position, Collider |
| Damage | Health, collision events |
| Rendering | Position, Sprite |
| Cleanup | ToDestroy |
The cleanup system, run last, removes entities marked during the frame. Destroying entities while other systems are still iterating over them is a classic source of bugs, and it is better to defer the operation to the end of the loop.
In short
- ECS replaces class hierarchies with composition: an object is defined by what it has.
- Entities are identifiers, components are pure data, systems are the logic.
- Adding behaviour means adding components, even during play.
- Components stored in contiguous arrays improve performance with many entities.
- System order, relationships and entity destruction must be designed carefully.