Game Programming Patterns

nineteen patterns from Robert Nystrom's book, visualised

$ man patterns | less

The code between

not graphics, not physics, not AI — how all of it is organised

Good design means that when I make a change, it’s as if the entire program was crafted in anticipation of it.
Robert Nystrom, Architecture, Performance, and Games
1. LEARN THE CODE 2. SOLVE 3. CLEAN UP the slow part: paging code into your head decoupling means less of it to load so the next person can’t tell when any line was written

At what cost?

EASY TO CHANGE FAST TO RUN DONE TODAY pick your flavour of wrong
  • Abstraction is a bet on needing that flexibility later. Lose the bet and it’s just more code.
  • Performance is all about assumptions. Flexibility is about removing them.
  • Prototype code is fine — as long as you really get to throw it away.
  • Simplicity eases all three: less code to load, less code to run.

“It’s easier to make a fun game fast than it is to make a fast game fun.”

keep it flexible while the design settles — then tear out abstraction where it costs you

Nineteen patterns

every tile is a link — the last slide links back here

I

Design Patterns Revisited

six Gang of Four patterns, seen from a game

Command

THE GIST“A command is a reified method call” — a method call wrapped in an object.

PROBLEM

Input code calls jump() and fireGun() directly. No rebinding buttons, no AI driving the same actor, no taking a move back.

PATTERN

Turn the call into an object you can keep in a variable, hand to any actor, queue, log — and undo.

  • Rebindable input: each button holds a command.
  • Player and AI emit the same commands. Demo mode for free.
  • Undo / redo: undo() plus a history list.
class Command {
public:
  virtual ~Command() {}
  virtual void execute() = 0;
  virtual void undo() = 0;
};

class MoveUnitCommand : public Command {
public:
  MoveUnitCommand(Unit* unit, int x, int y)
  : unit_(unit), x_(x), y_(y) {}

  virtual void execute() {
    // Remember where it was, so undo can go back.
    xBefore_ = unit_->x();
    yBefore_ = unit_->y();
    unit_->moveTo(x_, y_);
  }

  virtual void undo() {
    unit_->moveTo(xBefore_, yBefore_);
  }

private:
  Unit* unit_;
  int x_, y_, xBefore_, yBefore_;
};

seen in every editor’s Ctrl-Zkey rebinding menusreplaysa JS closure is a command

LIVE DEMO

Commands, kept

click it, then arrows to move · Z undo · Y redo · or a controller: stick, LB / RB · same history whoever is driving

Flyweight

THE GISTSplit an object in two: what every instance shares (intrinsic) and what makes this one different (extrinsic). Keep one copy of the first.

PROBLEM

A forest of thousands of trees. Each has a mesh and bark and leaf textures — and they’re all the same ones.

PATTERN

Every Tree points at one shared TreeModel and keeps only its own position, height and tint.

  • On the GPU: instanced rendering. The model once, the instances as a list, one draw call.
  • Terrain tiles: a grid of pointers to three Terrain objects instead of an enum and a pile of switches.
class TreeModel {
private:
  Mesh mesh_;
  Texture bark_;
  Texture leaves_;
};

class Tree {
private:
  TreeModel* model_;  // shared by every tree

  Vector position_;   // the rest is this tree's own
  double height_;
  double thickness_;
  Color barkTint_;
  Color leafTint_;
};

// The world is a grid of pointers to flyweights.
Terrain* tiles_[WIDTH][HEIGHT];

int cost = world.getTile(2, 3).getMovementCost();

seen in GPU instancingGodot: a Resource is shared by every node that uses itUnity: sharedMaterial

LIVE DEMO

A thousand instances

hover a tree — what’s its own, and what does it share?

Observer

THE GIST“Lets one piece of code announce that something interesting happened without actually caring who receives the notification.”

PROBLEM

An achievement for falling off a bridge. Do we really want unlockFallOffBridge() in the middle of the collision code?

PATTERN

