Game Programming Patterns

I wanted to cleanse my palette and decided to read Game Programming Patterns by Robert Nystrom. I’ve read it before so I knew what to expect. It’s a clearly written pattern book with simple example situation taken from the games industry. Don’t expect to get all the patterns here or advanced game programming knowledge. However if you want to get a general introduction for patterns before you read something bigger then this is great. Similarly if you want to write games but your spaghetti code is getting out of hand then this could be a lifeline.

Design patterns

Algorithms are often likened to recipes, a series of precise instructions. Following the instructions in a recipe takes ingredients and turns them into food. Following the instructions in an algorithm can, say, take an unsorted list of values and turn it into a sorted list. Libraries can often offer specific algorithms pre-packaged for easy use.

Design patterns are normally less precise, more a list of expected features than a proscription on how to do them. So less a recipe and more a style of cooking or a set of ingredients to use. Libraries might offer tools to help but it’s more often about recreating the pattern in your own code.

I don’t really think about named design patterns or plan my projects around them but I still use them all the time. For me a Builder is a classic example of this. You could try to manually construct a complicated object. However that can be require in-depth knowledge to do correctly or transform the object through a series of intermediate states. It can be safer and easier to encapsulate the construction within a builder. This can collect the information necessary for construction and then, in one go, produce the object. I don’t think “Let me use the builder pattern.” I think “Having a CommandLineBuilder means I don’t have to worry about escaping arguments with spaces.”

Presentation

Each section of the book covers a number of associated patterns. Each pattern gets one or two examples to get you familiar with them. Then there’s a discussion about the pros and cons of different variations of the pattern. For games the balance is normally between the flexibility of the system and the performance it provides. Really this is exactly what I’m writing about for balance programming. Having everything isn’t normally possible so how do you go about picking what’s best for your situation.

The sample code is really simple, sticking to straight C. I might have preferred something more modern but he’s made it accessible to everyone. I understand the choice. You’ll have to translate the examples into your preferred language.

Content overview

  • Design Patterns Revisited
    • Command: A reified method call, configuring input controls plus undo-redo.
    • Observer: Achievements, I’ll talk about this later.
    • Flyweight: A light weight placeholder, a forest of trees and terrain grids.
    • Prototype: Cloning an original, monster spawners and inheritance.
    • Singleton: A potentially dangerous singular global service.
    • State: Changing behaviour, finite state machines and variations.
  • Sequencing Patterns
    • Double Buffer: Dealing with simultaneous access, rendering and state changes.
    • Game Loop: Running the game, separating rendering and physics.
    • Update Method: Simulating many objects, movement and combat.
  • Behavioural Patterns
    • Bytecode: Virtual machines for complex behaviours, building magic spells.
    • Subclass Sandbox: Separating classes from the environment they need to interact with.
    • Type Object: Different monsters without different classes of monster.
  • Decoupling Patterns
    • Component: Composing behaviours from individual components, avoiding duplication and multiple inheritance.
    • Event Queue: Separating requests from fulfilling those requests.
    • Service Locator: Swapping out one system for another, plus null and logging services.
  • Optimization Patterns
    • Data Locality: High performance by organising your data. Better than the last book that covered it.
    • Dirty Flag: One bit to skip a lot of recalculation, fewer matrix multiplies.
    • Object Pool: Controlling object creation and avoiding fragmentation.
    • Spatial Partition: Divide and conquer space for faster processing.

Implementing Achievements

I think an achievement system might be one of the best examples of avoiding spaghetti code. An achievement could be practically anything:

  • Killing a specific number of giant rats.
  • Finding all four quarters of the treasure map.
  • Surviving 10 nights outside the city.
  • Dying in an explosion that you caused.
  • Playing for 24 hours of real time.

A naive implementation might have calls to the achievement system everywhere in your codebase. So physics, rendering, collision detection, inventory management, the weather system, it could go on and on. Every time the designer comes up with a new achievement the developers would have to rewire things. If the interface to the achievement system ever change then everything connected to it would also change. This can work on a small scale but sounds like a nightmare on a large scale. You don’t want to write a quick hack and then gradually find it growing until you are living that nightmare.

One solution to this is the Observer pattern. It allows one piece of code to announce something is happening without caring about who is listening. There might be no-one listening, there might be a dozens of other systems listening.

This is classically done with an Observer and a Subject but I might use Observer and Observed instead:

class Observed {
    void Add(Observer& observer) {
        m_Observers.push_back(&observer);
    }

    void Remove(Observer& observer) {
        std::erase(m_Observers, observer);
    }

    void Notify(const Entity& entity, Event event) {
        for (const auto observer : m_Observers) {
            observer->OnNotify(entity, event);
        }
    }

private:
    std::vector<Observer*> m_Observers;
};

class Observer {
public:
    virtual void OnNotify(const Entity& entity, Event event) = 0;
};

Dead simple. So in our achievement system example we would make our AchievementManager an Observer and various game subsystems would be Observed.

class AchievementManager: public Observer {
...
    void Register() {
        Get<MonsterManager>().Add(*this);
        Get<InventoryManager>().Add(*this);
        Get<MapManager>().Add(*this);
        Get<TimeManager>().Add(*this);
    }

    void OnNotify(const Entity& entity, Event event) override {
        switch (event) {
        case DEATH:
            if (entity.IsSpecies("giant rat")) {
                ...
            }
            else if (entity.IsPlayer()) {
                ...
            }
        }
    }
...
};

I think this is still a bit rough and ready. Each manager is an Observed class and so has to distinguish any action entirely based on the Entity and Event. This means it a lot of messages might get sent only to be ignored at the other end. It might be better to have each manager contain a number of Observed objects. That way the AchievementManager can pick and choose what it is really interested in:

class MonsterManager {
...
    Observed& Spawn;
    Observed& Arrive;
    Observed& Summon;
    Observed& Damage;
    Observed& Death;
    Observed& Despawn;
...
};

This pattern is very flexible and open to extension. If, say, you want the SaveManager to auto-save after a boss is killed then nothing has to change in MonsterManager, just add in another registration.

The book covers some perceived problems with this pattern:

  • It could slow things down: The overhead of sending is low although you have to make sure observers don’t take too long. Keep it out of the tightest of loops but it should be okay otherwise.
  • It could be too fast: Notifications are sent immediately and if threads are involved this could complicate things. You might need to involve an Event Queue.
  • It allocates memory: Excessive memory allocation can fragment the heap in games. If this is a problem then pre-allocating more space or using a list based approach is an option.
  • Destroying objects: If an Observer is destroyed then the Observer needs to be updated. This can be done manually although it can need some care or
    the association can be made two-way so that objects are automatically unregistered. If it’s done wrong then you can end up with zombie observers forever taking up processing.
  • More complex debugging: Although the pattern stops systems from knowing about each other they are still interacting. If something is going wrong it can be much less obvious what system is responsible.

On balance

If any of this sounds remotely interesting I’d highly recommend giving this a read. It’s inspired me to put another pattern book on my list but I doubt it will be as nice as this one.


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *