Game Programming Patterns

Czech technical University in Prague
Faculty of Information Technology
Department of Software Engineering
© Adam Vesecký, MI-APH, 2019

Serialization

Object Serialization

Serialized descriptors

  • XML, JSON, INI, CFG,...
  • some languages (C++) don't provide a standardized serialization facility
  • JSON is a good fit for JavaScript as it is it's natural object notation

Database

  • Web environment: Web storage (small data), IndexedDB (large data)
  • Desktop/mobile: SQLite, CastleDB,...
  • Games that don't have a multiplayer facility rarely need more than a simple database for config and save data

Save data

  • dump of all the parts of the game required to restore the full state
  • size varies, for most of the games it's usually several MB

Example: Web storage

Local Storage

  • for data of a smaller size (config, game state)
  • up to 10MB, no expiration time
  • keys and values are strings

let myStorage = window.localStorage;

 

myStorage.setItem('player_state', player.stateId);

myStorage.removeItem('player_state');

myStorage.clear();

IndexedDB

  • JS-based object-oriented DB
  • low-level API for structured data, including files/blobs
  • a bit cumbersome, yet many wrappers have been developped, such as DEXIE

await db.players.add({

  name: 'DoDo',

  avatar: await getBlob('dodo.png'),

  key_mapping: 'default'

});

Example: OpenTTD save format

  • 3 data layers
  • very complex format

Example: Doom save format

  • DSG files
  • the game loads the level and restores the state by going through its aspects
  • virtually nullifies the feasibility of the manual hacking except the player data
  • Aspects:

    • doors, switches, elevators, stairs, lights
    • items picked/available
    • projectiles/teleport fog/respawn
    • animation, damage
    • linedefs seen on automap
    • items/kills gained counter
LINK

// p_saveg.c::P_UnArchiveThinkers 

