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 <unordered_map>
#include <vector>
#include <memory>
class InputHandler {
std::unordered_map<int, std::unique_ptr<Command>> keyBindings;
std::vector<std::unique_ptr<Command>> commandHistory; // For undo/replay
size_t historyIndex = 0;
public:
void bind(int key, std::unique_ptr<Command> 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<MoveForwardCommand>());
input.bind(moveBackward, std::make_unique<MoveBackwardCommand>());
input.bind(jump, std::make_unique<JumpCommand>());
input.bind(interact, std::make_unique<InteractCommand>());
input.bind(dodge, std::make_unique<DodgeCommand>());
}
};
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 <unordered_map>
#include <vector>
#include <typeindex>
#include <memory>
// 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<Entity, std::unordered_map<std::type_index, std::unique_ptr<void>>> components;
public:
template<typename T, typename... Args>
T& emplace(Entity e, Args&&... args) {
auto& map = components[e];
auto ptr = std::make_unique<T>(std::forward<Args>(args)...);
T* ref = ptr.get();
map[std::type_index(typeid(T))] = std::move(ptr);
return *ref;
}
template<typename T>
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<T*>(cit->second.get()) : nullptr;
}
template<typename... Components>
class View {
Registry& reg;
public:
View(Registry& r) : reg(r) {}
template<typename Func>
void each(Func&& f) {
// Iterate entities with all Components...
}
};
template<typename... Components>
View<Components...> view() { return View<Components...>(*this); }
};
// Systems = behavior
class MovementSystem {
void update(Registry& reg, float dt) {
reg.view<Transform, Velocity>().each([&](Entity e, Transform& t, Velocity& v) {
t.pos += v.value * dt;
});
}
};
class RenderSystem {
void update(Registry& reg) {
reg.view<Transform, Sprite>().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<Transform, AIController>().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<Entity, Map<Class<?>, Object>> components = new HashMap<>();
public <T> T emplace(Entity e, T component) {
components.computeIfAbsent(e, k -> new HashMap<>())
.put(component.getClass(), component);
return component;
}
public <T> T get(Entity e, Class<T> type) {
var map = components.get(e);
return map != null ? type.cast(map.get(type)) : null;
}
public <T> Iterable<Entity> entitiesWith(Class<T>... 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<Entity, Dictionary<Type, object>> components = new();
public T Emplace<T>(Entity e, T component) where T : notnull {
if (!components.TryGetValue(e, out var map)) {
map = new Dictionary<Type, object>();
components[e] = map;
}
map[typeof(T)] = component;
return component;
}
public T Get<T>(Entity e) {
return components.TryGetValue(e, out var map) && map.TryGetValue(typeof(T), out var obj)
? (T)obj : default;
}
public IEnumerable<Entity> 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<Transform>(e);
var v = reg.Get<Velocity>(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<Transform>(e);
var s = reg.Get<Sprite>(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 <memory>
class Entity;
class State {
public:
virtual ~State() = default;
virtual void enter(Entity& entity) = 0;
virtual std::unique_ptr<State> 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<State> update(Entity& entity, float dt) override {
if (entity.canSeePlayer()) return std::make_unique<ChaseState>();
return nullptr;
}
void exit(Entity& entity) override {}
};
class ChaseState : public State {
void enter(Entity& entity) override { entity.playAnimation("run"); }
std::unique_ptr<State> update(Entity& entity, float dt) override {
if (!entity.canSeePlayer())
return std::make_unique<SearchState>(entity.getLastKnownPlayerPos());
if (entity.inAttackRange())
return std::make_unique<AttackState>();
entity.moveToward(entity.getPlayerPos());
return nullptr;
}
void exit(Entity& entity) override { entity.stopAnimation(); }
};
class Entity {
std::unique_ptr<State> state = std::make_unique<IdleState>();
void changeState(std::unique_ptr<State> 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 <functional>
#include <unordered_map>
#include <vector>
#include <any>
enum class GameEvent { PlayerDied, EnemyKilled, ItemCollected, LevelCompleted };
class EventBus {
std::unordered_map<GameEvent, std::vector<std::function<void(const std::any&)>>> listeners;
public:
template<typename F>
void subscribe(GameEvent event, F&& callback) {
listeners[event].push_back(std::forward<F>(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<std::unordered_map<std::string, int>>(data);
player.addXP(d.at("xp"));
ui.showXPPopup(d.at("xp"));
});
// Any system can publish
bus.publish(GameEvent::EnemyKilled,
std::unordered_map<std::string, int>{{"xp", 100}, {"enemy_type", "goblin"}});
import java.util.*;
import java.util.function.*;
enum GameEvent { PlayerDied, EnemyKilled, ItemCollected, LevelCompleted }
class EventBus {
private final Map<GameEvent, List<Consumer<Map<String, Object>>>> listeners = new EnumMap<>(GameEvent.class);
public void subscribe(GameEvent event, Consumer<Map<String, Object>> callback) {
listeners.computeIfAbsent(event, k -> new ArrayList<>()).add(callback);
}
public void publish(GameEvent event, Map<String, Object> 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<GameEvent, List<Action<Dictionary<string, object>>>> _listeners = new();
public void Subscribe(GameEvent evt, Action<Dictionary<string, object>> cb) {
if (!_listeners.TryGetValue(evt, out var list)) _listeners[evt] = list = new();
list.Add(cb);
}
public void Publish(GameEvent evt, Dictionary<string, object> 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<string, object> { ["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 <vector>
#include <memory>
#include <functional>
template<typename T>
class ObjectPool {
std::vector<T> pool;
std::vector<T*> active;
std::function<T()> factory;
public:
ObjectPool(std::function<T()> 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 = &pool.back();
pool.pop_back();
} else {
pool.emplace_back(factory());
obj = &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<typename F>
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<Particle> 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<T> {
private final Supplier<T> factory;
private final List<T> pool = new ArrayList<>();
private final List<T> active = new ArrayList<>();
public ObjectPool(Supplier<T> 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<T> 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<Particle> particlePool = new ObjectPool<>(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<T> where T : class, new() {
private readonly Func<T> _factory;
private readonly List<T> _pool = new();
private readonly List<T> _active = new();
public ObjectPool(Func<T> 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<T> 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<Particle>(() => 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 <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
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<Transform>(e, pos);
reg.emplace<Health>(e, 100, 100);
reg.emplace<Sprite>(e, resources.getTexture("enemy_" + type));
reg.emplace<AIController>(e, loadBehaviorTree(type));
return e;
}
Entity createPlayer(Vec3 pos) {
Entity e = reg.create();
reg.emplace<Transform>(e, pos);
reg.emplace<Health>(e, 100, 100);
reg.emplace<Sprite>(e, resources.getTexture("player"));
reg.emplace<PlayerControl>(e, &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<Transform>(e, data["pos"]);
} else if (compName == "Health") {
factory.reg.emplace<Health>(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
- Nystrom, R. (2014). Game Programming Patterns. Genever Benning. Free online at https://gameprogrammingpatterns.com/
- 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).