The subject keeps a list of observers. Anyone can add or remove themselves; notify() walks the list.

KEEP IN MIND

  • It’s synchronous. A slow observer blocks the subject.
  • Lapsed listeners: forget to unregister and zombies keep getting called.
  • Who’s listening is only known at runtime. Use it between lumps of code, not inside one.
void Physics::updateEntity(Entity& entity) {
  bool wasOnSurface = entity.isOnSurface();
  entity.accelerate(GRAVITY);
  entity.update();
  if (wasOnSurface && !entity.isOnSurface()) {
    notify(entity, EVENT_START_FALL);
  }
}

class Subject {
public:
  void addObserver(Observer* observer);
  void removeObserver(Observer* observer);

protected:
  void notify(const Entity& entity, Event event) {
    for (int i = 0; i < numObservers_; i++) {
      observers_[i]->onNotify(entity, event);
    }
  }

private:
  Observer* observers_[MAX_OBSERVERS];
  int numObservers_;
};

seen in Godot signalsC# eventsaddEventListenerthe Observer slide in the core loop

LIVE DEMO

The lapsed listener

open and close the stats screen a few times, then fall

Prototype

THE GISTAn object can spawn other objects like itself: clone() copies the class and the state.

PROBLEM

Ghost, Demon, Sorcerer… and then GhostSpawner, DemonSpawner, SorcererSpawner. A second, parallel class hierarchy.

PATTERN

One Spawner holds a prototype monster and stamps out clones. A fast-ghost spawner is just a fast ghost.

VERDICT

Nystrom never found the class pattern the best answer: spawn functions, templates or first-class types do it with less code. Where prototypes do shine is in data →

class Monster {
public:
  virtual Monster* clone() = 0;
};

class Ghost : public Monster {
public:
  Ghost(int health, int speed)
  : health_(health), speed_(speed) {}
  virtual Monster* clone() {
    return new Ghost(health_, speed_);
  }

private:
  int health_;
  int speed_;
};

class Spawner {
public:
  Spawner(Monster* prototype) : prototype_(prototype) {}
  Monster* spawnMonster() { return prototype_->clone(); }
private:
  Monster* prototype_;
};
Spawner* ghostSpawner = new Spawner(new Ghost(15, 3));

seen in JS prototype chainsUnity: prefab variantsGodot: inherited scenes

LIVE DEMO

Prototypes for data

dim rows are not stored on that object — they’re looked up on its prototype

Singleton

THE GIST“Ensure a class has one instance, and provide a global point of access to it.” Two problems, one pattern.

THE ANOMALY

Every other chapter shows how to use a pattern. This one shows how not to.

WHY WE REGRET IT

  • It’s a global variable in a nice class: harder to reason about, invites coupling, unfriendly to threads.
  • It solves two problems when you have one. You wanted easy access to the log; now there can only ever be one log, and every call site says so.
  • Lazy initialisation takes control away: the audio system loads on the first sound, mid-fight.
class FileSystem {
public:
  static FileSystem& instance() {
    // Lazy initialize.
    if (instance_ == NULL) {
      instance_ = new FileSystem();
    }
    return *instance_;
  }

private:
  FileSystem() {}
  static FileSystem* instance_;
};

// Fine, until the log file becomes a dumping
// ground and you want one log per domain:
Log::instance().write("Some event.");
// ...in every file that ever logged anything.

seen in Godot autoloadsGameManager.Instancethe core loop deck’s pattern card

See if you need the class at all

A MANAGER THAT WANTS TO BE A SINGLETON

class Bullet {
public:
  int getX() const { return x_; }
  int getY() const { return y_; }
  void setX(int x) { x_ = x; }
  void setY(int y) { y_ = y; }
private:
  int x_, y_;
};

class BulletManager {
public:
  Bullet* create(int x, int y);

  bool isOnScreen(Bullet& bullet) {
    return bullet.getX() >= 0 &&
           bullet.getX() < SCREEN_WIDTH &&
           bullet.getY() >= 0 &&
           bullet.getY() < SCREEN_HEIGHT;
  }