while (1) {

  tclass = *save_p++;

  switch (tclass) {   

    case tc_mobj:

      PADSAVEP();

      mobj = Z_Malloc (sizeof(*mobj), PU_LEVEL, NULL);

      memcpy (mobj, save_p, sizeof(*mobj));

      save_p += sizeof(*mobj);

      mobj->state = &states[(int)mobj->state];

      mobj->target = NULL;

      if (mobj->player) {

        mobj->player = &players[(int)mobj->player-1];

        mobj->player->mo = mobj;

      }

      P_SetThingPosition (mobj);

      mobj->info = &mobjinfo[mobj->type];

      P_AddThinker (&mobj->thinker);

Custom save data

Serialized object

  • either dump binary data or text files (JSON, XML,...)
  • circular dependencies and pointers need to be handled manually

Parsable format

  • binary format or a text file
  • text file is a good choice if the content isn't very large and security isn't concerned

Grid-based map in .txt file

Performance

Performance issues

Memory caching issues

  • CPU first tries to find data in the L1 cache
  • then it tries the larger but higher-latency L2 cache
  • then it tries L3 cache and DDR memory

Avoiding cache miss

  • arrange your data in RAM in such a way that min cache misses occure
  • organise data in contiguous blocks that are as small as possible
  • avoid calling functions from within a performance-critical section of code

Avoiding branch missprediction

  • branch = when you use an IF statement
  • pipelined CPU tries to guess at which branch is going to be taken
  • if the guess is wrong, pipeline must be flushed

// iterating inside out -> SLOW

for (i = 0 to size)

  for (j = 0 to size)

    do something with array[j][i]

    

// iterating outside in -> FAST

for (i = 0 to size)

  for (j = 0 to size)

    do something with array[i][j]


// assume only 50% active/visible objects

gameLoop(delta, absolute) {

  for(var entity in this.entities) {

    // 50% mispredictions

    if(entity.ACTIVE) { 

      entity.update(delta, absolute);

    }

    if(entity.VISIBLE) {

      entity.draw();

    }

  }

}

Memory gap

  • we can process data faster than ever, yet we can't get that data faster
  • RAM isn't so random access anymore
  • it can take hundreds of cycles to fetch a byte of data from RAM
  • algorithm full of cache misses can be up to 50x slower

Example: data stored randomly in memory

Example: data stored sequentially

Programming patterns

or... just a few ideas and suggestions

Two-stage initialization

  • Avoids passing everything through the constructor
  • Constructor creates an object, init method initializes it
  • Objects can be initialized several times
  • Objects can be allocated in-advance in a pool

class Brainbot extends Unit {

 

  private damage: number;

  private currentWeapon: WeaponType;

 

  constructor() {

    super(UnitType.BRAIN_BOT);

  }

 

  init(attributes: UnitAttribs) {

    // set default values

    this.damage = DEFAULT_DAMAGE_BRAINBOT;

    this.currentWeapon = WeaponType.LASERGUN;

    this.attributes = attributes;

  }

}

Separation of concerns

  • a common misuse is to handle complex events in one place
  • solution: send messages and let other parts of the game worry
  • in one place
  • if(asteroid.position.distance(rocket.position<= MIN_PROXIMITY) { // detect proximity

      rocket.runAnimation(ANIM_EXPLOSION); // react instantly and handle everything

      asteroid.runAnimation(ANIM_EXPLOSION);

      playSound(SOUND_EXPLOSION);

      asteroid.delete();

      rocket.delete();

    }

  • separated
  • let collisions = this.collisionSystem.checkProximity(allGameObjects);

    collisions.forEach(colliding => this.sendEvent(COLLISION_TRIGGERED, colliding));

    // rocket-handler.ts

    onCollisionTriggered(colliding) {

      this.destroy();

      this.sendEvent(ROCKET_DESTROYED);

    }

    // sound-component.ts

    onGameObjectDestroyed() {

      this.playSound(SOUND_EXPLOSION);

    }

ID generator

  • a simple way how to generate consecutive integers
  • Java (thread-safe)
  • public class Generator {

      private final static AtomicInteger counter = new AtomicInteger();

     

      public static int getId() {

         return counter.incrementAndGet();

      }

    }

  • TypeScript, using a generator
  • function* generateId() {

      let id = 0;

      while(true) {

        yield id;

        id++;

      }

    }

     

    let newId = generateId().next();

  • TypeScript, using a static variable
  • class Generator {

      static idCounter = 0;

     

      getId(): number {

        return Generator.idCounter++;

      }

    }

State

  • several meanings (state of the whole game, internal state of entities,...)
  • in this context, it is just a member of a set, determining what actions an object may execute
  • // stateless, the creature will jump each frame

    creatureUpdate(delta: number, absolute: number) {

      if(pressedKeys.has(KeyCode.UP)) {

        this.creature.jump();

      }

    }

     

    // a variable that makes certain actions possible only according to the current state

    creatureUpdate(delta: number, absolute: number) {

      if(pressedKeys.has(KeyCode.UP) && this.creature.state !== STATE_JUMPING) {

        this.creature.changeState(STATE_JUMPING);

        this.creature.jump();

      }

    }

Flags

  • bit array that stores binary properties of game objects
  • may be used for queries (e.g. find all DEAD objects)
  • similar to a state machine but behaves differently
  • if we maintain all flags within one single structure, we can search very fast

Example: Flag Table

Dirty Flag

  • marks changed objects
  • can be applied to various attributes (animation, physics, transformation)
  • you have to make sure to set the flag every time the state changes
  • you have to keep the previous derived data in memory

Cleaning

  • When the result is needed
    • Avoids doing recalculation if the result is never used
    • Game can freeze for expensive calculations
  • At well-defined checkpoints
    • less impact on user experience
    • you never know, when it happens
  • On the background
    • You can do more redundant work
    • race-condition may occur

Example: Atomic Game Engine markdirty

void Node::MarkDirty() {

  // a) whenever a node is marked dirty, all its children are marked dirty as well.

  // b) whenever a node is cleared from being dirty, all its parents must have been

  // cleared as well.

  if (this->dirty_return;

  this->dirty_ = true;

 

  // Notify listener components first, then mark child nodes

  for (auto i = this->listeners_.Begin(); i != this->listeners_.End();) {

      Component *= *i;

      if (c) {

          c->OnMarkedDirty(this);

          ++i;

      } else {

          *= this->listeners_.Back(); // listener expired -> erase from list

          this->listeners_.Pop();

      }

  }

  // Mark all children dirty

  for(auto child : children) {

      child->MarkDirty();

  }

}

 

 

Example: PIXI Container sort

  addChild(child) {

    // if the child has a parent then lets remove it as PixiJS objects can only exist in one place

    if (child.parent) {

      child.parent.removeChild(child);

    }

 

    child.parent = this;

    this.sortDirty = true;

    ...

    this.emit('childAdded', child, thisthis.children.length - 1);

    child.emit('added'this);

 

    return child;

}

  

updateTransform() {

  if (this.sortableChildren && this.sortDirty) {

    this.sortChildren();

  }

  ...

}

String hash

  • in C++, strings are expensive to work at runtime, strcmp has O(n) complexity
    • luckily, many scripting engines use string interning
  • game engines widely use string hash which maps a string onto a semi-unique integer
  • algorithms: djb2, sdbm, lose lose,...
  • example: sdbm

// hashing function

inline unsigned SDBMHash(unsigned hashunsigned char c

return c + (hash << 6+ (hash << 16- hash; }

 

unsigned calc(const char* strunsigned hash = 0) {

    while (*str) {

        // Perform the current hashing as case-insensitive

        char c = *str;

        hash = SDBMHash(hash, (unsigned char)tolower(c));

        ++str;

    }

    return hash;

}

Builder

  • slightly different from the builder defined by GoF
  • stores attributes needed to build a game object, can be used to build several objects
  • Aspect in Artemis framework, Prefab in Unity
  • class Builder {

      private _position: Vector;

      private _scale: Vector;

     

      position(pos: Vector) {

        this.position = pos;

        return this;

      }

     

      scale(scale: Vector) {

        this.scale = scale;

        return this;

      }

     

      build() {

        return new GameObject(this._positionthis._scale);

      }

    }

     

    new Builder().position(new Vector(1254)).scale(new Vector(21)).build();

Factory

  • Builder assembles an object, factory manages the assembling
  • Factory creates an object according to the parameters but with respect to the context
  • class UnitFactory {

     

      private pikemanBuilder: Builder// preconfigured to build pikemans

      private musketeerBuilder: Builder// preconfigured to build musketeers

      private archerBuilder: Builder// preconfigured to build archers

     

      public spawnPikeman(position: Vectorfaction: FactionType): GameObject {

        return this.pikeman.position(position).faction(faction).build();

      }

     

      public spawnMusketeer(position: Vectorfaction: FactionType): GameObject {

        return this.musketeerBuilder.position(position).faction(faction).build();

      }

     

      public spawnArcher(position: Vectorfaction: FactionType): GameObject {

        return this.archerBuilder.position(position).faction(faction).build();

      }

    }

Value provider

  • instead of passing a value, we pass a function pointer that returns this value
  • may generate new values upon every call or just return a value according to the context
  • class UnitFactory {

      private pikemanBuilder: Builder;

      private musketeerBuilder: Builder;

      private archerBuilder: Builder;

     

      public initBuilders() {

        // sprites are shared, weapons must be instantiated for each instance

        this.pikemanBuilder.sprite(Sprites.PIKEMAN).weapon(() => new Spear());

        this.musketeerBuilder.sprite(Sprites.Musketeer).weapon(() => new Musket());

        this.archerBuilder.sprite(Sprites.ARCHER).weapon(() => new Bow());

      }

     

      ...

    }

Flyweight

  • an object holds shared data to support large number of fine-grained objects
  • example: instanced rendering, geometry hashing, particle systems

Replay

  • allows to reproduce any state of a game at any time
  • much more complex than the save mechanism
  • all game entities must have a reproducible behavior (similar to multiplayer facility)
  • two main impediments: random functions and nondeterministic operations
  • Solution a)
    • store the state of all objects in the game - either on frame basis or at a fixed frequency
    • reproduce them by modifying all objects at each frame
  • Solution b)
    • if the game is completely message-driven, we can store all game messages
    • during the replay, forbid all components to send messages on their own
    • send queued messages one by one and let them be processed

Example: Doom DEMO file

  • Lump file (*.LMP)
  • Doom (1993) used fixed time-loop at a rate of 35 FPS (tic command)
  • the file contains ONLY keyboard inputs at each tick
  • the game plays the demo, bypassing input commands from the demo file
  • 13B header + 4B data for each tick ~ 140B/s

Case study: Tower defense

Tower defense

Goal

  • to defend a portal, obstructing the attackers by placing defensive structures along their path
  • the base must survive waves of multiple enemies

Common features

  • ability to build, repair and upgrade towers
  • enemies capable of traversing multiple paths
  • enemies capable of destroying towers

Rampart (1990)

Dungeon Defenders 2 (2017)

Tower defense games

Proposed features

  • Grid map
  • Indestructible towers
  • No obstructions on the path
  • No flying enemies
  • Active towers - fire projectiles
  • Passive towers - radiate energy

Map model

Data model

Entities

  • Level - aggregates all objects of the current level
  • Map - grid map
  • Tile - a map cell of a given type
  • Player - player that controls all towers
  • Wave - a single wave
  • Creep - spawned creep
  • CreepPrototype - keeps default attributes of each creep
  • Tower - tower placed on a map
  • TowerPrototype - keeps default attributes of each tower
  • Portal - end of the road that needs to be defended
  • Projectile - a tower projectile

Arcane Archer Cannon Crossbow Dark

Gremlin Goblin Imp Giant

Game attributes

Game components

Tower states

Game events

Lecture 5 Review

Programming patterns:
  • two-stage initialization - avoids passing everything through the constructor
  • state - a variable determining what actions an object may execute
  • flags - a bit array that stores binary properties of game objects
  • dirty flag - marks changed objectgs
  • builder - stores attributes needed to create a game object
  • factory - manages the way new objects are created
  • flyweight - holds shared data to support large number of objects
  • replay - allows to reproduce any state of the game at any time

Goodbye quote

I'm blind, not deafIllidan Stormrage, Warcraft