"Patterns are not invented; they are discovered. They are the vocabulary of experienced developers." — Robert Nystrom, Game Programming Patterns
Games are unique software systems: real-time, state-heavy, performance-critical, and content-driven. Patterns help manage this complexity.
1. Command Pattern
The Problem
# ❌ 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
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")
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)
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
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)
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())
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
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)
The Problem with Deep Inheritance
# ❌ 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
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, ...)
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
#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)
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
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
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)
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