Game Programming Patterns

Why Patterns Matter in Games

Design patterns are not invented so much as recognised: they are names for solutions that experienced developers keep independently reaching for, given as a shared vocabulary so a whole approach can be pointed at in a single word — Robert Nystrom, Game Programming Patterns [1].

Games are unique software systems: real-time, state-heavy, performance-critical, and content-driven. Patterns help manage this complexity.


1. Command Pattern

The Command pattern turns "do a thing" into an object, rather than a direct function call baked into an input handler. That sounds like unnecessary indirection until you need any of the features that fall out of it almost for free: remapping keys at runtime, recording input for a replay, letting the same code accept input from a human or an AI, or rewinding time. All of those are hard to bolt onto code that reads keys and calls player methods directly; they're nearly trivial once every action is a small object that knows how to execute (and optionally undo) itself.

The Problem

The naive version below hard-wires each key directly to a player method. It works, right up until a designer asks for remappable controls, or a feature needs the player's actions recorded for replay, or an AI needs to drive the same character — at which point every one of those becomes a separate special case bolted onto a function that was never designed to be observed or intercepted.

# ❌ Naive input handling — tightly coupled, hard to extend
def handle_input(player):
    if key_pressed(Key.W):
        player.move_forward()
    if key_pressed(Key.S):
        player.move_backward()
    if key_pressed(Key.SPACE):
        player.jump()
    if key_pressed(Key.E):
        player.interact()
    # Adding: remapping, macros, replay, AI control → nightmare
// ❌ Naive input handling — tightly coupled, hard to extend
void handleInput(Player& player) {
    if (keyPressed(Key::W)) player.moveForward();
    if (keyPressed(Key::S)) player.moveBackward();
    if (keyPressed(Key::Space)) player.jump();
    if (keyPressed(Key::E)) player.interact();
    // Adding: remapping, macros, replay, AI control → nightmare
}
// ❌ Naive input handling — tightly coupled, hard to extend
void handleInput(Player player) {
    if (keyPressed(Key.W)) player.moveForward();
    if (keyPressed(Key.S)) player.moveBackward();
    if (keyPressed(Key.SPACE)) player.jump();
    if (keyPressed(Key.E)) player.interact();
    // Adding: remapping, macros, replay, AI control → nightmare
}
// ❌ Naive input handling — tightly coupled, hard to extend
void HandleInput(Player player)
{
    if (KeyPressed(Key.W)) player.MoveForward();
    if (KeyPressed(Key.S)) player.MoveBackward();
    if (KeyPressed(Key.Space)) player.Jump();
    if (KeyPressed(Key.E)) player.Interact();
    // Adding: remapping, macros, replay, AI control → nightmare
}
# ❌ Naive input handling — tightly coupled, hard to extend
def handle_input(player)
  player.move_forward  if key_pressed?(Key::W)
  player.move_backward if key_pressed?(Key::S)
  player.jump          if key_pressed?(Key::SPACE)
  player.interact      if key_pressed?(Key::E)
  # Adding: remapping, macros, replay, AI control → nightmare
end

The Solution: Command Pattern

Instead, each action becomes its own small class implementing a shared Command interface: an execute method that performs the action, and an optional undo that reverses it. Nothing about a JumpCommand or a MoveForwardCommand needs to know how it got triggered — a key press, a network message, an AI decision, or a recorded replay can all call the same execute method identically.

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional

@dataclass
class Entity:
    velocity: 'Vec3' = None
    speed: float = 5.0
    jump_force: float = 10.0
    is_grounded: bool = True

@dataclass
class Vec3:
    x: float = 0.0
    y: float = 0.0
    z: float = 0.0

# Command interface
class Command(ABC):
    @abstractmethod
    def execute(self, entity: Entity) -> None:
        pass

    def undo(self, entity: Entity) -> None:
        """Optional: for replay/time-rewind"""
        pass

# Concrete commands
class MoveForwardCommand(Command):
    def execute(self, entity: Entity) -> None:
        if entity.velocity:
            entity.velocity.z += entity.speed * 0.016  # dt ≈ 16ms

    def undo(self, entity: Entity) -> None:
        if entity.velocity:
            entity.velocity.z -= entity.speed * 0.016

class JumpCommand(Command):
    def execute(self, entity: Entity) -> None:
        if entity.is_grounded and entity.velocity:
            entity.velocity.y = entity.jump_force

class InteractCommand(Command):
    def execute(self, entity: Entity) -> None:
        print("Interacting with nearby object")
#include <memory>
#include <vector>

struct Vec3 {
    float x = 0.0f, y = 0.0f, z = 0.0f;
};

struct Entity {
    Vec3 velocity{};
    float speed = 5.0f;
    float jumpForce = 10.0f;
    bool isGrounded = true;
};

// Command interface
class Command {
public:
    virtual ~Command() = default;
    virtual void execute(Entity& entity) = 0;
    virtual void undo(Entity& entity) {}  // Optional: for replay/time-rewind
};

// Concrete commands
class MoveForwardCommand : public Command {
public:
    void execute(Entity& entity) override {
        entity.velocity.z += entity.speed * 0.016f;  // dt ≈ 16ms
    }
    void undo(Entity& entity) override {
        entity.velocity.z -= entity.speed * 0.016f;
    }
};

class JumpCommand : public Command {
public:
    void execute(Entity& entity) override {
        if (entity.isGrounded) entity.velocity.y = entity.jumpForce;
    }
};

class InteractCommand : public Command {
public:
    void execute(Entity& entity) override {
        // entity.interactWithNearby();
    }
};
import java.util.*;

public final class Vec3 {
    public float x, y, z;
    public Vec3() {}
    public Vec3(float x, float y, float z) { this.x = x; this.y = y; this.z = z; }
}

public class Entity {
    public final Vec3 velocity = new Vec3();
    public float speed = 5.0f;
    public float jumpForce = 10.0f;
    public boolean isGrounded = true;
}

// Command interface
public interface Command {
    void execute(Entity entity);
    default void undo(Entity entity) {}  // Optional: for replay/time-rewind
}

// Concrete commands
public final class MoveForwardCommand implements Command {
    @Override
    public void execute(Entity entity) {
        entity.velocity.z += entity.speed * 0.016f;
    }
    @Override
    public void undo(Entity entity) {
        entity.velocity.z -= entity.speed * 0.016f;
    }
}

public final class JumpCommand implements Command {
    @Override
    public void execute(Entity entity) {
        if (entity.isGrounded) entity.velocity.y = entity.jumpForce;
    }
}

public final class InteractCommand implements Command {
    @Override
    public void execute(Entity entity) {
        // entity.interactWithNearby();
    }
}
using System;

public readonly struct Vec3 {
    public readonly float X, Y, Z;
    public Vec3(float x = 0, float y = 0, float z = 0) { X = x; Y = y; Z = z; }
}

public class Entity {
    public Vec3 Velocity { get; set; } = new Vec3();
    public float Speed = 5.0f;
    public float JumpForce = 10.0f;
    public bool IsGrounded = true;
}

public interface ICommand {
    void Execute(Entity entity);
    void Undo(Entity entity);
}

public abstract class Command : ICommand {
    public virtual void Undo(Entity entity) {}  // Optional: for replay/time-rewind
    public abstract void Execute(Entity entity);
}

public sealed class MoveForwardCommand : Command {
    public override void Execute(Entity entity) {
        entity.Velocity = new Vec3(entity.Velocity.X, entity.Velocity.Y, entity.Velocity.Z + entity.Speed * 0.016f);
    }
    public override void Undo(Entity entity) {
        entity.Velocity = new Vec3(entity.Velocity.X, entity.Velocity.Y, entity.Velocity.Z - entity.Speed * 0.016f);
    }
}

public sealed class JumpCommand : Command {
    public override void Execute(Entity entity) {
        if (entity.IsGrounded) entity.Velocity = new Vec3(entity.Velocity.X, entity.JumpForce, entity.Velocity.Z);
    }
}

