Entity Component System (ECS)
Entity Component System (ECS)
Definition: A game architecture pattern that composes game objects out of small, reusable data components instead of deep inheritance hierarchies, favoring composition over inheritance.
How It Works
- Entities are just IDs, with no behavior or data of their own, usually a plain integer or handle into a table
- Components are plain data attached to an entity (Position, Health, Sprite), with no logic inside them at all
- Systems hold the actual logic, operating on every entity that has a matching set of components (a MovementSystem processes every entity with both Position and Velocity)
- Storage is typically flat arrays per component type, and an entity is really just an index into those arrays, not an object
- Giving an entity new behavior means attaching a component, not writing a new subclass or overriding a method
- Systems run in a defined order each frame: input, AI, movement, collision, render, and each only touches the component types it declares
- Many implementations group entities that share an identical component set into an “archetype” table, so a query becomes “find the matching archetypes” instead of scanning every entity individually
- Adding or removing a component at runtime usually moves the entity between archetype tables behind the scenes, which is why doing it mid-iteration is unsafe in most implementations
Under the Hood
MovementSystem never touches Health or Sprite, it only queries entities that have both Position and Velocity. Adding a new “Poison” status effect later means writing a PoisonComponent and a PoisonSystem, no existing entity’s class needs to change, and no existing system needs to know poison exists.
Worked example 1: cache behavior at scale
- Given: 100,000 entities, each needing a position update this frame
- Step: classic inheritance layout stores each GameObject as one ~512-byte object (transform, renderer, colliders, scripts all bundled together); updating position touches 100,000 × 512 bytes = 51.2 MB scattered across memory, most of each 64-byte cache line fetched is wasted on unrelated fields
- Step: ECS layout stores Position and Velocity as two flat arrays of 12 bytes each; updating all 100,000 entities touches 100,000 × 24 bytes = 2.4 MB, read sequentially in order
- Answer: roughly 20x less memory traffic, and the access pattern is sequential and prefetchable instead of pointer-chased, which is why data-oriented ECS implementations (Unity DOTS, EnTT) outperform naive OOP hierarchies once entity counts get large
Worked example 2: querying by component mask
- Given: a scene with 5,000 entities; 1,200 have Renderable, 300 have AI, all 5,000 have Transform
- Step: each entity stores a bitmask of which components it owns (bit 0 = Transform, bit 1 = Renderable, bit 2 = AI)
- Step: RenderSystem’s query mask is
Transform | Renderable; it testsentityMask & queryMask == queryMaskper entity, or an archetype table groups entities by exact component set so the system iterates only the matching group directly - Answer: RenderSystem processes exactly 1,200 entities, not 5,000, with no per-field type checks, just one integer comparison or a pre-sorted archetype bucket
Worked example 3: system ordering dependency
- Given: DamageSystem reduces Health, and DeathSystem checks Health <= 0 to despawn an entity, both run in the same frame
- Step: if DeathSystem runs before DamageSystem in the frame’s system order, an entity that took lethal damage this frame still shows a stale Health value and survives one extra frame before despawning
- Step: if DamageSystem runs first, DeathSystem sees the updated Health immediately and despawns the entity in the same frame the damage landed
- Answer: system execution order is part of the design, not an implementation detail, most engines let you declare explicit “runs before/after” dependencies between systems instead of relying on registration order
Implementation Approaches
| Approach | How lookups work | Strength | Weakness |
|---|---|---|---|
| Archetype table | Entities with identical component sets stored together in one contiguous block | Very fast iteration, used by Unity DOTS | Adding/removing a component moves the whole entity’s data |
| Sparse set | Each component type has a dense array plus a sparse index array for O(1) lookup | Cheap add/remove, used by EnTT | Iteration slightly less cache-optimal than archetypes |
| Bitmask query | Each entity carries a bitmask, systems compare it against a query mask | Simple to implement | O(n) scan cost grows with total entity count |
Why It Matters
- Sidesteps the classic “does a flying enemy that also swims inherit from FlyingEnemy or SwimmingEnemy” problem, composition has no such dead end
- Data-oriented layout, grouping same-type data contiguously in memory, tends to run faster than deep object hierarchies once entity counts get large
- Systems and components can be tested and reused independently of any specific entity type, a PoisonSystem written once works on players, enemies, or destructible crates alike
- Encourages designers to think in terms of “what data does this need” rather than “what class does this belong to,” which scales better as a game’s content grows
Common Pitfalls
- Adopting ECS for a small, simple game where a straightforward object-oriented approach would ship faster and stay easier to reason about
- Designing components that are too coarse, bundling unrelated data together, which loses the flexibility ECS is meant to provide
- Letting systems reach into each other’s components directly instead of through their declared queries, which quietly reintroduces tight coupling
- Forgetting that component removal/addition mid-frame can invalidate iterators in some ECS implementations, corrupting the current system’s pass
- Splitting components too finely, so a simple update requires touching a dozen tiny arrays and the bookkeeping overhead outweighs the cache benefit
- Treating systems as a dumping ground for unrelated logic just because it happens to touch the same component, defeating the single-responsibility benefit ECS is meant to give
Comparison
| Aspect | Classic Inheritance | Component-based OOP (e.g. MonoBehaviour) | Data-Oriented ECS |
|---|---|---|---|
| Data layout | Per-object, mixed fields | Per-object, attached scripts | Flat arrays per component type |
| New behavior | Subclass or override | Attach a script component | Attach a data component + system |
| Cache friendliness | Poor at scale | Moderate, still pointer-heavy | High, contiguous iteration |
| Cross-cutting logic | Hard (diamond problem) | Easier, still per-object overhead | Natural, systems query freely |
| Memory overhead per object | High, full vtable + fields | Moderate, one object per script | Low, only the component fields exist |
| Multithreading | Difficult, shared mutable state | Difficult, shared mutable state | Natural, systems declare which components they touch |
| Learning curve | Familiar to most programmers | Familiar, gentle | Steeper, unfamiliar mental model |
Example
Unity’s DOTS/ECS package, Unreal’s Mass Entity system, and the open-source EnTT library (C++) are production implementations of this pattern. Unity DOTS was built specifically to push large-scale simulations (thousands of units on screen at once) past what MonoBehaviour-based GameObjects could sustain at a stable frame rate. A “Player” entity might have Position, Sprite, Health, and PlayerInput components, while an “Enemy” entity shares Position, Sprite, and Health but has an AIComponent instead of PlayerInput, letting MovementSystem and HealthSystem serve both without either entity needing a shared base class:
for entity in query(Position, Velocity):
entity.Position += entity.Velocity * deltaTime
That loop is the entire MovementSystem, it works identically whether it runs against 10 entities or 100,000.
Common Interview Questions
- What problem does ECS solve that inheritance doesn’t? — the combinatorial explosion of subclasses needed to express every combination of behaviors (flying, swimming, armored, and so on)
- Why are components required to hold no logic? — so any system can operate on any combination of components without needing to know which “class” an entity conceptually belongs to
- What’s an archetype? — a table grouping every entity that shares an identical set of component types, used to make queries fast
- Why does ECS tend to outperform OOP at scale? — contiguous, same-type data in memory means CPU cache lines are fully used instead of mostly wasted on unrelated fields
- Is ECS always the right choice? — no, its benefits show up at hundreds or thousands of entities; a game with a handful of unique objects rarely needs it
- How does ECS interact with save/load systems? — cleanly, since components are already plain data, serializing an entity is often just serializing the union of its component values
Migration Note
Retrofitting ECS onto an existing OOP codebase is rarely a clean rewrite, most teams migrate incrementally, converting one performance-critical system (like particle simulation or crowd AI) at a time while leaving stable, non-performance-critical gameplay code in its existing object-oriented form.
Related Terms
Referenced by