Feineigle.com - Game Programming Patterns

Home · Book Reports · 2023 · Game Programming Patterns

Published: October 29, 2023
Tags:  Games · Programming



The book in...
One sentence:
Gang of Four meets video games.

Five sentences:
This is a fairly no-nonsense look at programming patterns that are specifically, but not exclusively useful for games. Each patterns starts out with a basic definition and a simple use case for when the pattern might be applicable. Next, through pared down C++, a simple code example is given; this is often built up in a few steps so the reader can follow what is going on. After the pattern is thoroughly explained there are usually a few warning or gotchas to look out for when using the pattern. I like how the author, on several occasions, stresses that even though a pattern can work you should, when possible, opt for the simpler solution unless you are certain you need the full power of some of the more complex patterns.

designates my notes. / designates important. / designates very important.


Thoughts

I appreciate the humor sprinkled throughout.


Table of Contents


· Chapter 00 - Introduction

page 14:
page 17:

· Chapter 01 - Design Patterns Revisited

Command

Flyweight

page 40:

Observer

page 54:
page 57:
page 60:
page 61:
page 62:
page 63:
page 64:
page 65:

Prototype

page 71:
page 79:
page 80:
{
  "name": "goblin grunt",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"]
}
{
  "name": "goblin wizard",
  "prototype": "goblin grunt",
  "spells": ["fire ball", "lightning bolt"]
}
{
  "name": "goblin archer",
  "prototype": "goblin grunt",
  "attacks": ["short bow"]
}

Singleton

page 88:

This chapter is primarily about AVOIDING overuse of singletons.

page 92:

State

page 103:
enum State
{
  STATE_STANDING,
  STATE_JUMPING,
  STATE_DUCKING,
  STATE_DIVING
};
page 105:
class HeroineState
{
public:
  virtual ~HeroineState() {}
  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...
      heroine.setGraphics(IMAGE_STAND);
    }
  }
  virtual void update(Heroine& heroine) {
    chargeTime_++;
    if (chargeTime_ > MAX_CHARGE)
    {
      heroine.superBomb();
    }
  }
private:
  int chargeTime_;
};
page 106:
class Heroine
{
public:
  virtual void handleInput(Input input)
  {
    state_->handleInput(*this, input);
  }
  virtual void update()
  {
    state_->update(*this);
  }
  // Other methods...
private:
  HeroineState* state_;
};
page 116:
  1. You can push a new state onto the stack. The “current” state is always the one on top of the stack, so this transitions to the new state. But it leaves the previous state directly under it on the stack instead of discarding it.

  2. You can pop the topmost state off the stack. That state is discarded, and the state under it becomes the new current state.

· Chapter 02 - Sequencing Patterns

Double Buffer

page 121:

Game Loop

page 140:
page 145:

Update Method

page 160:
page 168:

Favor ‘object composition’ over ‘class inheritance’.

page 170:
page 172:
page 174:

· Chapter 03 - Behavioral Patterns

Bytecode

page 182:

Subclass Sandbox

Type Object

page 230:
class Breed
{
public:
    Monster* newMonster() { return new Monster(*this); }
    Breed(int health, const char* attack)
    : health_(health),
    attack_(attack)
    {}
        
    int getHealth() { return health_; }
    const char* getAttack() { return attack_; }
        
private:
    int health_; // Starting health.
    const char* attack_;
};
class Monster
{
    friend class Breed;

public:
    const char* getAttack() { return breed_.getAttack(); }
  
private:
    Monster(Breed& breed)
    : health_(breed.getHealth()),
    breed_(breed)
    {}
    
    int health_; // Current health.
    Breed& breed_;
};
Monster* monster = new Monster(someBreed);
Monster* monster = someBreed.newMonster();
page 231:
class Breed
{
public:
  Breed(Breed* parent, int health, const char* attack)
  : parent_(parent),
    health_(health),
    attack_(attack)
  {}
  int         getHealth();
  const char* getAttack();
private:
  Breed*      parent_;
  int         health_; // Starting health.
  const char* attack_;
};
page 232:

There are two ways we can implement this. One is to handle the delegation dynamically every time the attribute is requested, like this:

int Breed::getHealth()
{
  // Override
  if (health_ != 0 || parent_ == NULL) return health_;
  // Inherit.
  return parent_->getHealth();
}
const char* Breed::getAttack()
{
  // Override.
  if (attack_ != NULL || parent_ == NULL) return attack_;
  // Inherit.
  return parent_->getAttack();
}
Breed(Breed* parent, int health, const char* attack)
: health_(health),
  attack_(attack)
{
  // Inherit non-overridden attributes.
  if (parent != NULL)
  {
    if (health == 0) health_ = parent->getHealth();
    if (attack == NULL) attack_ = parent->getAttack();
  }
}
int         getHealth() { return health_; }
const char* getAttack() { return attack_; }
{
  "Troll": {
    "health": 25,
    "attack": "The troll hits you!"
  },
  "Troll Archer": {
    "parent": "Troll",
    "health": 0,
    "attack": "The troll archer fires an arrow!"
  },
  "Troll Wizard": {
    "parent": "Troll",
    "health": 0,
    "attack": "The troll wizard casts a spell on you!"
  }
}