public sealed class InteractCommand : Command {
    public override void Execute(Entity entity) {
        // entity.InteractWithNearby();
    }
}
Vec3 = Struct.new(:x, :y, :z) do
  def initialize(x = 0.0, y = 0.0, z = 0.0) = super
end

Entity = Struct.new(:velocity, :speed, :jump_force, :is_grounded) do
  def initialize(velocity: nil, speed: 5.0, jump_force: 10.0, is_grounded: true)
    super(velocity, speed, jump_force, is_grounded)
  end
end

# Command interface
class Command
  def execute(entity) = raise NotImplementedError

  # Optional: for replay/time-rewind
  def undo(entity); end
end

# Concrete commands
class MoveForwardCommand < Command
  def execute(entity)
    entity.velocity.z += entity.speed * 0.016 if entity.velocity # dt ≈ 16ms
  end

  def undo(entity)
    entity.velocity.z -= entity.speed * 0.016 if entity.velocity
  end
end

class JumpCommand < Command
  def execute(entity)
    entity.velocity.y = entity.jump_force if entity.is_grounded && entity.velocity
  end
end

class InteractCommand < Command
  def execute(_entity)
    puts "Interacting with nearby object"
  end
end

Input Mapping (Decoupled)

With commands as objects, the input handler's only job is to maintain a lookup from key to command and dispatch into it — nothing in this class knows or cares what any individual command actually does. That decoupling is also what makes recording a command history straightforward: every dispatched command can simply be appended to a list, giving replay and undo for free from a structure that exists anyway.

from typing import Dict
from abc import ABC

class InputHandler:
    def __init__(self):
        self.key_bindings: Dict[int, Command] = {}
        self.command_history: list[Command] = []  # For undo/replay
        self.history_index = 0

    def bind(self, key: int, cmd: Command) -> None:
        self.key_bindings[key] = cmd

    def handle_input(self, entity) -> None:
        for key, cmd in self.key_bindings.items():
            if is_pressed(key):
                cmd.execute(entity)
                # Record for replay/undo
                if self.history_index < len(self.command_history):
                    self.command_history = self.command_history[:self.history_index]
                self.command_history.append(cmd)
                self.history_index += 1

    def undo(self, entity) -> None:
        if self.history_index > 0:
            self.history_index -= 1
            self.command_history[self.history_index].undo(entity)

    def save_replay(self, filename: str) -> None:
        pass  # Serialize command_history to file

    def load_replay(self, filename: str) -> None:
        pass  # Deserialize from file

def is_pressed(key: int) -> bool:
    return False  # Placeholder
#include &lt;unordered_map&gt;
#include &lt;vector&gt;
#include &lt;memory&gt;

class InputHandler {
    std::unordered_map&lt;int, std::unique_ptr&lt;Command&gt;&gt; keyBindings;
    std::vector&lt;std::unique_ptr&lt;Command&gt;&gt; commandHistory;  // For undo/replay
    size_t historyIndex = 0;

public:
    void bind(int key, std::unique_ptr&lt;Command&gt; cmd) {
        keyBindings[key] = std::move(cmd);
    }

    void handleInput(Entity& player) {
        for (auto& [key, cmd] : keyBindings) {
            if (isPressed(key)) {
                cmd->execute(player);
                // Record for replay/undo
                if (historyIndex < commandHistory.size()) {
                    commandHistory.resize(historyIndex);
                }
                commandHistory.push_back(cmd->clone());  // Need virtual clone()
                historyIndex++;
            }
        }
    }

    void undo(Entity& entity) {
        if (historyIndex > 0) {
            historyIndex--;
            commandHistory[historyIndex]->undo(player);
        }
    }

    // Replay system: serialize commandHistory to file
    void saveReplay(const std::string& filename);
    void loadReplay(const std::string& filename);

private:
    bool isPressed(int key) { return false; }  // Placeholder
};
import java.util.*;

public class InputHandler {
    private final Map<Integer, Command> keyBindings = new HashMap<>();
    private final List<Command> commandHistory = new ArrayList<>();
    private int historyIndex = 0;

    public void bind(int key, Command cmd) {
        keyBindings.put(key, cmd);
    }

    public void handleInput(Entity player) {
        for (var entry : keyBindings.entrySet()) {
            if (isPressed(entry.getKey())) {
                Command cmd = entry.getValue();
                cmd.execute(player);
                // Record for replay/undo
                if (historyIndex < commandHistory.size()) {
                    commandHistory.subList(historyIndex, commandHistory.size()).clear();
                }
                commandHistory.add(cmd);
                historyIndex++;
            }
        }
    }

    public void undo(Entity entity) {
        if (historyIndex > 0) {
            historyIndex--;
            commandHistory.get(historyIndex).undo(entity);
        }
    }

    public void saveReplay(String filename) { /* Serialize commandHistory */ }
    public void loadReplay(String filename) { /* Deserialize from file */ }

    private boolean isPressed(int key) { return false; }
}
using System;
using System.Collections.Generic;

public class InputHandler {
    private readonly Dictionary<int, ICommand> keyBindings = new();
    private readonly List<ICommand> commandHistory = new();
    private int historyIndex = 0;

    public void Bind(int key, ICommand cmd) {
        keyBindings[key] = cmd;
    }

    public void HandleInput(Entity player) {
        foreach (var kvp in keyBindings) {
            if (IsPressed(kvp.Key)) {
                kvp.Value.Execute(player);
                // Record for replay/undo
                if (historyIndex < commandHistory.Count) {
                    commandHistory.RemoveRange(historyIndex, commandHistory.Count - historyIndex);
                }
                commandHistory.Add(kvp.Value);
                historyIndex++;
            }
        }
    }

    public void Undo(Entity entity) {
        if (historyIndex > 0) {
            historyIndex--;
            commandHistory[historyIndex].Undo(entity);
        }
    }

    public void SaveReplay(string filename) { /* Serialize commandHistory */ }
    public void LoadReplay(string filename) { /* Deserialize from file */ }

    private bool IsPressed(int key) => false;
}
class InputHandler
  def initialize
    @key_bindings = {}
    @command_history = [] # For undo/replay
    @history_index = 0
  end

  def bind(key, cmd)
    @key_bindings[key] = cmd
  end

  def handle_input(entity)
    @key_bindings.each do |key, cmd|
      next unless pressed?(key)

      cmd.execute(entity)
      # Record for replay/undo
      @command_history = @command_history[0...@history_index]
      @command_history << cmd
      @history_index += 1
    end
  end

  def undo(entity)
    return unless @history_index > 0

    @history_index -= 1
    @command_history[@history_index].undo(entity)
  end

  def save_replay(filename); end # Serialise command_history to file
  def load_replay(filename); end # Deserialise from file

  private

  def pressed?(_key)
    false # Placeholder
  end
end

Player Remapping (Runtime)

Remappable controls, the feature that's usually the whole reason for reaching for this pattern, now falls out of the design almost incidentally. A PlayerConfig is just a mapping from logical actions to keys, and applying it means binding fresh command instances to whatever keys the player has chosen — the input handler and the commands themselves need no changes at all to support it.

from dataclasses import dataclass
from enum import IntEnum

class Key(IntEnum):
    W = 87
    S = 83
    SPACE = 32
    E = 69
    SHIFT = 16

@dataclass
class PlayerConfig:
    move_forward: int = Key.W
    move_backward: int = Key.S
    jump: int = Key.SPACE
    interact: int = Key.E
    dodge: int = Key.SHIFT

    def apply_to(self, input_handler: InputHandler) -> None:
        input_handler.bind(self.move_forward, MoveForwardCommand())
        input_handler.bind(self.move_backward, MoveBackwardCommand())
        input_handler.bind(self.jump, JumpCommand())
        input_handler.bind(self.interact, InteractCommand())
        input_handler.bind(self.dodge, DodgeCommand())
enum class Key : int {
    W = 87, S = 83, Space = 32, E = 69, Shift = 16
};

struct PlayerConfig {
    Key moveForward = Key::W;
    Key moveBackward = Key::S;
    Key jump = Key::Space;
    Key interact = Key::E;
    Key dodge = Key::Shift;

