Javascript Gauntlet - Foundations

Sun, May 5, 2013

Before I embark on trying to make a multi-player Gauntlet-style game, I wanted to spend some time writing up how the single player version works first, while it is still relatively fresh in my mind.

This game is a little bit more complex than my previous games, and so requires a little more structure, and in turn, a little more explanation for how-it-works.

This first article will try to cover a lot of ground to establish the foundation upon which the game sits, including:

  • Code Structure
  • Dependencies
  • The Game Engine
  • The Game Loop
  • State Machines and Events
  • Configuration
  • Gauntlet Classes

Most of the topics for this article are all quite general topics that can apply to any javascript game, many of which have been touched on in my previous articles, and will quite likely resurface in future games as I build up more and more of a re-usable ‘engine’.

Subsequent articles will be more Gauntlet-specific, and cover topics such as maps, entities, collision detection, and game logic…

… but, for now, lets go ahead and dive into the game’s foundations

Code Structure

The javascript code for this game is broken into 3 parts:

  • vendor.js - 3rd party dependencies
  • game.js - general purpose game engine code, including game loop
  • gauntlet.js - gauntlet-specific game code

Both vendor.js and game.js are unified versions of multiple, smaller files. A small Ruby gem called unified-assets is used to provide a command line interface for re-generating these files whenever the underlying source files are modified.

NOTE: a more modern version of this idea would be to use tools like Sprockets, or RequireJS

Dependencies

I’m not a big fan of large, controlling, frameworks. I much prefer to work with small (micro-) libraries that do one job and do it well. In the unified vendor.js file you will find the 3rd party dependencies used for this project:

These are fairly self-explanatory. While this game is mostly canvas-based. The scoreboard is DOM-based and uses sizzle.js to locate DOM elements, and animator.js to fade them in & out. The audio-fx.js library is a little library I wrote when making my snakes game to hide some of the quirkiness of cross-browser HTML5, and the state-machine.js library is the finite state machine I wrote when making my breakout game (I will talk more about state machines later in this article)

Game Engine

The base game library consists of the generic code that is not Gauntlet specific, and might be reused in other games. This is code that started off life as the base for a Pong game, evolved through Breakout and Snakes and I’m sure will evolve further as I make future HTML5 games.

It is broken down into individual source modules:

  • base.js - javascript prerequisites
  • game.js - a simple game loop and some helper methods
  • dom.js - minimal jQuery-like $() DOM helper
  • key.js - a config based keyboard event map
  • math.js - additional math helper methods
  • pubsub.js - a simple publish-subscribe model

NOTE: at this point, the code is still evolving as it is copied from game to game and there is no single ‘game library’ that is shared across games. At some point I hope that will change when this code matures into a stable re-usable game library.

To use the engine, we define a game object implementing the following methods:

var Gauntlet = {

  run: function(runner) {
    // game setup code goes here
  },

  update: function(frame) {
    // game update logic goes here
  },

  draw: function(ctx, frame) {
    // game rendering logic goes here
  },

}

Then get the game going with the Game.run() static method:

  Game.run(Gauntlet);

Game Loop

The base game engine started back in my first javascript game, pong. For that game, the loop was very simple. Using requestAnimationFrame to run a 60fps loop and, for each iteration:

  • call game.update(dt) - providing dt timer interval since last frame
  • call game.draw(ctx) - providing canvas context for drawing on

This worked well for simple bouncing ball games, but requestAnimationFrame (or any browser implementation of a game loop) cannot guarantee a constant 60fps loop, and in more complex games it becomes important to know how long a single frame is going to be.

So when I moved on to make a boulderdash game I switched to a more stable fixed timestep loop where the game frame rate is independent of the rendering frame rate.

The idea being, the Game.Runner can .start() a game loop that will run as fast as the browser allows (using requestAnimationFrame) but we will maintain an independent update loop, by accumulating dt until enough time has passed to trigger one (or more) game update() calls:

initialize: function(game, cfg) {
  this.game = game;
  this.fps   = cfg.fps || 60;  // requested fixed frame rate
  this.dstep = 1.0 / cfg.fps;  // fixed timestep (in seconds)
  this.frame = 0;              // current frame counter
  ...
},

start: function() {

  var current, last = timestamp(), dt = 0.0;

  var step = function() {
    current = timestamp();
    dt = dt + Math.min(1, (current - last)/1000.0);  // MAX of 1s to avoid huge delta's when requestAnimationFrame puts us in the background
    while(dt >= this.dstep) {
      this.game.update(this.frame++);      // game update
      dt = dt - this.dstep;
    }
    this.game.draw(this.ctx, this.frame);  // game render
    last = current;
    requestAnimationFrame(step);
  }
  step();

},