· Chapter 04 - Decoupling Patterns

Component

page 243:
7.1.2. 249:
class Bjorn
{
public:
  Bjorn()
  : velocity_(0),
    x_(0), y_(0)
  {}
  void update(World& world, Graphics& graphics);
private:
  static const int WALK_ACCELERATION = 1;
  int velocity_;
  int x_, y_;
  Volume volume_;
  Sprite spriteStand_;
  Sprite spriteWalkLeft_;
  Sprite spriteWalkRight_;
};
void Bjorn::update(World& world, Graphics& graphics)
{
  // Apply user input to hero's velocity.
  switch (Controller::getJoystickDirection())
  {
    case DIR_LEFT:
    velocity_ -= WALK_ACCELERATION;
    break;
    case DIR_RIGHT:
    velocity_ += WALK_ACCELERATION;
    break;
  }
  // Modify position by velocity.
  x_ += velocity_;
  world.resolveCollision(volume_, x_, y_, velocity_);
  // Draw the appropriate sprite.
  Sprite* sprite = &spriteStand_;
  if (velocity_ < 0)
  {
    sprite = &spriteWalkLeft_;
  }
  else if (velocity_ > 0)
  {
    sprite = &spriteWalkRight_;
  }
  graphics.draw(*sprite, x_, y_);
page 250:
class InputComponent
{
public:
  void update(Bjorn& bjorn)
  {
    switch (Controller::getJoystickDirection())
    {
      case DIR_LEFT:
        bjorn.velocity -= WALK_ACCELERATION;
        break;
      case DIR_RIGHT:
        bjorn.velocity += WALK_ACCELERATION;
        break;
    }
  }
private:
  static const int WALK_ACCELERATION = 1;
};
class Bjorn
{
public:
  int velocity;
  int x, y;
  void update(World& world, Graphics& graphics)
  {
    input_.update(*this);
    
    // Modify position by velocity.
    x += velocity;
    world.resolveCollision(volume_, x, y, velocity);
    
    // Draw the appropriate sprite.
    Sprite* sprite = &spriteStand_;
    if (velocity < 0)
    {
      sprite = &spriteWalkLeft_;
    }
    else if (velocity > 0)
    {
      sprite = &spriteWalkRight_;
    }
    graphics.draw(*sprite, x, y);
  }
  
private:
  InputComponent input_;
  
  Volume volume_;
  
  Sprite spriteStand_;
  Sprite spriteWalkLeft_;
  Sprite spriteWalkRight_;
};
page 251:
class PhysicsComponent
{
public:
  void update(Bjorn& bjorn, World& world)
  {
    bjorn.x += bjorn.velocity;
    world.resolveCollision(volume_,
        bjorn.x, bjorn.y, bjorn.velocity);
  }
private:
  Volume volume_;
};
page 252:
class GraphicsComponent
{
public:
  void update(Bjorn& bjorn, Graphics& graphics)
  {
    Sprite* sprite = &spriteStand_;
    if (bjorn.velocity < 0)
    {
      sprite = &spriteWalkLeft_;
    }
    else if (bjorn.velocity > 0)
    {
      sprite = &spriteWalkRight_;
    }
    
    graphics.draw(*sprite, bjorn.x, bjorn.y);
  }
  
private:
  Sprite spriteStand_;
  Sprite spriteWalkLeft_;
  Sprite spriteWalkRight_;
};
class Bjorn
{
public:
  int velocity;
  int x, y;
  void update(World& world, Graphics& graphics)
  {
    input_.update(*this);
    physics_.update(*this, world);
    graphics_.update(*this, graphics);
  }
private:
  InputComponent input_;
  PhysicsComponent physics_;
  GraphicsComponent graphics_;
};
page 258:
  1. By modifying the container object’s state

  2. By referring directly to each other

  3. By sending messages

page 260:

Event Queue

page 271:
page 272:
  1. A sends an event.
  2. B receives it and responds by sending an event.
  3. That event happens to be one that A cares about, so it receives it. In response, it sends an event…
  4. Go to 2.
page 280:
page 284:

Service Locator

· Chapter 05 - Optimization Patterns

Data Locality

Dirty Flag

Object Pool

page 361:

Spatial Partition

page 365:
page 380: