ECS
Entity-Component-System (ECS) decouples DATA (components on entities) from LOGIC (systems that iterate over entities with specific components). The result: cache-friendly memory layout, easy parallelism, and clean composition — the architecture behind Unity DOTS, Bevy, EnTT, and most modern game engines.
Entity, Component, System, archetypes
EXAMPLE
// 1) The core idea
// Entity = an ID (just a number)
// Component = pure data (no behaviour) attached to entities
// System = function that operates on entities with a specific set of components
//
// 'A Bullet is an entity with Position, Velocity, Damage, Lifetime components.'
// 'A Player is an entity with Position, Velocity, Health, Input, Sprite components.'
//
// Same Position+Velocity components → same physics system handles both.
// 2) Vanilla JS ECS example
class Entity { static next = 0; static create() { return ++Entity.next; } }
class World {
constructor() {
this.components = new Map(); // componentName -> Map<entityId, data>
this.systems = [];
}
addComponent(entityId, name, data) {
if (!this.components.has(name)) this.components.set(name, new Map());
this.components.get(name).set(entityId, data);
}
removeComponent(entityId, name) {
this.components.get(name)?.delete(entityId);
}
query(...componentNames) {
const maps = componentNames.map((n) => this.components.get(n));
if (maps.some((m) => !m)) return [];
const smallest = maps.reduce((a, b) => a.size < b.size ? a : b);
const result = [];
for (const entityId of smallest.keys()) {
if (maps.every((m) => m.has(entityId))) {
result.push([entityId, ...maps.map((m) => m.get(entityId))]);
}
}
return result;
}
addSystem(fn) { this.systems.push(fn); }
update(dt) { for (const sys of this.systems) sys(this, dt); }
}
// 3) Components — just data
const Position = (x, y) => ({ x, y });
const Velocity = (vx, vy) => ({ vx, vy });
const Sprite = (image) => ({ image, w: 32, h: 32 });
const Health = (hp) => ({ hp, max: hp });
const Damage = (amt) => ({ amt });
const Lifetime = (sec) => ({ remaining: sec });
const Player = () => ({ }); // marker / tag
const Enemy = () => ({ });
// 4) Systems — pure functions over components
function MovementSystem(world, dt) {
for (const [, pos, vel] of world.query('Position', 'Velocity')) {
pos.x += vel.vx * dt;
pos.y += vel.vy * dt;
}
}
function LifetimeSystem(world, dt) {
for (const [id, life] of world.query('Lifetime')) {
life.remaining -= dt;
if (life.remaining <= 0) {
for (const m of world.components.values()) m.delete(id); // despawn
}
}
}
function CollisionDamageSystem(world) {
for (const [eid, ep, , eh] of world.query('Position', 'Enemy', 'Health')) {
for (const [, bp, dmg] of world.query('Position', 'Damage')) {
const dx = ep.x - bp.x, dy = ep.y - bp.y;
if (dx*dx + dy*dy < 100) {
eh.hp -= dmg.amt;
// remove the bullet entity (broker logic)
}
}
}
}
function RenderSystem(world, ctx) {
for (const [, pos, sp] of world.query('Position', 'Sprite')) {
ctx.drawImage(sp.image, pos.x - sp.w / 2, pos.y - sp.h / 2, sp.w, sp.h);
}
}
// 5) Setup
const world = new World();
world.addSystem(MovementSystem);
world.addSystem(LifetimeSystem);
world.addSystem(CollisionDamageSystem);
const player = Entity.create();
world.addComponent(player, 'Position', Position(100, 100));
world.addComponent(player, 'Velocity', Velocity(0, 0));
world.addComponent(player, 'Sprite', Sprite(playerImg));
world.addComponent(player, 'Health', Health(100));
world.addComponent(player, 'Player', Player());
function spawnEnemy(x, y) {
const e = Entity.create();
world.addComponent(e, 'Position', Position(x, y));
world.addComponent(e, 'Velocity', Velocity(-20, 0));
world.addComponent(e, 'Sprite', Sprite(enemyImg));
world.addComponent(e, 'Health', Health(20));
world.addComponent(e, 'Enemy', Enemy());
return e;
}
function shoot(from, dx, dy) {
const b = Entity.create();
world.addComponent(b, 'Position', Position(from.x, from.y));
world.addComponent(b, 'Velocity', Velocity(dx, dy));
world.addComponent(b, 'Sprite', Sprite(bulletImg));
world.addComponent(b, 'Damage', Damage(10));
world.addComponent(b, 'Lifetime', Lifetime(2));
}
// 6) Game loop
let last = performance.now();
function loop(t) {
const dt = Math.min(1/30, (t - last) / 1000);
last = t;
world.update(dt);
ctx.clearRect(0, 0, W, H);
RenderSystem(world, ctx);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// 7) Why ECS beats OOP for games
// • Composition over inheritance — 'TankPlayer extends Vehicle extends Entity' hierarchies don't scale
// • Data-oriented design — components stored contiguously in memory = cache-friendly
// • Parallelism — systems with disjoint component reads/writes run in parallel
// • Hot reload — swap a system function; world state survives
// • Replay + debugging — serialise component data, replay deterministically
// 8) Archetypes — advanced layout
// Group entities by SET of component types. All entities with the same set live in one chunk.
// Iteration: linear over the chunk = max cache hits.
// Adding/removing a component MOVES the entity to a new archetype chunk.
// Engines: Unity DOTS, Bevy, EnTT, flecs.
// 9) Tagging components
// Empty components serve as tags / queries: Player, Enemy, Dead, Boss.
// Cheap (no data), expressive filtering.
// 10) Real-world ECS frameworks
// • Bevy (Rust) — modern, ergonomic, plugin-based
// • EnTT (C++) — header-only, battle-tested
// • flecs (C/C++) — data-oriented, query language
// • Unity DOTS — first-party ECS for Unity (preview/stable depending on version)
// • bitecs / koota / ape-ecs (JavaScript) — varies in ergonomics and perf
// 11) Anti-patterns
// • Putting LOGIC in components (e.g. method on Position)
// • Inheritance between components — components compose; don't extend
// • One God Component holding all data — defeats the purpose; split it
// • Systems hold state — keep state in components; systems are pure transforms
// • Cross-system dependencies — order them explicitly; document
// 12) Performance tips
// • Reuse arrays + use typed arrays (Float32Array) for hot-path data
// • Avoid creating components per frame — pool entities + reuse
// • Profile system iteration count + time; archetype migration is expensive
// • Sort entities for spatial queries; broadphase + narrowphase collision
// 13) When ECS is overkill
// • Tiny games (jam, prototype) — OOP with composition is plenty
// • Few entities (< 100) — bench it; ECS overhead may dominate
// • UI / menus — usually trivial event handlers, not ECS
// 14) Networking
// • Replicate components, not entities, when efficient
// • Server authoritative; client interpolates or predicts
// • Component dirty flags for delta sync
// 15) Common bugs
// • System iteration WHILE inserting / removing entities → invalid iteration; queue changes for end-of-frame
// • Forgetting to despawn dead entities → memory growth
// • Mutable shared component references between entities — accidental sharing
// • Component name typos in query — silently 0 matches
// • Systems run in wrong order (physics before input, rendering before update) → visual glitches
// • Bypassing the ECS for 'quick' state (global mutable variables) → defeats determinism + replay
// • Mixing OOP scene graph with ECS — pick one and commit
Why it matters
ECS organises games around data: entities are IDs, components are pure data, systems iterate over entities with specific component sets. The result is cache-friendly memory, easy parallelism, and clean composition. Reach for it for any non-trivial game; for tiny prototypes, simpler OOP+composition is fine.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Entity-Component-System: data-oriented design. // Entities are IDs. Components are plain data. Systems iterate components. // Pros: cache-friendly, composable. Cons: indirection.Try it Yourself »
Discussion
Loading…