    void applyTo(InputHandler& input) {
        input.bind(moveForward, std::make_unique&lt;MoveForwardCommand&gt;());
        input.bind(moveBackward, std::make_unique&lt;MoveBackwardCommand&gt;());
        input.bind(jump, std::make_unique&lt;JumpCommand&gt;());
        input.bind(interact, std::make_unique&lt;InteractCommand&gt;());
        input.bind(dodge, std::make_unique&lt;DodgeCommand&gt;());
    }
};
public enum Key {
    W(87), S(83), SPACE(32), E(69), SHIFT(16);
    public final int value;
    Key(int v) { value = v; }
}

public record PlayerConfig(
    Key moveForward,
    Key moveBackward,
    Key jump,
    Key interact,
    Key dodge
) {
    public PlayerConfig() {
        this(Key.W, Key.S, Key.SPACE, Key.E, Key.SHIFT);
    }

    public void applyTo(InputHandler input) {
        input.bind(moveForward.value, new MoveForwardCommand());
        input.bind(moveBackward.value, new MoveBackwardCommand());
        input.bind(jump.value, new JumpCommand());
        input.bind(interact.value, new InteractCommand());
        input.bind(dodge.value, new DodgeCommand());
    }
}
public enum Key { W = 87, S = 83, Space = 32, E = 69, Shift = 16 }

public record PlayerConfig(
    Key MoveForward = Key.W,
    Key MoveBackward = Key.S,
    Key Jump = Key.Space,
    Key Interact = Key.E,
    Key Dodge = Key.Shift
) {
    public void ApplyTo(InputHandler input) {
        input.Bind((int)MoveForward, new MoveForwardCommand());
        input.Bind((int)MoveBackward, new MoveBackwardCommand());
        input.Bind((int)Jump, new JumpCommand());
        input.Bind((int)Interact, new InteractCommand());
        input.Bind((int)Dodge, new DodgeCommand());
    }
}
module Key
  W = 87
  S = 83
  SPACE = 32
  E = 69
  SHIFT = 16
end

PlayerConfig = Struct.new(:move_forward, :move_backward, :jump, :interact, :dodge) do
  def initialize(move_forward: Key::W, move_backward: Key::S, jump: Key::SPACE,
                 interact: Key::E, dodge: Key::SHIFT)
    super(move_forward, move_backward, jump, interact, dodge)
  end

  def apply_to(input_handler)
    input_handler.bind(move_forward, MoveForwardCommand.new)
    input_handler.bind(move_backward, MoveBackwardCommand.new)
    input_handler.bind(jump, JumpCommand.new)
    input_handler.bind(interact, InteractCommand.new)
    input_handler.bind(dodge, DodgeCommand.new)
  end
end

Command Pattern Benefits in Games

None of the features below were the original motivation for adopting the pattern in any one project — they're what a team discovers it gets for free once commands already exist as objects rather than direct calls. That's the general case for design patterns worth knowing well: the first payoff that justifies the pattern is rarely the last one it delivers.

Feature Implementation
Remappable controls Swap commands in keymap at runtime
Replay system Serialise command history to file
Time rewind Execute undo() in reverse (Braid-style)
Macro/scripting Sequence commands, bind to single key
AI control AI outputs same Command interface
Multiplayer Send commands over network (deterministic)
Undo/Redo editor Level editor uses same system

2. Entity-Component-System (ECS)

Entity-Component-System is games' answer to a problem that shows up the moment a project's object hierarchy grows past a handful of classes: single inheritance forces every new combination of behaviour into an awkward choice about where in the tree it belongs. ECS replaces "is-a" inheritance with "has-a" composition — an entity is nothing but an ID, behaviour lives in components (pure data) and systems (pure logic operating on components), and a new combination of behaviour is just a new set of components attached to an existing entity, no new class required.

The Problem with Deep Inheritance

The classic symptom is the "diamond problem" scaled up across a whole game's cast: once FlyingEnemy exists as a subclass several levels deep, there's nowhere for a flying player character to inherit from without either duplicating code or breaking the hierarchy that everything else depends on. Every new gameplay combination — a swimming enemy, an invisible ally, a giant slow-moving boss — risks the same collision.

# ❌ Inheritance hierarchy hell
class Entity: ...
class Actor(Entity): ...
class Pawn(Actor): ...
class Character(Pawn): ...
class Enemy(Character): ...
class FlyingEnemy(Enemy): ...

# Want flying player? Can't inherit from both!
// ❌ Inheritance hierarchy hell
class Entity { /* ... */ };
class Actor : public Entity { /* ... */ };
class Pawn : public Actor { /* ... */ };
class Character : public Pawn { /* ... */ };
class Enemy : public Character { /* ... */ };
class FlyingEnemy : public Enemy { /* ... */ };
// Want flying player? Can't inherit from both!
// ❌ Inheritance hierarchy hell
class Entity { /* ... */ }
class Actor extends Entity { /* ... */ }
class Pawn extends Actor { /* ... */ }
class Character extends Pawn { /* ... */ }
class Enemy extends Character { /* ... */ }
class FlyingEnemy extends Enemy { /* ... */ }
// Want flying player? Can't inherit from both!
// ❌ Inheritance hierarchy hell
class Entity { /* ... */ }
class Actor : Entity { /* ... */ }
class Pawn : Actor { /* ... */ }
class Character : Pawn { /* ... */ }
class Enemy : Character { /* ... */ }
class FlyingEnemy : Enemy { /* ... */ }
// Want flying player? Can't inherit from both!
# ❌ Inheritance hierarchy hell
class Entity; end
class Actor < Entity; end
class Pawn < Actor; end
class Character < Pawn; end
class Enemy < Character; end
class FlyingEnemy < Enemy; end

# Want a flying player? Can't inherit from both!
# (Ruby modules help, but data-driven composition scales further)

ECS Solution: Composition Over Inheritance

Components below carry only data — a Transform, a Velocity, a Health — with no methods of their own, and an entity is just whatever combination of these a registry has attached to a given ID. A flying player is simply an entity with both PlayerControl and whatever component governs flight; nothing about the inheritance tree has to change to allow it, because there is no inheritance tree. Systems then do the actual work, each one processing every entity that happens to have the specific combination of components it cares about — MovementSystem only ever touches entities with both Transform and Velocity, regardless of what else those entities are.

from dataclasses import dataclass
from typing import Dict, Type, Any

# Components = pure data (no behavior)
@dataclass
class Transform:
    position: tuple = (0, 0, 0)
    rotation: tuple = (0, 0, 0)
    scale: tuple = (1, 1, 1)

@dataclass
class Velocity:
    x: float = 0.0
    y: float = 0.0
    z: float = 0.0

@dataclass
class Health:
    current: float = 100.0
    maximum: float = 100.0

@dataclass
class Sprite:
    texture_id: str
    uv_rect: tuple = (0, 0, 1, 1)

@dataclass
class AIController:
    behavior_tree: str

@dataclass
class PlayerControl:
    input_handler: Any = None

# Entities = IDs (integers)
Entity = int

# Systems = behavior (operate on component combinations)
class MovementSystem:
    def update(self, registry: Dict[Entity, Dict[Type, Any]], dt: float) -> None:
        for entity, components in registry.items():
            if Transform in components and Velocity in components:
                transform = components[Transform]
                velocity = components[Velocity]
                x, y, z = transform.position
                transform.position = (
                    x + velocity.x * dt,
                    y + velocity.y * dt,
                    z + velocity.z * dt
                )

class RenderSystem:
    def update(self, registry: Dict[Entity, Dict[Type, Any]]) -> None:
        for entity, components in registry.items():
            if Transform in components and Sprite in components:
                transform = components[Transform]
                sprite = components[Sprite]
                # renderer.draw(sprite.texture_id, transform.position, ...)
#include &lt;unordered_map&gt;
#include &lt;vector&gt;
#include &lt;typeindex&gt;
#include &lt;memory&gt;

