Javascript Game Foundations - State Management

Tue, Dec 10, 2013

Ten Essential Foundations of Javascript Game Development

  1. A Web Server and a Module Strategy
  2. Loading Assets
  3. The Game Loop
  4. Player Input
  5. Math
  6. DOM
  7. Rendering
  8. Sound
  9. State Management
  10. Juiciness

State Management

As our games get more complex, we look for ways to structure our projects to avoid the dreaded spaghetti code or the big ball of mud. In the first article we talked about breaking up our code into a sensible module structure, but that alone is not enough. We still have to be careful to avoid having too many dependencies between those modules/classes.

There are a number of patterns that we can use to improve our code architecture, and the two that we will primarily be concerned with in this article are:

A finite state machine provides events that transition between high level states, such as:

… and allows for code to be executed when those events fire:

… or, for more control, when a particular state is entered (or exited):

A publish/subscribe event system can enhance this architecture by allowing for additional custom events to occur at any time, e.g:

State vs State

Lets take a moment to clarify some terminology. Depending on the context, when we say game state we could mean one of 2 things:

This article is about the second case, managing the game mode and responding to events when it changes. It is during these events that the other kind of state, game values, can be cleared or set.

Finite State Machines

I would recommend using a 3rd party finite state machine library.

While I try to avoid large frameworks, I am in favor of finding small, discrete, libraries that do one thing, and do it well. And if you can’t find one then build one, which is what I did during an earlier game:

For a simple game, we might only need 2 states:

state: {
  initial: 'menu',
  events: [
    { name: 'play',    from: 'menu', to: 'game' },
    { name: 'abandon', from: 'game', to: 'menu' },
    { name: 'lose',    from: 'game', to: 'menu' }
  ]},

The javascript-state-machine library provides…

… and allows us to hook into the state machine events when a transition occurs:

onmenu: function() {
  // setup the menu
},

ongame: function() {
  // start a new game
},

onleavegame: function() {
  // save any highscores
},

onbeforeabandon: function() {
  // confirm "are you sure"
}

This kind of infrastructure allows our game to stay a clean, event driven application instead of devolving into a complex procedural set of if/then/else statements.

PubSub Events

During the course of a game, there can be many other events that can occur in addition to the high level FSM state transitions, examples include:

There might be many components within our game system that would like to react to these events:

It can be very useful to construct an event publish/subscribe mechanism to handle these custom events. If you are already using a 3rd party library such as jQuery you might be able to use their existing custom event framework. If you are using very modern browsers you might be able to piggy-back off the existing DOM event framework.

However, if none of these options are available, you can still implement a simple custom event framework yourself using a pub/sub pattern. There are a variety of ways to implement this (javascript is a very flexible language). One approach is to manage an array of callback methods to be triggered when an event is published.

Here is one possible implementation that can turn any target into a pub/sub broker:

PubSub = {

  enable: function(target) {

    var n, max;

    target.subscribe = function(event, callback) {
      this.subscribers = this.subscribers || {};
      this.subscribers[event] = this.subscribers[event] || [];
      this.subscribers[event].push(callback);
    },

    target.publish = function(event) {
      if (this.subscribers && this.subscribers[event]) {
        var subs = this.subscribers[event],
            args = [].slice.call(arguments, 1),
            n, max;
        for(n = 0, max = subs.length ; n < max ; n++)
          subs[n].apply(target, args);
      }
    }

  }

}

With this helper method, your can enable pub/sub on your main game object:

var EVENT =          // arbitray enum to define custom events
  PLAYER_HURT: 0,
  DOOR_OPEN:   1,
  ...
};

PubSub.enable(game);

… other objects can subscribe to the events they care about…

game.subscribe(EVENT.PLAYER_HURT, function(player) { gfx.sparks(); });
game.subscribe(EVENT.PLAYER_HURT, function(player) { sfx.grunt();  });
game.subscribe(EVENT.DOOR_OPEN,   function(door)   { sfx.creak();  });

… then, within your game logic, you can publish events:

  game.publish(EVENT.DOOR_OPEN, door);

  ...

  game.publish(EVENT.PLAYER_HURT, player);

… and eny existing subscribers will respond to it appropriately.

NOTE: A more robust implementation would probably be smarter about using Function.bind to allow callbacks to be called with an appropriate ’this’ context (see footnote)

