Guide
Entity component system (ECS) in games explained
A browser RPG with hundreds of NPCs, a tower-defense wave with fifty enemy
types, or a provably-fair dice table with animated chips — all need a way to
represent many things that share some behavior but not all of it.
Classic object-oriented inheritance ("Enemy extends Character extends
Entity") collapses under that combinatorics. The
entity component system (ECS) pattern answers with a simple
split: entities are lightweight IDs, components
are plain data bags, and systems are functions that process
matching components each frame. This guide explains why ECS exists, how
storage layouts affect performance, how queries and scheduling work, where
engines like Bevy and Unity DOTS apply it, and how ECS fits alongside your
game loop
and collision pipeline.
Why deep inheritance trees fail
Early game tutorials model everything as classes. A Player class
gets health, position, and input handling. An Enemy subclass adds
AI. A FlyingEnemy adds gravity exemption. A
ExplodingFlyingEnemy adds a timer. Soon you have a fragile
hierarchy where:
- Features combine explosively — "poisonous, flying, loot-dropping boss" does not map cleanly to one subclass without multiple inheritance or interface soup.
- Unused data ships everywhere — a static crate inherits
velocity fields it never uses because it shares a
PhysicsObjectancestor. - Cross-cutting updates are awkward — rendering every drawable object means walking unrelated branches or maintaining duplicate sprite lists.
- Cache performance suffers — objects scattered on the heap with vtables cause pointer chasing; see CPU caches and memory hierarchy for why contiguous arrays win on hot loops.
ECS flips the design: instead of "what is this object?" you ask "what data does it carry, and which systems should touch that data this frame?"
The three building blocks
Entities
An entity is usually an integer ID (sometimes paired with a generation counter to detect stale references). It has no behavior and almost no fields — it is a handle that groups components together. Creating an entity means allocating an ID; destroying one removes all its components and recycles the ID when safe.
Components
A component is a struct of data: Position { x, y },
Velocity { dx, dy }, Health { current, max },
Sprite { atlasId, frame }. Components should not contain game logic
in strict ECS — no update() methods. Keeping them as plain data
(POD structs) lets the engine lay them out in memory for fast iteration and
makes serialization, networking snapshots, and replay determinism easier.
Systems
A system is a function (or scheduled job) that runs over every
entity matching a query — for example, all entities with both
Position and Velocity. A movement system might do
position += velocity * dt for thousands of rows in a tight loop.
A render system collects Position + Sprite pairs and submits draw
calls. A health system reacts when Health.current <= 0 and
queues a despawn event.
Composition replaces inheritance: a flying, poisonous boss is just an entity
with Position, Velocity, Health,
AIController, PoisonAura, and BossTag
components — no new class required.
Data-oriented design and memory layout
ECS is the practical face of data-oriented design (DOD): optimize
for how the CPU reads memory, not for how humans draw UML diagrams. When a
movement system touches 10,000 entities, you want 10,000 consecutive
Velocity floats, not 10,000 heap objects each pointing at disparate
fields.
Engines achieve this with two common storage strategies:
- Sparse sets / struct-of-arrays (SoA) — each component type lives in its own dense array indexed by entity ID. Adding or removing a component updates bookkeeping tables. Iteration is extremely fast when systems touch one or two component types. Libraries like EnTT and flecs popularized this approach.
- Archetypes — entities with the same component signature are stored together in a table (rows = entities, columns = component types). Adding a component may move the entity to a new archetype table. Unity DOTS and Bevy use archetype-style storage; queries over matching archetypes scan contiguous chunks with SIMD-friendly layouts.
Both approaches beat naive "bag of pointers" OOP for hot systems. Cold data (quest flags read once per minute) can stay in side maps without polluting every archetype.
Queries, filters, and tags
Systems declare queries — which component sets they need:
- With — required components (must be present).
- Without — exclusion filters (e.g. process
Velocitybut skip entities markedPaused). - Changed — optional optimization: run only when a component was modified since last frame (common in editors and networking).
Tag components are zero-size markers: PlayerTag,
EnemyTag, StaticGeometry. They carry no fields but
let queries target subsets cheaply. Layer masks in
collision detection
map naturally to tag + collider component pairs.
Queries are usually compiled at startup into iterator functions so the hot path does not hash strings every frame.
System scheduling and parallelism
In a single-threaded game, systems run in a defined order inside the update phase of the game loop: input, then physics integration, then collision broad phase, then narrow phase, then gameplay rules, then rendering. ECS engines expose a scheduler that orders systems and detects dependencies.
If system A writes Velocity and system B reads
Position derived from velocity, B must run after A. Independent
systems — particle animation vs UI layout — can run in parallel on worker
threads when the storage layout prevents data races (each archetype chunk
locked, or read-only phases). This is where ECS pairs well with
thread pools
on multi-core desktops and consoles.
Browser games often stay single-threaded on the main canvas thread for simplicity; ECS still helps by making system order explicit and batching work for fewer cache misses. Heavy physics can offload to a WebAssembly module that reads the same SoA buffers.
ECS in production engines
- Bevy (Rust) — archetype ECS with a plugin graph and parallel scheduler; popular for indie and jam games targeting WASM.
- Unity DOTS — Entities, IComponentData, and Systems (or ISystem in newer APIs) with Burst compilation for SIMD; integrates with GameObjects through conversion workflows.
- flecs / EnTT (C/C++) — lightweight libraries embedded in custom engines; flecs adds an optional entity-relationship graph for hierarchies (parent/child transforms).
- Custom minimal ECS — many 2D web games implement a trimmed version: arrays of components keyed by entity ID, a handful of systems, no archetype migration. That is valid when entity counts stay below a few thousand.
Full engine ECS adds command buffers (defer structural changes
— add/remove components — until the end of a system pass to avoid mutating
tables mid-iteration), events (collision callbacks, damage
dealt), and resources (singleton components like
GameTime or InputState).
ECS vs traditional OOP — when to use which
Prefer ECS when you have many homogeneous entities, hot per-frame loops (movement, particles, AI steering), need deterministic replays, or want parallel system execution. Strategy sims, bullet hells, MMO-style crowds, and physics-heavy scenes benefit most.
Stay with OOP or scene graphs when entity counts are tiny (under ~200), behavior is highly unique per object (narrative adventure with hand-authored scripts), or your team already ships in an engine centered on GameObjects/Nodes. Hybrid designs are common: ECS for simulation cores, OOP wrappers for editor tooling and UI.
Migrating mid-project is painful — adopt ECS at the prototype stage or isolate it to one subsystem (projectiles, debris) first.
Integrating ECS with other game subsystems
ECS does not replace everything. Typical boundaries:
- Rendering — a render system gathers drawable components and builds instance batches; the GPU API layer stays outside ECS.
- Physics — export positions/velocities to a physics world
(Box2D, Rapier) each frame, or implement collision as systems reading
Collidercomponents. - Audio — one-shot events from gameplay systems enqueue sounds; the audio engine mixes on its own thread.
- Networking — replicate component deltas, not whole objects; entity IDs become the wire protocol's stable handles.
For provably-fair or wallet-native games, keep outcome resolution in a small, auditable system that reads bet state components and writes result components — separate from animation or VFX systems so verification code stays minimal.
Common pitfalls
- Putting logic inside components — erodes cache benefits and makes parallelization harder; keep components as data.
- Over-fragmenting components — fifty one-field components inflate query bookkeeping; group fields that always change together.
- Structural changes in hot loops — spawning 500 entities inside a system that iterates the same archetype causes table moves; batch spawns via command buffers.
- Stale entity references — storing entity IDs without generation checks leads to use-after-free bugs; use handles or weak refs.
- God systems — one 2,000-line system that does AI, combat, and loot; split by query and schedule order instead.
- Ignoring editor ergonomics — designers need hierarchy and prefabs; pure ECS without tooling frustrates content teams.
Key takeaways
- ECS models games as entities (IDs) + components (data) + systems (logic), favoring composition over inheritance.
- Data-oriented storage (SoA or archetypes) keeps hot loops cache-friendly and enables parallelism.
- Queries and schedulers define which entities each system touches and in what order relative to physics, collision, and render.
- Use ECS for large, similar crowds; use hybrids or OOP for small, bespoke scenes.
- Integrate ECS at subsystem boundaries — simulation inside, I/O (render, audio, network) outside.
Related reading
- Game loop and frame timing — where systems run each frame and how fixed timestep interacts with simulation
- Collision detection in games — broad/narrow phases as systems over collider components
- WebAssembly (WASM) explained — offloading hot ECS systems to compiled modules in the browser
- CPU caches and memory hierarchy — why SoA component storage wins on modern processors