  void move(Bullet& bullet) {
    bullet.setX(bullet.getX() + 5);
  }
};

HOW MANY BULLETMANAGERS? ZERO.

class Bullet {
public:
  Bullet(int x, int y) : x_(x), y_(y) {}

  bool isOnScreen() {
    return x_ >= 0 && x_ < SCREEN_WIDTH &&
           y_ >= 0 && y_ < SCREEN_HEIGHT;
  }

  void move() { x_ += 5; }

private:
  int x_, y_;
};

MonsterManager, ParticleManager, SoundManager, ManagerManager… a “manager” that babysits another class usually just wants its methods moved into that class. OOP is letting objects take care of themselves.

What to do instead

PASS IT IN the simplest, and often the best: render(context) reached by: the caller and nobody else GET IT FROM THE BASE CLASS protected getLog() on GameObject reached by: its subclasses (a Subclass Sandbox) FROM SOMETHING ALREADY GLOBAL Game::instance().getAudioPlayer() one global, not five A SERVICE LOCATOR global, but the provider can change see Decoupling SINGLETON reached by: everything forever

keep every object’s scope as narrow as still gets the job done — and for “only one”, a static class or an assert in the constructor

State

THE GIST“Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.”

PROBLEM

A platformer heroine with isJumping_ and isDucking_. Every fix opens a new bug: air jumps, standing up in mid-air, jumping out of a dive.

PATTERN

A finite state machine: a fixed set of states, one at a time, inputs, and transitions between them.

  • Simplest: an enum and a switch on it.
  • The State pattern: one class per state, the heroine delegates to the current one.
  • enter() and exit() actions; static or per-machine states.
class HeroineState {
public:
  virtual void handleInput(Heroine& heroine, Input input) {}
  virtual void update(Heroine& heroine) {}
};

class DuckingState : public HeroineState {
public:
  DuckingState() : chargeTime_(0) {}

  virtual void handleInput(Heroine& heroine, Input input) {
    if (input == RELEASE_DOWN) {
      // Change to standing state...
    }
  }

  virtual void update(Heroine& heroine) {
    chargeTime_++;
    if (chargeTime_ > MAX_CHARGE) heroine.superBomb();
  }

private:
  int chargeTime_;  // only means anything while ducking
};

// Heroine just delegates:
//   state_->handleInput(*this, input);

seen in Unity’s AnimatorGodot’s AnimationTree state machinemenus, AI, network protocols

TWO WAYS

Flags, or a state machine

if (input == PRESS_B) {
  if (!isJumping_ && !isDucking_) jump();
} else if (input == PRESS_DOWN) {
  if (!isJumping_) duck();
  else { isJumping_ = false; dive(); }
}
switch (state_) {
  case STATE_STANDING: // B: jump, DOWN: duck
  case STATE_JUMPING:  // DOWN: dive
  case STATE_DUCKING:  // release DOWN: stand
  case STATE_DIVING:   // nothing - B is ignored
}

replaying jump, dive, jump… · click it, then Space is B and ↓ is down · or a controller: A and ↓

LIVE DEMO

States, and a stack of them

click it: Space jump · ↓ duck (hold to charge) / dive · F fire · controller: A, ↓, X · it plays itself until you do

Beyond a single machine

Concurrent

doing: JUMPING carrying: GUN

Armed and unarmed versions of every state is a combinatorial explosion. Two machines — state_ and equipment_ — each get every input.

Hierarchical

OnGround: B jumps, DOWN ducks STANDING WALKING RUNNING

A substate that doesn’t handle an input passes it up to its superstate — just like an overridden method calling the base class.

Pushdown

FIRING STANDING push, then pop back

An FSM has no memory of where it came from. A stack of states does: push Firing, pop it when done, and she’s back to whatever she was doing.

