Game Programming Patterns: Revisiting Design Patterns

This book has a game programming perspective, but it’s applicable to software of all sorts. It’s kind of a modern take on the original GoF Design Patterns, showing the evolving meaning of the some and extending others.

Redux author Dan Abrahamov has recommended it, saying he “wouldn’t claim it’s the best book ever” but also that it’s “practical and fun” and “required reading for UI engineers.” You certainly get explanations of some of the trends in programming models that motivated the creators of React, like favoring functional reactive components over imperative events, components over class hierachies, etc.

The patterns

I. Command

II. Flyweight

III. Prototype

IV. Singleton

V. State

Command

  • Defintion: an object-oriented replacement for callbacks
    • That is, a method call wrapped in an object
  • Supports attaching additional behavior to actions:
    • Permissions
    • Logging metadata

Actor controls example: BUTTON_A -> jumpCommand BUTTON_B -> shootCommand

ShootCommand extends Command {
    execute(actor) {
        actor.jump()
    }
}
  • Allows AI and player to be treated the same: just emit commands
  • Allows easily switching the actor that a player controls

Undoing

  • Tie an actor and previous state to each command
  • Keep a command history
  • Undoing a command means:
  • Pop the command off the command history
  • Set the state of the actor to the previous state

Ties in with:

  • Subclass sandbox: execute method may be better expressed as a composition of other methods
  • Chain of responsibility: actor may delegate to another object (e.g., shoot delegates to actor.gun)
  • Flyweight: if we have multiple controls creating a JumpCommand that’s largely the same, we might implement it as a Flyweight

Flyweight

  • If there are elements duplicated among objects, limit the per-instance data to whatever’s unique and point each object to a shared copy of the rest.
Tree1 Tree2 Tree3
|————|———|
|
TreeModel
  • Another example: a tile array points to terrain instances, which store info about each terrain type:
  [tile1, tile2, tile3, tile4]
  |———|    |    |—————————|
  |        |    |
  Grass River Hill
  • Retains the advantages of an API that works with real objects

Observer

  • This is an extremely ubiquitous pattern, baked into, eg, Javascript and C# with the “Event” class.
  • Decouples the logic of responding to events from creating the events
Subject (ie, event emitter)
-- Observer[] (ie, event handlers)
---- onNotify(event)
-- notify(event) (ie, emit)
  • Potential concerns:
    • Speed: it’s fast, with little overhead (just dynamic dispatch)
    • Blocking: if event handlers are synchronous, they should do not too much work or risk blocking the notifying thread
    • Observer list is dynamically allocated, but observers can also be treated as a linked list (with pointers part of the observer state)
    • Memory management: observers must take care to deregister themselves, including in GCed languages (where they otherwise won’t be GCed)
    • Reasoning about behavior: hard to do statically, since the list of observers is only know at runtime (and these can come from anywhere in the code base)
    • Observers and subjects are loosely coupled, and reducing dependency is generally good. But if there is inherent dependency, best make it explicit.

Once common in UI programming, declarative reactive programming has replaced many use cases. A prescient quote:

Like other declarative systems, data binding is probably a bit too slow and complex to fit inside the core of a game engine. But I would be surprised if I didn’t see it start making inroads into less critical areas of the game like UI.

Prototype

  • Example: a spawner creates monsters. The spawner holds a monster “prototype,” which can be cloned to create new monster instances.
Spawner {
    prototype: Monster;

    spawn() -> Monster {
        return prototype.clone();
    }
}
  • Concerns:

    • We usually wouldn’t maintain a massive class hierarchy for monsters, but make them instances of a monster type class.
  • Alternatives:

    • Type parameters:
Spawner<T> {

    spawn() -> T {
        return new T();
    }
}
  • Another application: reducing redundancy in data modeling:
goblin = {
    height: 10,
    width: 10
}

greenGoblin = {
    prototype: goblin,
    color: "green"
}

Prototypes in Javascript and other languages

  • In a pure prototype-based language – Self is the first example – objects are created by cloning other objects, at which point other fields can be added.
  • ES5 classes more closely resemble traditional classes, just defined a bit strangely (something that ES6 basically admits). A constructor is a function, not an object that can be cloned.

Singleton

  • Alternatives:
    • For preventing multiple instances:
    • Can make assert that a class is only created once, but not give global access
    • For “manager” or utility classes:
    • Use utility functions instead
    • For convenient access:
    • Attach to state that’s already local (eg, attach logger to the game instance). Would, eg, prevent clashes between logs if multiple game instances are required in the future.

State

  • Using dynamic dispatch:
    • Assume we have a set of actions, like update and handleInput, that depend on state. We implement that as classes that implement those actions as virtual methods:
class DuckingState : public HeroineState {
    virtual void handleInput(Heroine& heroine, Input input) {
        ...
    }

    virtual void update(Heroine& heroine) {
        ...
    }
}
  • State transitions can be delegated to the individual states. Here, handleInput returns the new state:
class GameState {
    handleInput(input: Input)
    {
        this.state = this.state.handleInput(input);
        state.enter()
    }
}

Concerns:

  • A single state is often not enough. Eg, a character can be both running and jumping.
    • Mulitple state machines can solve this problem, but this doesn’t model interaction well.
    • Hierachical state machines: can delegate common behavior to parent states. Has the normal problems of inheritance, though, like increased complexity of hidden behavior.