iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…