good fit: behaviour that depends on internal state, split into a few distinct options, driven by inputs over time — for rich AI, look at behaviour trees and planners

II

Sequencing Patterns

a game world lives at its own cadence — we have to invent its time

Double Buffer

INTENTCause a series of sequential operations to appear instantaneous or simultaneous.

PROBLEM

The display reads the framebuffer while we’re still writing to it. Half a face, then the old one: tearing.

PATTERN

Two stages in one theatre: play scene one on A while the stagehands set up scene two on the dark B, then switch the lights.

  • Read from current, write to next, swap() when done.
  • Not just pixels: any state that’s read while it’s being changed — even by the code changing it.
  • The swap must be atomic and quick. You pay with two copies in memory.
class Scene {
public:
  Scene() : current_(&buffers_[0]), next_(&buffers_[1]) {}

  void draw() {
    next_->clear();
    next_->draw(1, 1);
    // ...
    swap();
  }

  Framebuffer& getBuffer() { return *current_; }

private:
  void swap() {
    // Just switch the pointers.
    Framebuffer* temp = current_;
    current_ = next_;
    next_ = temp;
  }

  Framebuffer  buffers_[2];
  Framebuffer* current_;
  Framebuffer* next_;
};

seen in OpenGL’s swapBuffers()Direct3D swap chainsphysics and AI that must appear simultaneous

TWO WAYS

One buffer, or two

slowed right down: the beam scans what the player sees, the pen is the game drawing the next frame

LIVE DEMO

Slapstick comedians

Harry is slapped once; every frame the stage updates each actor in array order

Game Loop

INTENTDecouple the progression of game time from user input and processor speed.

PROBLEM

An event loop waits for input. A game can’t: the monster keeps chomping on your hero while you think. And it must run at the same speed on every machine.

PATTERN

Process input without blocking, update, render — and track the passage of time.

  • As fast as it can: speed depends on the machine.
  • Variable time step: keeps up, but non-deterministic and unstable.
  • Fixed update, variable render: catch up in fixed steps, render when you can, interpolate the leftover.
double previous = getCurrentTime();
double lag = 0.0;
while (true) {
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;

  processInput();

  while (lag >= MS_PER_UPDATE) {
    update();
    lag -= MS_PER_UPDATE;
  }

  render(lag / MS_PER_UPDATE);
}

If you’re in a browser or an engine, the platform owns the loop and calls you back — the pattern is still there.

seen in requestAnimationFrameGodot: _process(delta) and _physics_processUnity: Update and FixedUpdate“Fix Your Timestep”

LIVE DEMO

Same game, slower machine

60 fps

Update Method

INTENTSimulate a collection of independent objects by telling each to process one frame of behavior at a time.

PROBLEM

A skeleton guard patrolling, two statues firing lightning: each one’s counters and flags end up as loose variables mushed together in the main loop.

PATTERN

The world keeps a collection of objects. Each has an update() that does one frame of its behaviour. Each frame, call them all.

KEEP IN MIND

  • Slicing behaviour into frames means storing where you left off — State helps.
  • Not truly concurrent: update order matters.
  • Careful adding or removing objects while you walk the list →
class Skeleton : public Entity {
public:
  Skeleton() : patrollingLeft_(false) {}

  virtual void update() {
    if (patrollingLeft_) {
      setX(x() - 1);
      if (x() == 0) patrollingLeft_ = false;
    } else {
      setX(x() + 1);
      if (x() == 100) patrollingLeft_ = true;
    }
  }

private:
  bool patrollingLeft_;  // was a local in the loop
};

void World::gameLoop() {
  while (true) {
    // Handle user input...
    for (int i = 0; i < numEntities_; i++) {
      entities_[i]->update();
    }
    // Physics and rendering...
  }
}

seen in Unity: MonoBehaviour.Update()Godot: _process(delta)the core loop’s Pong

TWO WAYS

Removing while you iterate

for (int i = 0; i < numObjects_; i++) {
  objects_[i]->update();
  // a slain object is removed at once
}
for (int i = 0; i < numObjects_; i++) {
  if (!objects_[i]->isDead()) objects_[i]->update();
}
// then sweep out the dead

watch the peasant

III

Behavioral Patterns

a role-playing game has more behaviour than a word processor has features

Bytecode

INTENTGive behavior the flexibility of data by encoding it as instructions for a virtual machine.

PROBLEM

Spells for a wizard fighting game. Designers want to tweak them without a recompile, you want to patch them after ship, modders want to write their own — without crashing anyone.

PATTERN

An instruction set, encoded as a sequence of bytes. A virtual machine runs them one at a time, passing values on a stack.

  • The Interpreter pattern’s tree of objects is flexible but slow. Bytecode is dense and linear, like machine code — but sandboxed.
  • It grows like vines: keep it on a short leash.
  • You’ll need an authoring tool — and you’ll miss your debugger.
class VM {
public:
  void interpret(char bytecode[], int size) {
    for (int i = 0; i < size; i++) {
      char instruction = bytecode[i];
      switch (instruction) {
        case INST_LITERAL: {
          // The value is the next byte in the stream.
          int value = bytecode[++i];
          push(value);
          break;
        }
        case INST_SET_HEALTH: {
          int amount = pop();
          int wizard = pop();
          setHealth(wizard, amount);
          break;
        }
        // ... GET_HEALTH, ADD, DIVIDE
      }
    }
  }
};

seen in LuaGDScriptUnreal BlueprintsWren, Nystrom’s own language

LIVE DEMO

A spell, one byte at a time

wizard 0
health  45
agility 7
wisdom  11

Subclass Sandbox

INTENTDefine behavior in a subclass using a set of operations provided by its base class.

PROBLEM

A hundred superpowers, each written by someone different, each poking at audio, particles, physics, the renderer’s private layers. Duplicate code everywhere, coupling everywhere.

PATTERN

The base class offers protected provided operations and one abstract sandbox method. Subclasses only use what they’re given.

  • Coupling moves into one place: the base class.
  • A shallow, wide hierarchy — a point of leverage.
  • Watch for the brittle base class; split helpers out (getSoundPlayer()) when it bloats.
class Superpower {
protected:
  virtual void activate() = 0;  // the sandbox method

  void move(double x, double y, double z);
  void playSound(SoundId sound, double volume);
  void spawnParticles(ParticleType type, int count);
  double getHeroZ();
};

class SkyLaunch : public Superpower {
protected:
  virtual void activate() {
    // Spring into the air.
    playSound(SOUND_SPROING, 1.0f);
    spawnParticles(PARTICLE_DUST, 10);
    move(0, 0, 20);
  }
};

// Every power: inherit, override activate(),
// build it out of the provided operations.

seen in Unity: a MonoBehaviour calling GetComponent, InstantiateGodot: a Node calling get_node, add_child

TWO WAYS

Where the coupling lives

count what has to change

LIVE DEMO

SkyLaunch::activate()

click it, then Space (or A on a controller) · ground: spring · low: double jump · high: dive

Type Object

INTENTAllow the flexible creation of new “classes” by creating a single class, each instance of which represents a different type of object.

PROBLEM

Dragon : Monster, Troll : Monster, hundreds more. A designer emails: troll health 48 → 52. Check out, edit, recompile, check in, reply. Repeat.

PATTERN

Two classes and no inheritance: Monster and Breed. Every monster points at its breed; breeds come from data.

  • A breed can construct its monsters (newMonster()) and control their memory.
  • Breeds can inherit from a parent breed — at lookup, or copied down at load.
  • Data is easy; behaviour per type is harder (pick from a fixed set, or Bytecode).
class Breed {
public:
  Breed(int health, const char* attack)
  : health_(health), attack_(attack) {}

  Monster* newMonster() { return new Monster(*this); }
  int getHealth() { return health_; }
  const char* getAttack() { return attack_; }

private:
  int health_;  // Starting health.
  const char* attack_;
};