// Components = pure data (no behavior)
struct Transform { Vec3 pos, rot, scale; };
struct Velocity { Vec3 value; };
struct Health { float current, max; };
struct Sprite { std::string tex; Rect uv; };
struct AIController { BehaviorTree bt; };
struct PlayerControl { InputHandler* input; };

// Entities = IDs
using Entity = uint64_t;

// Archetype-based storage (EnTT style)
class Registry {
    std::unordered_map&lt;Entity, std::unordered_map&lt;std::type_index, std::unique_ptr&lt;void&gt;&gt;&gt; components;

public:
    template&lt;typename T, typename... Args&gt;
    T& emplace(Entity e, Args&&... args) {
        auto& map = components[e];
        auto ptr = std::make_unique&lt;T&gt;(std::forward&lt;Args&gt;(args)...);
        T* ref = ptr.get();
        map[std::type_index(typeid(T))] = std::move(ptr);
        return *ref;
    }

    template&lt;typename T&gt;
    T* get(Entity e) {
        auto it = components.find(e);
        if (it == components.end()) return nullptr;
        auto cit = it->second.find(std::type_index(typeid(T)));
        return cit != it->second.end() ? static_cast&lt;T*&gt;(cit->second.get()) : nullptr;
    }

    template&lt;typename... Components&gt;
    class View {
        Registry& reg;
    public:
        View(Registry& r) : reg(r) {}

        template&lt;typename Func&gt;
        void each(Func&& f) {
            // Iterate entities with all Components...
        }
    };

    template&lt;typename... Components&gt;
    View&lt;Components...&gt; view() { return View&lt;Components...&gt;(*this); }
};

// Systems = behavior
class MovementSystem {
    void update(Registry& reg, float dt) {
        reg.view&lt;Transform, Velocity&gt;().each([&](Entity e, Transform& t, Velocity& v) {
            t.pos += v.value * dt;
        });
    }
};

class RenderSystem {
    void update(Registry& reg) {
        reg.view&lt;Transform, Sprite&gt;().each([&](Entity e, Transform& t, Sprite& s) {
            renderer.draw(s.tex, t.pos, t.rot, s.uv);
        });
    }
};

class AISystem {
    void update(Registry& reg, float dt) {
        reg.view&lt;Transform, AIController&gt;().each([&](Entity e, Transform& t, AIController& ai) {
            ai.bt.tick(e, reg, dt);
        });
    }
};
import java.util.*;
import java.util.function.*;

// Components = pure data (records for immutability)
public record Transform(Vec3 pos, Vec3 rot, Vec3 scale) {}
public record Velocity(Vec3 value) {}
public record Health(float current, float max) {}
public record Sprite(String tex, Rect uv) {}
public record AIController(BehaviorTree bt) {}
public record PlayerControl(InputHandler input) {}

// Entity = ID
public record Entity(long id) {}

// Archetype-based storage (similar to flecs/EnTT)
public class Registry {
    private final Map&lt;Entity, Map&lt;Class&lt;?&gt;, Object&gt;&gt; components = new HashMap&lt;&gt;();

    public &lt;T&gt; T emplace(Entity e, T component) {
        components.computeIfAbsent(e, k -> new HashMap&lt;&gt;())
                  .put(component.getClass(), component);
        return component;
    }

    public &lt;T&gt; T get(Entity e, Class&lt;T&gt; type) {
        var map = components.get(e);
        return map != null ? type.cast(map.get(type)) : null;
    }

    public &lt;T&gt; Iterable&lt;Entity&gt; entitiesWith(Class&lt;T&gt;... types) {
        return () -> components.entrySet().stream()
            .filter(e -> Arrays.stream(types).allMatch(t -> e.getValue().containsKey(t)))
            .map(Map.Entry::getKey)
            .iterator();
    }
}

// Systems = behavior
public class MovementSystem {
    public void update(Registry reg, float dt) {
        for (Entity e : reg.entitiesWith(Transform.class, Velocity.class)) {
            Transform t = reg.get(e, Transform.class);
            Velocity v = reg.get(e, Velocity.class);
            t.pos = t.pos.add(v.value.mul(dt));
        }
    }
}

public class RenderSystem {
    public void update(Registry reg) {
        for (Entity e : reg.entitiesWith(Transform.class, Sprite.class)) {
            Transform t = reg.get(e, Transform.class);
            Sprite s = reg.get(e, Sprite.class);
            renderer.draw(s.tex, t.pos, t.rot, s.uv);
        }
    }
}
using System;
using System.Collections.Generic;

// Components = pure data (records/structs)
public readonly record struct Transform(Vec3 Pos, Vec3 Rot, Vec3 Scale);
public readonly record struct Velocity(Vec3 Value);
public readonly record struct Health(float Current, float Max);
public readonly record struct Sprite(string Tex, Rect UV);
public readonly record struct AIController(BehaviorTree BT);
public readonly record struct PlayerControl(InputHandler Input);

public readonly record struct Entity(ulong ID);

// Archetype-based storage
public class Registry {
    private readonly Dictionary&lt;Entity, Dictionary&lt;Type, object&gt;&gt; components = new();

    public T Emplace&lt;T&gt;(Entity e, T component) where T : notnull {
        if (!components.TryGetValue(e, out var map)) {
            map = new Dictionary&lt;Type, object&gt;();
            components[e] = map;
        }
        map[typeof(T)] = component;
        return component;
    }

    public T Get&lt;T&gt;(Entity e) {
        return components.TryGetValue(e, out var map) && map.TryGetValue(typeof(T), out var obj)
            ? (T)obj : default;
    }

    public IEnumerable&lt;Entity&gt; EntitiesWith(params Type[] types) {
        foreach (var kvp in components) {
            if (types.All(t => kvp.Value.ContainsKey(t))) yield return kvp.Key;
        }
    }
}

// Systems = behavior
public class MovementSystem {
    public void Update(Registry reg, float dt) {
        foreach (var e in reg.EntitiesWith(typeof(Transform), typeof(Velocity))) {
            var t = reg.Get&lt;Transform&gt;(e);
            var v = reg.Get&lt;Velocity&gt;(e);
            t.Pos += v.Value * dt; // Requires mutable or replace
        }
    }
}

public class RenderSystem {
    public void Update(Registry reg) {
        foreach (var e in reg.EntitiesWith(typeof(Transform), typeof(Sprite))) {
            var t = reg.Get&lt;Transform&gt;(e);
            var s = reg.Get&lt;Sprite&gt;(e);
            Renderer.Draw(s.Tex, t.Pos, t.Rot, s.UV);
        }
    }
}
# Components = pure data (no behaviour)
Transform = Struct.new(:position, :rotation, :scale) do
  def initialize(position = [0, 0, 0], rotation = [0, 0, 0], scale = [1, 1, 1]) = super
end

Velocity = Struct.new(:x, :y, :z) do
  def initialize(x = 0.0, y = 0.0, z = 0.0) = super
end

Health = Struct.new(:current, :maximum) do
  def initialize(current = 100.0, maximum = 100.0) = super
end

Sprite = Struct.new(:texture_id, :uv_rect) do
  def initialize(texture_id, uv_rect = [0, 0, 1, 1]) = super
end

AIController = Struct.new(:behaviour_tree)
PlayerControl = Struct.new(:input_handler)

# Entities = IDs (integers); registry maps entity => { ComponentClass => instance }

# Systems = behaviour (operate on component combinations)
class MovementSystem
  def update(registry, dt)
    registry.each_value do |components|
      next unless components[Transform] && components[Velocity]

      transform = components[Transform]
      velocity = components[Velocity]
      x, y, z = transform.position
      transform.position = [x + velocity.x * dt,
                            y + velocity.y * dt,
                            z + velocity.z * dt]
    end
  end
end

class RenderSystem
  def update(registry)
    registry.each_value do |components|
      next unless components[Transform] && components[Sprite]

      # renderer.draw(components[Sprite].texture_id, components[Transform].position, ...)
    end
  end
end

ECS vs Traditional OOP