NOTE: this is a simplified version of the real code for clarity.

NOTE: Since requestAnimationFrame will go idle when the browser is not visible, it is possible for dt to be very large, therefore we limit the actual dt to automatically ‘pause’ the loop when this happens. Interesting to note is that when we do have a large dt we make sure to run game.update in a while loop to ensure the game ‘catches up’ without missing any updates, but we don’t bother doing this for render.update where simply rendering the most recent state once is enough to catch up.

State Machines and Events

In order to keep game logic from becoming spaghetti code, a little decoupling is in order. There are 2 key patterns that I find greatly simplify my game logic:

  • state machine - manage states and the transitions between them
  • events - manage events that occur in the game

For implementing a state machine, I use my own javascript-state-machine library that I introduced back in my breakout game.

For Gauntlet, the high level FSM configuration manages switching between:

booting > menu > starting > loading > playing > help > won/lost > menu

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
  ]
},


For implementing custom events, I have hand-rolled a very simple publish/subscribe pattern in the js/game/pubsub.js module.

For Gauntlet, the high level PubSub configuration is:

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);                 } }
],

This allows us to concentrate our game logic within simple event handlers:

  // FSM event handlers:

  onload: function() { ... },
  onplay: function() { ... },
  onwin:  function() { ... },
  ...

  // PUBSUB event handlers:

  onPlayerCollide: function(player, entity)       { ... },
  onPlayerFire: function(player)                  { ... },
  onTreasureCollected: function(treasure, player) { ... },
  ...

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.

NOTE: I plan to unify these 2 patterns in a future release of the javascript-state-machine library.

Configuration

Much of the game is data-driven, either via the constants defined at the top of gauntlet.js, or via the cfg object.

The constants are generally ‘magic numbers’ (e.g. sprite indices) or values that can be tweaked to affect gameplay, such as how fast the player can move, or how much damage it takes to kill a monster. We will examine these constants in much more detail in future articles when we talk about the game maps, entities, and collision detection.

The cfg object contains configuration used by various game components such as the finite state machine, the pubsub handler, the resource loader, the keyboard handler, etc:

var cfg = {
  runner:  { ... } // custom game runner state (e.g. frame rate, show stats, etc)
  state:   { ... } // fsm configuration (described previously)
  pubsub:  [ ... ] // pubsub configuration (described previously)
  images:  [ ... ] // array of images to load during booting (background and entity sprites)
  sounds:  [ ... ] // array of sounds to load during booting (music and sound effects)
  levels:  [ ... ] // array of level definitions - name, music, score, floor/wall colors, etc
  keys:    [ ... ] // array of key handler configuration (used by base game engine key.js module)
}

Gauntlet Classes

Pushing the generic code down into a base game library allows the Gauntlet-specific code to stay tidy, and fit into a single js\gauntlet.js file! We can use the javascript module pattern to create a closure over some private implementation details and return our game object at the very end.

Gauntlet = function() {

  var cfg = {
    // declarative configuration (described previously)
  };

  var game = { }; // define the core (singleton) game class - also an FSM and PubSub model

  var Map        = Class.create({ ... }); // manage the map for each level
  var Monster    = Class.create({ ... }); // manage the monster entities
  var Generator  = Class.create({ ... }); // manage the monster generator entities
  var Weapon     = Class.create({ ... }); // manage the weapon entities
  var Treasure   = Class.create({ ... }); // manage the treasure entities
  var Door       = Class.create({ ... }); // manage the door entities
  var Exit       = Class.create({ ... }); // manage the exit entities
  var Fx         = Class.create({ ... }); // manage the fx (explosions) entities
  var Player     = Class.create({ ... }); // manage the player entities
  var Scoreboard = Class.create({ ... }); // manage the game scoreboard
  var Viewport   = Class.create({ ... }); // manage the game viewport (the part of the map currently visible)
  var Render     = Class.create({ ... }); // the graphics rendering methods
  var Sounds     = Class.create({ ... }); // the audio playing methods

  return game;

};

Next Time…

Phew, we just whizzed through a lot of game foundations:

  • Code Structure - vendor.js, game.js, and gauntlet.js
  • Dependencies - 3rd party libraries
  • The Game Engine - the base game engine
  • The Game Loop - the fixed timestep game loop
  • State Machines and Events - patterns for FSM & PubSub
  • Configuration - constants, gameplay values and component configuration
  • Gauntlet Classes - the high level game classes

Much of this is general purpose code that can apply to any javascript game, and so far, not very Gauntlet-like…

… but never fear! Subsequent articles will be much more Gauntlet-specific, and cover topics such as maps, entities, collision detection, and game logic…