class Monster {
public:
  const char* getAttack() { return breed_.getAttack(); }

private:
  int health_;  // Current health.
  Breed& breed_;
};
{
  "Troll":        { "health": 25, "attack": "The troll hits you!" },
  "Troll Archer": { "parent": "Troll", "health": 0,
                    "attack": "The troll archer fires an arrow!" }
}

seen in Unity ScriptableObjectsGodot custom Resourcesevery item database

TWO WAYS

Classes, or breeds

IV

Decoupling Patterns

change one piece without having to change another

Component

INTENTAllow a single entity to span multiple domains without coupling the domains to each other.

PROBLEM

Bjørn, a Danish baker in a platformer: input, physics, animation, rendering and sound in one 5,000-line class. Changing anything means knowing everything:

if (collidingWithFloor() && getRenderState() != INVISIBLE) playSound(HIT_FLOOR);

PATTERN

Slice the class along domain lines into components. The entity becomes a container that holds them, and the state they share.

  • Swap one component: DemoInputComponent and the computer plays.
  • Components talk through the container’s state, direct references, or messages.
class GameObject {
public:
  int velocity;
  int x, y;

  GameObject(InputComponent* input,
             PhysicsComponent* physics,
             GraphicsComponent* graphics)
  : input_(input), physics_(physics), graphics_(graphics) {}

  void update(World& world, Graphics& graphics) {
    input_->update(*this);
    physics_->update(*this, world);
    graphics_->update(*this, graphics);
  }

private:
  InputComponent* input_;
  PhysicsComponent* physics_;
  GraphicsComponent* graphics_;
};

GameObject* createBjorn() {
  return new GameObject(new PlayerInputComponent(),
                        new BjornPhysicsComponent(),
                        new BjornGraphicsComponent());
}

seen in Unity: GameObject + componentsGodot: a scene built from child nodesECS engines

Inheritance can’t pick the parts

GameObject Zone + collision Decoration + rendering Prop + rendering (again) the deadly diamond
objectphysicsgraphics
Decoration—■
Zone■—
Prop■■

one GameObject class, two component classes — no duplication, no multiple inheritance, three classes instead of four

LIVE DEMO

Plug and play

click a socket to plug a component in or out · click the world, then ← → ↑ drive Bjørn · or a controller

Event Queue

INTENTDecouple when a message or event is sent from when it is processed.

PROBLEM

Audio::playSound() sprinkled everywhere. It blocks while the sound loads; two enemies hit in one frame play the same wail twice as loud; and it runs on whichever thread called it.

PATTERN

Sending enqueues a request and returns at once. The receiver pulls requests later, when it suits it — push on one end, pull on the other, a buffer in between.

  • A ring buffer: an array whose head and tail wrap around.
  • The receiver can merge, delay or drop requests.
  • Careful: the world changes before the event is handled, and loops of events don’t crash — they slosh.
void Audio::playSound(SoundId id, int volume) {
  // Merge with a pending request for the same sound.
  for (int i = head_; i != tail_;
       i = (i + 1) % MAX_PENDING) {
    if (pending_[i].id == id) {
      pending_[i].volume = max(volume, pending_[i].volume);
      return;
    }
  }

  assert((tail_ + 1) % MAX_PENDING != head_);

  pending_[tail_].id = id;
  pending_[tail_].volume = volume;
  tail_ = (tail_ + 1) % MAX_PENDING;
}

void Audio::update() {
  if (head_ == tail_) return;

  ResourceId resource = loadSound(pending_[head_].id);
  int channel = findOpenChannel();
  if (channel == -1) return;
  startSound(resource, channel, pending_[head_].volume);

  head_ = (head_ + 1) % MAX_PENDING;
}

seen in every OS event loopGodot: call_deferred()Go channelspub/sub

LIVE DEMO

A ring buffer of sounds

Service Locator

INTENTProvide a global point of access to a service without coupling users to the concrete class that implements it.

