How to Manage Game State Using ECS (Entity Component System)

Team 6 min read

#ecs

#gamedev

#architecture

#patterns

Introduction

Entity Component System (ECS) is a design pattern that helps you organize game state and logic in a way that scales with complexity. In ECS, you separate data (components) from behavior (systems) and treat game objects as collections of components (entities). This separation enables flexible composition, cache-friendly data layouts, and easier reasoning about gameplay systems.

What is ECS

  • Entities are lightweight identifiers.
  • Components are plain data structures representing aspects of an entity (position, velocity, health, input state, etc.).
  • Systems contain the logic that operates on entities with a matching set of components.

The key idea is to decouple data from behavior and to let systems run over archetypes—entities that share the same component composition—so that memory access patterns are predictable and cache-friendly.

Why ECS for game state

  • Decoupled logic: You can add or remove features by composing components without touching system code.
  • Flexible composition: New gameplay mechanics are created by mixing existing components rather than wiring new classes.
  • Scalable state management: Large numbers of entities can be processed efficiently thanks to data-oriented layouts.
  • Testability: Systems can be tested in isolation with mock component data.
  • Serialization: Components map naturally to save data, making persistence simpler.

Core concepts: Entities, Components, Systems

  • Entity: a unique ID that represents an object in the game world.
  • Component: a small, plain data holder (no behavior) attached to an entity.
  • System: a function or module that queries entities with a specific set of components and updates their data.

Example concept map:

  • Position: x, y
  • Velocity: vx, vy
  • Sprite: image or frame data
  • Health: current HP, max HP
  • Input: current input state
  • AIState: current AI behavior

Designing state with ECS: global vs per-entity state

  • Per-entity state: Most gameplay state lives in components attached to entities (e.g., Position, Velocity, Health).
  • Global or world state: Global rules, timers, or world-level data can be stored as resources or in a dedicated “World” entity. Systems read and mutate these resources as needed.
  • Marker components: Use empty components as tags to classify entities (e.g., PlayerTag, EnemyTag) without adding data.
  • Snapshot and replay: ECS makes it straightforward to capture entity-component snapshots for replay or debugging.

Practical patterns for managing game state

  • Data-oriented layouts: Store related component data contiguously for better cache locality.
  • Archetypes: Group entities by their component composition to minimize branching and improve iteration speed.
  • Event-like communication: Rather than direct calls between systems, publish and subscribe to events or use component flags to signal changes.
  • Time and delta: Systems usually receive a delta time (dt) to make updates frame-rate independent.
  • Serialization: Persist only the component data you care about, and reconstruct entities on load.

Example: Simple top-down shooter using ECS

Entities:

  • Player
  • Bullets
  • Enemies

Components:

  • Position { x, y }
  • Velocity { vx, vy }
  • Sprite { assetId, frame }
  • Input { up, down, left, right, shoot }
  • Health { hp, maxHp }
  • Collision { radius }
  • AISate { behavior }

Systems:

  • InputSystem: reads Input component and writes Velocity for movement; handles shooting by spawning new Bullet entities.
  • MovementSystem: updates Position using Velocity and dt.
  • CollisionSystem: detects overlaps using Position and Collision; applies damage or spawns pickups.
  • HealthSystem: clamps hp and handles death (removing entities or triggering explosion).
  • RenderingSystem: draws Sprites at Position.
  • BulletLifetimeSystem: removes bullets after traveling beyond a range or when they hit a target.
  • AISystem: updates enemy behavior based on AI state.

A compact illustration (TypeScript-like pseudo-code):

type Entity = number;

type Position = { x: number; y: number; };
type Velocity = { vx: number; vy: number; };
type Health = { hp: number; maxHp: number; };
type InputState = { up: boolean; down: boolean; left: boolean; right: boolean; shoot: boolean; };
type Sprite = { assetId: string; frame: number; };
type Collision = { radius: number; };

class World {
  nextId: number = 1;
  // Component stores
  positions = new Map<Entity, Position>();
  velocities = new Map<Entity, Velocity>();
  healths = new Map<Entity, Health>();
  inputs = new Map<Entity, InputState>();
  sprites = new Map<Entity, Sprite>();
  collisions = new Map<Entity, Collision>();
  // Image render pipeline, etc.
  // Global state
  gameTime: number = 0;
}

function MovementSystem(world: World, dt: number) {
  for (const [id, pos] of world.positions) {
    const vel = world.velocities.get(id);
    if (!vel) continue;
    pos.x += vel.vx * dt;
    pos.y += vel.vy * dt;
  }
}

function InputSystem(world: World, dt: number) {
  for (const [id, input] of world.inputs) {
    const vel = world.velocities.get(id) ?? { vx: 0, vy: 0 };
    vel.vx = (input.right ? 1 : 0) - (input.left ? 1 : 0);
    vel.vy = (input.down ? 1 : 0) - (input.up ? 1 : 0);
    world.velocities.set(id, vel);

    if (input.shoot) {
      // spawn bullet as a new entity with components
      const bullet = world.nextId++;
      world.positions.set(bullet, { x: world.positions.get(id)!.x, y: world.positions.get(id)!.y });
      world.velocities.set(bullet, { vx: 0, vy: -300 });
      world.healths.set(bullet, { hp: 1, maxHp: 1 });
      world.sprites.set(bullet, { assetId: "bullet", frame: 0 });
      world.collisions.set(bullet, { radius: 2 });
    }
  }
}

Note: This is a simplified illustration. Real ECS implementations use efficient storage (like dense arrays or sparse sets) and archetype-based iteration for performance.

Performance considerations

  • Archetypes matter: Entities sharing the same component set are stored contiguously, improving cache locality.
  • Avoid per-entity allocations within hot paths; favor preallocated storage.
  • Use sparse sets or packed arrays to efficiently query components.
  • Optimize systems to read data in a linear fashion and minimize branching inside tight loops.
  • Consider batch rendering and system ordering to minimize stalls.

Common pitfalls and tips

  • Overmixing data and behavior: Keep components pure data; avoid including logic in components.
  • Excessive component counts: Too many small components can fragment data; find a balance between granularity and performance.
  • Incorrect system dependencies: Ensure systems run in a deterministic order when needed (e.g., physics before collision resolution).
  • Over-abstracting too early: Start simple, then optimize with archetypes and layout refinements as needed.
  • Persistence complexity: When saving, serialize only essential component data and reconstruct entities on load carefully.

How to start implementing ECS in your project

  • Choose a naming and data layout strategy that fits your language (structs/classes for components in OOP languages, flat data arrays for data-oriented languages).
  • Start with a small, well-defined subset: a player entity with Position, Velocity, Health, and Input.
  • Implement a simple MovementSystem and InputSystem, then gradually add more systems.
  • Measure performance early using a small test scene and scale up.
  • Revisit serialization: design a simple save/load format that captures essential component data.

Conclusion

ECS offers a robust framework for managing game state by cleanly separating data from behavior. With careful design—clear entity definitions, purposeful components, and well-structured systems—you can build scalable, maintainable game logic that adapts to growing complexity. Start small, reason about your world composition, and expand with archetypes and optimization as your game’s needs evolve.