The trade-off isn't one-sided — ECS costs you the familiarity and encapsulation of "an enemy object that knows how to update itself" in exchange for the properties below, most of which only start mattering at the scale where inheritance breaks down anyway: thousands of entities, tight cache budgets, or a level editor that needs to serialise arbitrary combinations of behaviour without a fixed class list to draw from.

Aspect OOP (Inheritance) ECS (Composition)
Adding features New subclass, modify hierarchy New component + system
Flying player Impossible (diamond) Add Flight component
Cache performance Poor (scattered vtables) Excellent (SoA layout)
Parallelism Hard (shared mutable state) Easy (disjoint components)
Scripting integration Complex Natural (data-driven)
Serialisation Complex (pointers, vtables) Trivial (POD components)

3. State Pattern for AI/Game States

The State pattern gives the finite-state-machine idea introduced on the Game AI page a concrete object shape: each state is a small class with enter, update, and exit methods, and the entity holding the current state simply calls into whichever one is active. Transitioning states means calling exit on the old one, swapping the reference, and calling enter on the new one — which guarantees setup and teardown logic (starting an animation, resetting a timer) can never accidentally be skipped or run twice, a bug that's easy to introduce with a bare enum-plus-switch version of the same idea.

from abc import ABC, abstractmethod
from typing import Optional

class State(ABC):
    @abstractmethod
    def enter(self, entity: Entity) -> None:
        pass

    @abstractmethod
    def update(self, entity: Entity, dt: float) -> Optional['State']:
        pass

    @abstractmethod
    def exit(self, entity: Entity) -> None:
        pass

class IdleState(State):
    def enter(self, entity: Entity) -> None:
        print("Entering Idle")

    def update(self, entity: Entity, dt: float) -> Optional[State]:
        if entity.can_see_player():
            return ChaseState()
        return None

    def exit(self, entity: Entity) -> None:
        print("Exiting Idle")

class ChaseState(State):
    def enter(self, entity: Entity) -> None:
        entity.play_animation("run")

    def update(self, entity: Entity, dt: float) -> Optional[State]:
        if not entity.can_see_player():
            return SearchState(entity.last_known_player_pos)
        elif entity.in_attack_range():
            return AttackState()
        else:
            entity.move_toward(entity.player_pos)
            return None

    def exit(self, entity: Entity) -> None:
        entity.stop_animation()

# Entity holds current state
class Entity:
    def __init__(self):
        self.state: State = IdleState()

    def change_state(self, new_state: State) -> None:
        self.state.exit(self)
        self.state = new_state
        self.state.enter(self)

    def update(self, dt: float) -> None:
        next_state = self.state.update(self, dt)
        if next_state:
            self.change_state(next_state)
#include &lt;memory&gt;

class Entity;

class State {
public:
    virtual ~State() = default;
    virtual void enter(Entity& entity) = 0;
    virtual std::unique_ptr&lt;State&gt; update(Entity& entity, float dt) = 0;
    virtual void exit(Entity& entity) = 0;
};

class IdleState : public State {
    void enter(Entity& entity) override { /* play idle anim */ }
    std::unique_ptr&lt;State&gt; update(Entity& entity, float dt) override {
        if (entity.canSeePlayer()) return std::make_unique&lt;ChaseState&gt;();
        return nullptr;
    }
    void exit(Entity& entity) override {}
};

class ChaseState : public State {
    void enter(Entity& entity) override { entity.playAnimation("run"); }
    std::unique_ptr&lt;State&gt; update(Entity& entity, float dt) override {
        if (!entity.canSeePlayer()) 
            return std::make_unique&lt;SearchState&gt;(entity.getLastKnownPlayerPos());
        if (entity.inAttackRange()) 
            return std::make_unique&lt;AttackState&gt;();
        entity.moveToward(entity.getPlayerPos());
        return nullptr;
    }
    void exit(Entity& entity) override { entity.stopAnimation(); }
};

class Entity {
    std::unique_ptr&lt;State&gt; state = std::make_unique&lt;IdleState&gt;();

    void changeState(std::unique_ptr&lt;State&gt; newState) {
        state->exit(*this);
        state = std::move(newState);
        state->enter(*this);
    }

    void update(float dt) {
        if (auto next = state->update(*this, dt)) changeState(std::move(next));
    }
};
interface State {
    void enter(Entity entity);
    State update(Entity entity, float dt);
    void exit(Entity entity);
}

class IdleState implements State {
    public void enter(Entity e) { /* play idle anim */ }
    public State update(Entity e, float dt) { 
        return e.canSeePlayer() ? new ChaseState() : null; 
    }
    public void exit(Entity e) {}
}

class ChaseState implements State {
    public void enter(Entity e) { e.playAnimation("run"); }
    public State update(Entity e, float dt) {
        if (!e.canSeePlayer()) return new SearchState(e.getLastKnownPlayerPos());
        if (e.inAttackRange()) return new AttackState();
        e.moveToward(e.getPlayerPos());
        return null;
    }
    public void exit(Entity e) { e.stopAnimation(); }
}

public class Entity {
    private State state = new IdleState();

    public void changeState(State newState) {
        state.exit(this);
        state = newState;
        state.enter(this);
    }

    public void update(float dt) {
        State next = state.update(this, dt);
        if (next != null) changeState(next);
    }
}
public interface IState {
    void Enter(Entity entity);
    IState Update(Entity entity, float dt);
    void Exit(Entity entity);
}

public class IdleState : IState {
    public void Enter(Entity e) { /* play idle anim */ }
    public IState Update(Entity e, float dt) => 
        e.CanSeePlayer() ? new ChaseState() : null;
    public void Exit(Entity e) {}
}

public class ChaseState : IState {
    public void Enter(Entity e) => e.PlayAnimation("run");
    public IState Update(Entity e, float dt) {
        if (!e.CanSeePlayer()) return new SearchState(e.LastKnownPlayerPos);
        if (e.InAttackRange()) return new AttackState();
        e.MoveToward(e.PlayerPos);
        return null;
    }
    public void Exit(Entity e) => e.StopAnimation();
}

public class Entity {
    private IState state = new IdleState();

    public void ChangeState(IState newState) {
        state.Exit(this);
        state = newState;
        state.Enter(this);
    }

    public void Update(float dt) {
        var next = state.Update(this, dt);
        if (next != null) ChangeState(next);
    }
}
class State
  def enter(entity) = raise NotImplementedError
  def update(entity, dt) = raise NotImplementedError
  def exit(entity) = raise NotImplementedError
end

class IdleState < State
  def enter(_entity)
    puts "Entering Idle"
  end

  def update(entity, _dt)
    entity.can_see_player? ? ChaseState.new : nil
  end

  def exit(_entity)
    puts "Exiting Idle"
  end
end

class ChaseState < State
  def enter(entity)
    entity.play_animation("run")
  end

  def update(entity, _dt)
    if !entity.can_see_player?
      SearchState.new(entity.last_known_player_pos)
    elsif entity.in_attack_range?
      AttackState.new
    else
      entity.move_toward(entity.player_pos)
      nil
    end
  end

  def exit(entity)
    entity.stop_animation
  end
end

# Entity holds current state
class Entity
  def initialize
    @state = IdleState.new
  end

  def change_state(new_state)
    @state.exit(self)
    @state = new_state
    @state.enter(self)
  end

  def update(dt)
    next_state = @state.update(self, dt)
    change_state(next_state) if next_state
  end
end

4. Observer Pattern (Event System)

Games are full of moments where one system's outcome needs to trigger reactions in several unrelated others — an enemy dying should update the player's XP, pop a UI notification, maybe trigger an achievement check and a sound cue. Wiring the combat system to call all of those directly would tangle it up with UI and audio code it has no business knowing about. An event bus inverts the dependency: combat just publishes "enemy killed" and moves on, and anything that cares — UI, audio, achievements, quest tracking — subscribes independently, with neither side needing to know the other exists.

from enum import Enum
from typing import Callable, Dict, List
from collections import defaultdict

class GameEvent(Enum):
    PLAYER_DIED = "player_died"
    ENEMY_KILLED = "enemy_killed"
    ITEM_COLLECTED = "item_collected"
    LEVEL_COMPLETED = "level_completed"