State Management in Gauntlet

When taken together, these two patterns provide a powerful architecture to keep our game code clean and event driven.

For a game such as Gauntlet, the combination of a finite-state-machine to manage game states, and a simple publish-subscribe custom event mechanism allow much of the game logic to be declarative:

var cfg = {

  state: {
    initial: 'booting',
    events: [
      { name: 'ready',  from: 'booting',               to: 'menu'     }, // initial page loads images and sounds then transitions to 'menu'
      { name: 'start',  from: 'menu',                  to: 'starting' }, // start a new game from the menu
      { name: 'load',   from: ['starting', 'playing'], to: 'loading'  }, // start loading a new level (either to start a new game, or next level while playing)
      { name: 'play',   from: 'loading',               to: 'playing'  }, // play the level after loading it
      { name: 'help',   from: ['loading', 'playing'],  to: 'help'     }, // pause the game to show a help topic
      { name: 'resume', from: 'help',                  to: 'playing'  }, // resume playing after showing a help topic
      { name: 'lose',   from: 'playing',               to: 'lost'     }, // player died
      { name: 'quit',   from: 'playing',               to: 'lost'     }, // player quit
      { name: 'win',    from: 'playing',               to: 'won'      }, // player won
      { name: 'finish', from: ['won', 'lost'],         to: 'menu'     }  // back to menu
    ]
  },

  pubsub: [
    { event: EVENT.MONSTER_DEATH,      action: function(monster, by, nuke) { this.onMonsterDeath(monster, by, nuke);     } },
    { event: EVENT.GENERATOR_DEATH,    action: function(generator, by)     { this.onGeneratorDeath(generator, by);       } },
    { event: EVENT.DOOR_OPENING,       action: function(door, speed)       { this.onDoorOpening(door, speed);            } },
    { event: EVENT.DOOR_OPEN,          action: function(door)              { this.onDoorOpen(door);                      } },
    { event: EVENT.TREASURE_COLLECTED, action: function(treasure, player)  { this.onTreasureCollected(treasure, player); } },
    { event: EVENT.WEAPON_COLLIDE,     action: function(weapon, entity)    { this.onWeaponCollide(weapon, entity);       } },
    { event: EVENT.PLAYER_COLLIDE,     action: function(player, entity)    { this.onPlayerCollide(player, entity);       } },
    { event: EVENT.MONSTER_COLLIDE,    action: function(monster, entity)   { this.onMonsterCollide(monster, entity);     } },
    { event: EVENT.PLAYER_NUKE,        action: function(player)            { this.onPlayerNuke(player);                  } },
    { event: EVENT.PLAYER_FIRE,        action: function(player)            { this.onPlayerFire(player);                  } },
    { event: EVENT.MONSTER_FIRE,       action: function(monster)           { this.onMonsterFire(monster);                } },
    { event: EVENT.PLAYER_EXITING,     action: function(player, exit)      { this.onPlayerExiting(player, exit);         } },
    { event: EVENT.PLAYER_EXIT,        action: function(player)            { this.onPlayerExit(player);                  } },
    { event: EVENT.FX_FINISHED,        action: function(fx)                { this.onFxFinished(fx);                      } },
    { event: EVENT.PLAYER_DEATH,       action: function(player)            { this.onPlayerDeath(player);                 } }
  ],

  ...
}

If this configuration is used to enable FSM and Pub/Sub features on our primary game object then it will be able to easily handle state transitions…

  onready: function() {
    ...
  },

  onmenu: function() {
    ...
  },

  onstart: function() {
    ...
  },

… as well as custom events …

  onPlayerDeath: function(player) {
    ...
  },

  onDoorOpen: function(door) {
    ...
  },

Keeping the code structure manageable, flexible, and open to future extensions.

A Note on Function Binding

When adding patterns such as a finite-state-machine, or a pub-sub event system we will find ourselves having to provide functions as callback methods. Since we are likely to want to provide object instance methods as the target of these callbacks it becomes very important to understand how those callback methods are called with the correct object context.

Luckily, there are a multitude of references available to learn about javascript function invocation:

If you are not already familiar with these concepts then I would highly recommend that you take some time out to study these articles until you have a good conceptual understanding of Javascripts (quirky) function invocation patterns.