PROBLEM

Physics, AI and UI all play sounds. AudioSystem::instance() at every call site ties each one to the concrete class and to how to reach it.

PATTERN

A phone book. Callers look the service up by its interface; the locator hides who provides it and how it was found.

  • Null service: nothing registered? You get one that does nothing, not a crash.
  • Decorated service: wrap the real one in LoggedAudio to log every call.
  • Still global: use it sparingly. Pass the object in first.
class Locator {
public:
  static void initialize() { service_ = &nullService_; }

  static Audio& getAudio() { return *service_; }

  static void provide(Audio* service) {
    if (service == NULL) {
      // Revert to null service.
      service_ = &nullService_;
    } else {
      service_ = service;
    }
  }

private:
  static Audio* service_;
  static NullAudio nullService_;
};

// At startup, somewhere that knows the concrete class:
Locator::provide(new ConsoleAudio());

// Everywhere else:
Locator::getAudio().playSound(VERY_LOUD_BANG);

seen in Unity: GetComponentXNA’s GameServicesdependency injection containers

LIVE DEMO

Look it up, don’t hard-wire it

click a caller to play its sound

V

Optimization Patterns

games are one of the few places where the hardware is still the limit

Data Locality

INTENTAccelerate memory access by arranging data to take advantage of CPU caching.

PROBLEM

CPUs got fast; RAM didn’t. Fetching a byte from main memory can cost hundreds of cycles. An accountant who can do a box in a minute, with a warehouse that takes a day to bring one.

PATTERN

Every read fetches a whole cache line — the pallet the forklift brings anyway. Keep data contiguous, in the order you process it, and use the whole pallet.

  • Arrays of components, not pointers to them.
  • Active objects packed at the front.
  • Hot data apart from cold.
// Before: pointer, pointer, cache miss, cache miss.
for (int i = 0; i < numEntities; i++) {
  entities[i]->ai()->update();
}

// After: one flat array per component type.
AIComponent* aiComponents =
    new AIComponent[MAX_ENTITIES];
PhysicsComponent* physicsComponents =
    new PhysicsComponent[MAX_ENTITIES];

while (!gameOver) {
  for (int i = 0; i < numEntities; i++) {
    aiComponents[i].update();
  }
  for (int i = 0; i < numEntities; i++) {
    physicsComponents[i].update();
  }
  // ...
}

In Nystrom’s test: the same work, fifty times faster.

seen in Unity DOTSBevyany ECS

TWO WAYS

Chasing pointers, or streaming

each miss fetches a line of eight cells into the cache; a miss stalls, a hit is nearly free

Two more ways to fill the line

Packed data

if (isActive()) — skips across memory sorted: the first numActive_ are live

Activating swaps a particle with the first inactive one. No flag to check — and no flag to store.

Hot / cold splitting

AIComponent animation_, energy_, goalPos_ — every frame loot_ → LootDrop used once, when it dies

Data you touch every frame stays in the packed array; data you need once in a lifetime moves out behind a pointer.

Dirty Flag

INTENTAvoid unnecessary work by deferring it until the result is needed.

PROBLEM

A scene graph: a parrot on a pirate in a crow’s nest on a ship. Its world transform is the whole parent chain combined. In one busy frame all four move — recalculating eagerly does ten combines, the parrot four times.

PATTERN

Primary data (local transforms), derived data (world transforms), and one bit: set when the primary changes, checked and cleared when the derived is needed.

  • Worth it when primary data changes more often than derived data is used, and can’t be updated incrementally.
  • Set the flag on every change. Miss one and you use stale data.
void GraphNode::setTransform(Transform local) {
  local_ = local;
  dirty_ = true;
}

void GraphNode::render(Transform parentWorld, bool dirty) {
  dirty |= dirty_;
  if (dirty) {
    world_ = local_.combine(parentWorld);
    dirty_ = false;
  }

  if (mesh_) renderMesh(mesh_, world_);

  for (int i = 0; i < numChildren_; i++) {
    children_[i]->render(world_, dirty);
  }
}