class EventBus:
    def __init__(self):
        self._listeners: Dict[GameEvent, List[Callable]] = defaultdict(list)

    def subscribe(self, event: GameEvent, callback: Callable) -> None:
        self._listeners[event].append(callback)

    def publish(self, event: GameEvent, data: dict = None) -> None:
        for callback in self._listeners[event]:
            callback(data or {})

# Usage
bus = EventBus()

bus.subscribe(GameEvent.ENEMY_KILLED, lambda data: {
    player.add_xp(data.get("xp", 0)),
    ui.show_xp_popup(data.get("xp", 0))
})

# Any system can publish
bus.publish(GameEvent.ENEMY_KILLED, {"xp": 100, "enemy_type": "goblin"})
#include &lt;functional&gt;
#include &lt;unordered_map&gt;
#include &lt;vector&gt;
#include &lt;any&gt;

enum class GameEvent { PlayerDied, EnemyKilled, ItemCollected, LevelCompleted };

class EventBus {
    std::unordered_map&lt;GameEvent, std::vector&lt;std::function&lt;void(const std::any&)&gt;&gt;&gt; listeners;
public:
    template&lt;typename F&gt;
    void subscribe(GameEvent event, F&& callback) {
        listeners[event].push_back(std::forward&lt;F&gt;(callback));
    }

    void publish(GameEvent event, const std::any& data = {}) {
        for (auto& cb : listeners[event]) cb(data);
    }
};

// Usage
EventBus bus;
bus.subscribe(GameEvent::EnemyKilled, [](const std::any& data) {
    auto d = std::any_cast&lt;std::unordered_map&lt;std::string, int&gt;&gt;(data);
    player.addXP(d.at("xp"));
    ui.showXPPopup(d.at("xp"));
});

// Any system can publish
bus.publish(GameEvent::EnemyKilled, 
    std::unordered_map&lt;std::string, int&gt;{{"xp", 100}, {"enemy_type", "goblin"}});
import java.util.*;
import java.util.function.*;

enum GameEvent { PlayerDied, EnemyKilled, ItemCollected, LevelCompleted }

class EventBus {
    private final Map&lt;GameEvent, List&lt;Consumer&lt;Map&lt;String, Object&gt;&gt;&gt;&gt; listeners = new EnumMap&lt;&gt;(GameEvent.class);

    public void subscribe(GameEvent event, Consumer&lt;Map&lt;String, Object&gt;&gt; callback) {
        listeners.computeIfAbsent(event, k -> new ArrayList&lt;&gt;()).add(callback);
    }

    public void publish(GameEvent event, Map&lt;String, Object&gt; data) {
        for (var cb : listeners.getOrDefault(event, List.of())) cb.accept(data);
    }
}

// Usage
EventBus bus = new EventBus();
bus.subscribe(GameEvent.EnemyKilled, data -> {
    player.addXP((int) data.get("xp"));
    ui.showXPPopup((int) data.get("xp"));
});

bus.publish(GameEvent.EnemyKilled, Map.of("xp", 100, "enemy_type", "goblin"));
using System;
using System.Collections.Generic;

public enum GameEvent { PlayerDied, EnemyKilled, ItemCollected, LevelCompleted }

public class EventBus {
    private readonly Dictionary&lt;GameEvent, List&lt;Action&lt;Dictionary&lt;string, object&gt;&gt;&gt;&gt; _listeners = new();

    public void Subscribe(GameEvent evt, Action&lt;Dictionary&lt;string, object&gt;&gt; cb) {
        if (!_listeners.TryGetValue(evt, out var list)) _listeners[evt] = list = new();
        list.Add(cb);
    }

    public void Publish(GameEvent evt, Dictionary&lt;string, object&gt; data = null) {
        if (_listeners.TryGetValue(evt, out var list))
            foreach (var cb in list) cb(data ?? new());
    }
}

// Usage
var bus = new EventBus();
bus.Subscribe(GameEvent.EnemyKilled, data => {
    player.AddXP((int)data["xp"]);
    ui.ShowXPPopup((int)data["xp"]);
});

bus.Publish(GameEvent.EnemyKilled, new Dictionary&lt;string, object&gt; { ["xp"] = 100, ["enemy_type"] = "goblin" });
module GameEvent
  PLAYER_DIED = :player_died
  ENEMY_KILLED = :enemy_killed
  ITEM_COLLECTED = :item_collected
  LEVEL_COMPLETED = :level_completed
end

class EventBus
  def initialize
    @listeners = Hash.new { |hash, key| hash[key] = [] }
  end

  def subscribe(event, &callback)
    @listeners[event] << callback
  end

  def publish(event, data = {})
    @listeners[event].each { |callback| callback.call(data) }
  end
end

# Usage
bus = EventBus.new

bus.subscribe(GameEvent::ENEMY_KILLED) do |data|
  player.add_xp(data.fetch(:xp, 0))
  ui.show_xp_popup(data.fetch(:xp, 0))
end

# Any system can publish
bus.publish(GameEvent::ENEMY_KILLED, xp: 100, enemy_type: "goblin")

5. Object Pool Pattern

Bullets, particles, and hit-effects share a shape: they're created and destroyed constantly, often dozens of times a frame, and allocating fresh memory for each one is exactly the kind of per-frame cost the frame budget can't absorb — it also fragments the heap and triggers garbage collection pauses in managed languages. The Object Pool pattern sidesteps both problems by never actually destroying anything: a fixed set of objects is created once up front, and "creating" a new bullet just means pulling an already-allocated one out of the pool and resetting its state, with "destroying" it meaning putting it back for reuse.

from typing import TypeVar, Generic, List, Callable
T = TypeVar('T')

class ObjectPool(Generic[T]):
    def __init__(self, factory: Callable[[], T], initial_size: int = 100):
        self._factory = factory
        self._pool: List[T] = [factory() for _ in range(initial_size)]
        self._active: List[T] = []

    def acquire(self) -> T:
        if self._pool:
            obj = self._pool.pop()
        else:
            obj = self._factory()
        self._active.append(obj)
        return obj

    def release(self, obj: T) -> None:
        if obj in self._active:
            self._active.remove(obj)
            self._pool.append(obj)

    def update_all(self, dt: float) -> None:
        for obj in self._active[:]:  # Copy to allow removal during iteration
            obj.update(dt)
            if obj.should_destroy:
                self.release(obj)

# Particle example
class Particle:
    def __init__(self):
        self.position = (0, 0, 0)
        self.velocity = (0, 0, 0)
        self.lifetime = 1.0
        self.should_destroy = False

    def update(self, dt: float):
        x, y, z = self.position
        vx, vy, vz = self.velocity
        self.position = (x + vx*dt, y + vy*dt, z + vz*dt)
        self.lifetime -= dt
        if self.lifetime <= 0:
            self.should_destroy = True

pool = ObjectPool(Particle, initial_size=10000)

def spawn_explosion(pos: tuple):
    import random
    for _ in range(50):
        p = pool.acquire()
        p.position = (pos[0] + random.uniform(-0.5, 0.5),
                      pos[1] + random.uniform(-0.5, 0.5),
                      pos[2] + random.uniform(-0.5, 0.5))
        p.velocity = (random.uniform(-5, 5), random.uniform(-5, 5), random.uniform(-5, 5))
        p.lifetime = 1.0
        p.should_destroy = False
#include &lt;vector&gt;
#include &lt;memory&gt;
#include &lt;functional&gt;

template&lt;typename T&gt;
class ObjectPool {
    std::vector&lt;T&gt; pool;
    std::vector&lt;T*&gt; active;
    std::function&lt;T()&gt; factory;

public:
    ObjectPool(std::function&lt;T()&gt; f, size_t initial = 100) : factory(f) {
        pool.reserve(initial);
        for (size_t i = 0; i < initial; ++i) pool.emplace_back(f());
    }

    T* acquire() {
        T* obj;
        if (!pool.empty()) {
            obj = &amp;pool.back();
            pool.pop_back();
        } else {
            pool.emplace_back(factory());
            obj = &amp;pool.back();
        }
        active.push_back(obj);
        return obj;
    }

