Javascript Gauntlet - Foundations
Sun, May 5, 2013Before 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:
- stats.js - the fps counter
- sizzle.js - a CSS selector engine for easy DOM selection
- animator.js - a small, fast, animating library for easing/tweening
- audio-fx.js - a small library that wraps HTML5 audio quirks
- state-machine.js - a finite state machine
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 prerequisitesgame.js
- a simple game loop and some helper methodsdom.js
- minimal jQuery-like $() DOM helperkey.js
- a config based keyboard event mapmath.js
- additional math helper methodspubsub.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 rungame.update
in a while loop to ensure the game ‘catches up’ without missing any updates, but we don’t bother doing this forrender.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…
Related Links
- play the game
- view the source code
- read more about game foundations
- read more about game level maps
- read more about game entities
- read more about game collision detection
- read more about game logic