The dirty parameter carries a parent’s dirtiness down, so moving a node never has to walk its children.

seen in the “unsaved changes” dot in your editorengines’ cached global transformssleeping bodies in physics engines

LIVE DEMO

A parrot on a pirate on a ship

Object Pool

INTENTImprove performance and memory use by reusing objects from a fixed pool instead of allocating and freeing them individually.

PROBLEM

Every wave of the wand spawns hundreds of sparkles. Allocating that fast is slow — and fragments the heap: fourteen bytes free in two seven-byte holes still can’t fit twelve.

PATTERN

Allocate the whole array up front. Each object knows if it’s in use; create() takes a free one, death hands it back.

  • A free list threads through the dead objects’ own memory: constant-time create, no extra bytes.
  • When it’s full: size it so it never is, don’t create, kill the least noticeable, or grow.
  • Reused objects aren’t cleared — initialise everything.
class Particle {
  // ...
private:
  int framesLeft_;
  union {
    // State when it's in use.
    struct { double x, y, xVel, yVel; } live;
    // State when it's available.
    Particle* next;
  } state_;
};

void ParticlePool::create(double x, double y,
                          double xVel, double yVel,
                          int lifetime) {
  // Make sure the pool isn't full.
  assert(firstAvailable_ != NULL);

  // Remove it from the available list.
  Particle* newParticle = firstAvailable_;
  firstAvailable_ = newParticle->getNext();

  newParticle->init(x, y, xVel, yVel, lifetime);
}

seen in Unity: UnityEngine.Pool.ObjectPool<T>bullets in every shootersound channels

LIVE DEMO

Sparkles from a pool

click the sky to cast · cast fast enough and the pool runs dry

Spatial Partition

INTENTEfficiently locate objects by storing them in a data structure organized by their positions.

PROBLEM

Real-time strategy: hundreds of units, and each needs to know who it’s close enough to hit. Comparing every pair grows with the square of the army.

PATTERN

On a 1D battle line you’d sort by position. In 2D, lay graph paper over the field: each cell keeps the units inside it, and a unit only checks its own cell and its neighbours.

grida persistent bucket sort
BSP, k-d tree, BVHbinary search trees
quadtree, octreetries
void Grid::handleCell(int x, int y) {
  Unit* unit = cells_[x][y];
  while (unit != NULL) {
    // Handle other units in this cell.
    handleUnit(unit, unit->next_);

    // Also try half of the neighboring cells,
    // so no pair is checked twice.
    if (x > 0 && y > 0) handleUnit(unit, cells_[x - 1][y - 1]);
    if (x > 0) handleUnit(unit, cells_[x - 1][y]);
    if (y > 0) handleUnit(unit, cells_[x][y - 1]);
    if (x > 0 && y < NUM_CELLS - 1) {
      handleUnit(unit, cells_[x - 1][y + 1]);
    }

    unit = unit->next_;
  }
}

seen in physics broadphaseBox2D’s dynamic treequadtrees in 2D games

LIVE DEMO

Graph paper on a battlefield

hover a unit to see which cells it checks

See also

hover a pattern — every link is one the book draws in its See Also sections

Get on with it, already

  • Abstraction and decoupling make evolving a program easier — but only spend them where you’re confident the flexibility is needed.
  • Design for performance throughout, but put off the low-level optimisations that lock assumptions in until as late as possible.
  • Move quickly to explore the design space, but don’t leave a mess you’ll have to live in.
  • If you’re going to throw code away, don’t waste time making it pretty. Rock stars trash hotel rooms because they check out tomorrow.
  • Most of all: if you want to make something fun, have fun making it.

Read the book

gameprogrammingpatterns.com

Robert Nystrom — free to read on the web, in print and as an ebook to support him

the quotes, examples and code are the book’s (code trimmed to fit); the demos are new — back to the map