    void release(T* obj) {
        obj->~T();
        // Move-released object back to pool
        std::swap(*obj, pool.emplace_back());
        pool.pop_back();
        // Remove from active
        auto it = std::find(active.begin(), active.end(), obj);
        if (it != active.end()) active.erase(it);
    }

    template&lt;typename F&gt;
    void updateAll(float dt, F&& updateFn) {
        for (size_t i = 0; i < active.size(); ) {
            updateFn(*active[i], dt);
            if (active[i]->shouldDestroy) release(active[i]);
            else ++i;
        }
    }
};

// Particle system — 10,000 particles
struct Particle {
    Vec3 position, velocity;
    float lifetime = 1.0f;
    bool shouldDestroy = false;
};

ObjectPool&lt;Particle&gt; particlePool(10000, []{ return Particle{}; });

void spawnExplosion(Vec3 pos) {
    for (int i = 0; i < 50; ++i) {
        Particle* p = particlePool.acquire();
        p->position = pos + randomVec3(0.5f);
        p->velocity = randomVec3(5.0f);
        p->lifetime = 1.0f;
        p->shouldDestroy = false;
    }
}
import java.util.*;
import java.util.function.*;

public class ObjectPool&lt;T&gt; {
    private final Supplier&lt;T&gt; factory;
    private final List&lt;T&gt; pool = new ArrayList&lt;&gt;();
    private final List&lt;T&gt; active = new ArrayList&lt;&gt;();

    public ObjectPool(Supplier&lt;T&gt; factory, int initial) {
        this.factory = factory;
        for (int i = 0; i < initial; i++) pool.add(factory.get());
    }

    public T acquire() {
        T obj = pool.isEmpty() ? factory.get() : pool.remove(pool.size() - 1);
        active.add(obj);
        return obj;
    }

    public void release(T obj) {
        active.remove(obj);
        pool.add(obj);
    }

    public void updateAll(float dt, Consumer&lt;T&gt; updateFn) {
        for (int i = 0; i < active.size(); ) {
            T obj = active.get(i);
            updateFn.accept(obj);
            if (obj.shouldDestroy) release(obj);
            else i++;
        }
    }
}

// Particle system — 10,000 particles
public class Particle {
    public Vec3 position, velocity;
    public float lifetime = 1.0f;
    public boolean shouldDestroy = false;
}

ObjectPool&lt;Particle&gt; particlePool = new ObjectPool&lt;&gt;(Particle::new, 10000);

void spawnExplosion(Vec3 pos) {
    Random rng = new Random();
    for (int i = 0; i < 50; i++) {
        Particle p = particlePool.acquire();
        p.position = pos.add(randomVec3(0.5f));
        p.velocity = randomVec3(5.0f);
        p.lifetime = 1.0f;
        p.shouldDestroy = false;
    }
}
using System;
using System.Collections.Generic;

public class ObjectPool&lt;T&gt; where T : class, new() {
    private readonly Func&lt;T&gt; _factory;
    private readonly List&lt;T&gt; _pool = new();
    private readonly List&lt;T&gt; _active = new();

    public ObjectPool(Func&lt;T&gt; factory, int initial = 100) {
        _factory = factory;
        for (int i = 0; i < initial; i++) _pool.Add(factory());
    }

    public T Acquire() {
        var obj = _pool.Count > 0 ? _pool[_pool.Count - 1] : _factory();
        if (_pool.Count > 0) _pool.RemoveAt(_pool.Count - 1);
        _active.Add(obj);
        return obj;
    }

    public void Release(T obj) {
        _active.Remove(obj);
        _pool.Add(obj);
    }

    public void UpdateAll(float dt, Action&lt;T&gt; updateFn) {
        for (int i = 0; i < _active.Count; ) {
            var obj = _active[i];
            updateFn(obj);
            if (obj.ShouldDestroy) Release(obj);
            else i++;
        }
    }
}

// Particle system — 10,000 particles
public class Particle {
    public Vec3 Position, Velocity;
    public float Lifetime = 1.0f;
    public bool ShouldDestroy = false;
}

var particlePool = new ObjectPool&lt;Particle&gt;(() => new Particle(), 10000);

void SpawnExplosion(Vec3 pos) {
    var rng = new Random();
    for (int i = 0; i < 50; i++) {
        var p = particlePool.Acquire();
        p.Position = pos + RandomVec3(0.5f);
        p.Velocity = RandomVec3(5.0f);
        p.Lifetime = 1.0f;
        p.ShouldDestroy = false;
    }
}
class ObjectPool
  def initialize(initial_size: 100, &factory)
    @factory = factory
    @pool = Array.new(initial_size) { factory.call }
    @active = []
  end

  def acquire
    obj = @pool.pop || @factory.call
    @active << obj
    obj
  end

  def release(obj)
    return unless @active.delete(obj)

    @pool << obj
  end

  def update_all(dt)
    @active.dup.each do |obj| # dup so we can release during iteration
      obj.update(dt)
      release(obj) if obj.should_destroy
    end
  end
end

# Particle example
class Particle
  attr_accessor :position, :velocity, :lifetime, :should_destroy

  def initialize
    @position = [0, 0, 0]
    @velocity = [0, 0, 0]
    @lifetime = 1.0
    @should_destroy = false
  end

  def update(dt)
    @position = @position.zip(@velocity).map { |p, v| p + v * dt }
    @lifetime -= dt
    @should_destroy = true if @lifetime <= 0
  end
end

pool = ObjectPool.new(initial_size: 10_000) { Particle.new }

def spawn_explosion(pool, pos)
  50.times do
    p = pool.acquire
    p.position = pos.map { |c| c + rand(-0.5..0.5) }
    p.velocity = Array.new(3) { rand(-5.0..5.0) }
    p.lifetime = 1.0
    p.should_destroy = false
  end
end

6. Factory Pattern for Level Loading

Spawning a game entity usually means attaching a whole cluster of components in the right combination — a player needs a transform, health, sprite and input control; an enemy needs the same transform and health but an AI controller instead. Scattering that construction logic across every call site that spawns something invites the two copies to drift apart. A factory centralises it: one place knows what "an enemy" or "a player" is actually made of, and — combined with a data-driven template, as in the JSON-loading example below — level designers can define entirely new entity types without a programmer writing new construction code for each one.

from typing import Dict, Any
import json

class EntityFactory:
    def __init__(self, registry, resources):
        self.registry = registry
        self.resources = resources

    def create_enemy(self, enemy_type: str, pos: tuple) -> int:
        e = self.registry.create()
        self.registry.emplace(e, Transform(pos))
        self.registry.emplace(e, Health(100, 100))
        self.registry.emplace(e, Sprite(self.resources.get_texture(f"enemy_{enemy_type}")))
        self.registry.emplace(e, AIController(load_behavior_tree(enemy_type)))
        return e

    def create_player(self, pos: tuple) -> int:
        e = self.registry.create()
        self.registry.emplace(e, Transform(pos))
        self.registry.emplace(e, Health(100, 100))
        self.registry.emplace(e, Sprite(self.resources.get_texture("player")))
        self.registry.emplace(e, PlayerControl(input_handler))
        return e

# Data-driven: load from JSON
def create_from_template(factory: EntityFactory, template: dict) -> int:
    e = factory.registry.create()
    for component_name, data in template.get("components", {}).items():
        if component_name == "Transform":
            factory.registry.emplace(e, Transform(data["pos"]))
        elif component_name == "Health":
            factory.registry.emplace(e, Health(data["current"], data["max"]))
        # ... other components
    return e

# Level loading
def load_level(factory: EntityFactory, level_path: str) -> None:
    with open(level_path) as f:
        level_data = json.load(f)

    for entity_template in level_data.get("entities", []):
        create_from_template(factory, entity_template)
#include &lt;nlohmann/json.hpp&gt;
#include &lt;string&gt;
#include &lt;unordered_map&gt;

using json = nlohmann::json;

class EntityFactory {
    Registry& reg;
    ResourceManager& resources;

public:
    EntityFactory(Registry& r, ResourceManager& res) : reg(r), resources(res) {}

    Entity createEnemy(const std::string& type, Vec3 pos) {
        Entity e = reg.create();
        reg.emplace&lt;Transform&gt;(e, pos);
        reg.emplace&lt;Health&gt;(e, 100, 100);
        reg.emplace&lt;Sprite&gt;(e, resources.getTexture("enemy_" + type));
        reg.emplace&lt;AIController&gt;(e, loadBehaviorTree(type));
        return e;
    }

    Entity createPlayer(Vec3 pos) {
        Entity e = reg.create();
        reg.emplace&lt;Transform&gt;(e, pos);
        reg.emplace&lt;Health&gt;(e, 100, 100);
        reg.emplace&lt;Sprite&gt;(e, resources.getTexture("player"));
        reg.emplace&lt;PlayerControl&gt;(e, &amp;inputHandler);
        return e;
    }
};

// Data-driven: load from JSON
Entity createFromTemplate(EntityFactory& factory, const json& tmpl) {
    Entity e = factory.reg.create();
    for (auto& [compName, data] : tmpl["components"].items()) {
        if (compName == "Transform") {
            factory.reg.emplace&lt;Transform&gt;(e, data["pos"]);
        } else if (compName == "Health") {
            factory.reg.emplace&lt;Health&gt;(e, data["current"], data["max"]);
        }
        // ... other components
    }
    return e;
}

// Level loading
void loadLevel(EntityFactory& factory, const std::string& path) {
    std::ifstream f(path);
    json levelData = json::parse(f);

    for (auto& entityTmpl : levelData["entities"]) {
        createFromTemplate(factory, entityTmpl);
    }
}
import com.fasterxml.jackson.databind.JsonNode;
import java.io.*;
import java.util.*;

public class EntityFactory {
    private final Registry reg;
    private final ResourceManager resources;

    public EntityFactory(Registry reg, ResourceManager res) { this.reg = reg; this.resources = res; }

    public Entity createEnemy(String type, Vec3 pos) {
        Entity e = reg.create();
        reg.emplace(e, new Transform(pos));
        reg.emplace(e, new Health(100, 100));
        reg.emplace(e, new Sprite(resources.getTexture("enemy_" + type)));
        reg.emplace(e, new AIController(loadBehaviorTree(type)));
        return e;
    }

    public Entity createPlayer(Vec3 pos) {
        Entity e = reg.create();
        reg.emplace(e, new Transform(pos));
        reg.emplace(e, new Health(100, 100));
        reg.emplace(e, new Sprite(resources.getTexture("player")));
        reg.emplace(e, new PlayerControl(inputHandler));
        return e;
    }
}

// Data-driven: load from JSON
Entity createFromTemplate(EntityFactory factory, JsonNode tmpl) {
    Entity e = factory.reg.create();
    var components = tmpl.get("components");
    if (components != null) {
        components.fields().forEachRemaining(entry -> {
            String compName = entry.getKey();
            JsonNode data = entry.getValue();
            if (compName.equals("Transform")) {
                factory.reg.emplace(e, new Transform(data.get("pos")));
            } else if (compName.equals("Health")) {
                factory.reg.emplace(e, new Health(data.get("current").asInt(), data.get("max").asInt()));
            }
        });
    }
    return e;
}

void loadLevel(EntityFactory factory, String path) throws IOException {
    var mapper = new ObjectMapper();
    JsonNode levelData = mapper.readTree(new File(path));
    for (JsonNode entityTmpl : levelData.get("entities")) {
        createFromTemplate(factory, entityTmpl);
    }
}
using System.Text.Json;
using System.IO;

public class EntityFactory {
    private readonly Registry _reg;
    private readonly ResourceManager _resources;

    public EntityFactory(Registry reg, ResourceManager res) {
        _reg = reg; _resources = res;
    }

    public Entity CreateEnemy(string type, Vec3 pos) {
        var e = _reg.Create();
        _reg.Emplace(e, new Transform(pos));
        _reg.Emplace(e, new Health(100, 100));
        _reg.Emplace(e, new Sprite(_resources.GetTexture($"enemy_{type}")));
        _reg.Emplace(e, new AIController(LoadBehaviorTree(type)));
        return e;
    }

    public Entity CreatePlayer(Vec3 pos) {
        var e = _reg.Create();
        _reg.Emplace(e, new Transform(pos));
        _reg.Emplace(e, new Health(100, 100));
        _reg.Emplace(e, new Sprite(_resources.GetTexture("player")));
        _reg.Emplace(e, new PlayerControl(_inputHandler));
        return e;
    }
}

Entity CreateFromTemplate(EntityFactory factory, JsonElement tmpl) {
    var e = factory._reg.Create();
    foreach (var prop in tmpl.GetProperty("components").EnumerateObject()) {
        if (prop.Name == "Transform") {
            factory._reg.Emplace(e, new Transform(prop.Value.GetProperty("pos")));
        } else if (prop.Name == "Health") {
            factory._reg.Emplace(e, new Health(prop.Value.GetProperty("current").GetInt32(), prop.Value.GetProperty("max").GetInt32()));
        }
    }
    return e;
}

void LoadLevel(EntityFactory factory, string path) {
    var json = JsonDocument.Parse(File.ReadAllText(path));
    foreach (var entityTmpl in json.RootElement.GetProperty("entities").EnumerateArray()) {
        CreateFromTemplate(factory, entityTmpl);
    }
}
require "json"

class EntityFactory
  attr_reader :registry

  def initialize(registry, resources)
    @registry = registry
    @resources = resources
  end

  def create_enemy(enemy_type, pos)
    e = registry.create
    registry.emplace(e, Transform.new(pos))
    registry.emplace(e, Health.new(100, 100))
    registry.emplace(e, Sprite.new(@resources.texture("enemy_#{enemy_type}")))
    registry.emplace(e, AIController.new(load_behaviour_tree(enemy_type)))
    e
  end

  def create_player(pos, input_handler)
    e = registry.create
    registry.emplace(e, Transform.new(pos))
    registry.emplace(e, Health.new(100, 100))
    registry.emplace(e, Sprite.new(@resources.texture("player")))
    registry.emplace(e, PlayerControl.new(input_handler))
    e
  end
end

# Data-driven: load from JSON
def create_from_template(factory, template)
  e = factory.registry.create
  template.fetch("components", {}).each do |component_name, data|
    case component_name
    when "Transform"
      factory.registry.emplace(e, Transform.new(data["pos"]))
    when "Health"
      factory.registry.emplace(e, Health.new(data["current"], data["max"]))
      # ... other components
    end
  end
  e
end

# Level loading
def load_level(factory, level_path)
  level_data = JSON.parse(File.read(level_path))
  level_data.fetch("entities", []).each do |entity_template|
    create_from_template(factory, entity_template)
  end
end

Pattern Selection Guide

None of the six patterns above is a default to reach for on every project — each solves a specific, recognisable problem, and using one where it doesn't fit adds indirection for no benefit. The table below is a quick lookup from symptom to pattern; the Service Locator entry names one more pattern not covered in detail on this page, for providing cross-cutting services (logging, audio, save data) to code that shouldn't need a hard dependency on how those services are wired up.

Problem Recommended Pattern
Input handling, replay, AI control Command
Complex entity composition ECS
AI/behaviour states State
Decoupled game events Observer/Event Bus
Frequent allocation (particles, bullets) Object Pool
Data-driven entity creation Factory
Cross-cutting global services Service Locator

References

  1. Nystrom, R. (2014). Game Programming Patterns. Genever Benning. Free online at https://gameprogrammingpatterns.com/
  2. Mertens, S. Entity Component System FAQ. https://github.com/SanderMertens/ecs-faq

Further ECS implementations worth studying directly: EnTT (fast, header-only C++ ECS), flecs (C/C++ ECS with queries, modules and stats), and bevy_ecs (the Rust ECS that has shaped several recent C++ designs).