Javascript Boulderdash

Tue, Oct 25, 2011

Still on the subject of exploring HTML5 games, the next game I chose to implement was a <canvas> version of the c64 classic Boulderdash.

Boulderdash

Anyone who owned a c64 back in the 80’s will likely have fond memories of the Boulderdash series of games. In fact they were so popular that there are a multitude of fan sites and remakes, even the original publisher First Star is still making commercial versions for todays mobile platforms. I highly recommend you go purchase one for your favorite device.

I wanted to take a stab at making an HTML5 version, and found a huge amount of information on Martijn’s and Arno’s fan sites… including cave data and an actual specification!

This wealth of information allowed me to put together a playable version of Boulderdash in just a couple of weekends.

Room for Improvement

This is just a programming experiment of the Boulderdash game mechanics. It’s not intended to be a shippable game and therefore does not include a menu system or sound effects, but it does include all 20 levels from the original Boulderdash I game.

If you were to polish this game further you would want to add things like:

Implementation Details

The game consists of a single static index.html page, a pretty minimal boulderdash.css stylesheet and two javascript files:

There are no 3rd party libraries or server side components needed, simply use your web server of choice to serve up the static files.

The game can be broken down into a game loop plus 3 main areas, and I will write each of those up as a separate article over the next couple of days, so check back here shortly for links to the following articles:

But to get us started lets talk about…

The Game Loop

The fundamental Boulderdash algorithm iterates through the cave, updating every object once in each game frame. The original c64 version of Boulderdash controls its difficulty by adjusting the game logic frame rate - meaning - the faster the frame rate, the more times each object is updated and therefore, the faster Rockford moves or the boulders fall.

On difficulty level 1 (the easiest) this game frame rate is approximately 10fps.

The game frame rate is independent of the rendering frame rate. In fact when we talk about rendering we will find we have a number of sprites that must be animated at different frame rates, some as slow as 8fps, others as fast as 30fps, in order to animate at the correct speed.

That sounds kind of complex, but is actually quite simple in practice, we just need to maintain a game loop whose rate is independent of our rendering loop. This is generally considered good practice for games anyway to avoid variations in hardware rendering performance affecting our underlying game physics behavior.

Lets start by assuming we have 2 objects representing our game logic and our renderer:

  var game   = new Game(),
  var render = new Render(game);

Don’t worry! These will be examined in excruciating detail in the upcoming articles, but for now, we only need to know that they each define an update() method and 2 constants: fps - the desired frame rate and step = 1/fps.

We will need a way to measure the current time. Until we get a high resolution javascript timer (don’t hold your breath) we must do it the old fashioned way:

  function timestamp() {
    return new Date().getTime();
  };

We will run our main loop as fast as the browser allows (using requestAnimationFrame), but we will maintain independent game and render loops using the following 5 variables:

We accumulate time until we reach the step required to trigger an update:

var now, last = timestamp(), dt = 0, gdt = 0, rdt = 0;
function frame() {
  now = timestamp();
  dt  = Math.min(1, (now - last) / 1000); // (see NOTE)
  gdt = gdt + dt;
  while (gdt > game.step) {
    gdt = gdt - game.step;
    game.update();
  }
  rdt = rdt + dt;
  if (rdt > render.step) {
    rdt = rdt - render.step;
    render.update();
  }
  last = now;
  requestAnimationFrame(frame);
}

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.

Loading Assets

Once last thing we must deal with before actually starting the loop is to be sure our external image resources have loaded and are ready for rendering to the canvas.

For Boulderdash, all of our sprites are contained on a single sprites.png image, so we need a very simple function for creating our single image and waiting until it has finished loading:

function load(cb) {
  var sprites = document.createElement('img');
  sprites.addEventListener('load', function() { cb(sprites); } , false);
  sprites.src = 'images/sprites.png';
};

With this in place, starting the game loop can become as simple as:

load(function(sprites) {
  render.reset(sprites); // reset the canvas renderer with the loaded sprites <IMG>
  game.reset();          // reset the game
  frame();               //  ... and start the first frame !
});

Summary

Encapsulating the above code into a single run() function gives us our final game loop:

  var game   = new Game(),
      render = new Render(game);

  function run() {

    var now, last = timestamp(), dt = 0, gdt = 0, rdt = 0;
    function frame() {
      now = timestamp();
      dt  = Math.min(1, (now - last) / 1000);
      gdt = gdt + dt;
      while (gdt > game.step) {
        gdt = gdt - game.step;
        game.update();
      }
      rdt = rdt + dt;
      if (rdt > render.step) {
        rdt = rdt - render.step;
        render.update();
      }
      stats.update();
      last = now;
      requestAnimationFrame(frame, render.canvas);
    }

    load(function(sprites) {
      render.reset(sprites); // reset the canvas renderer with the loaded sprites <IMG>
      game.reset();          // reset the game
      frame();               //  ... and start the first frame !
    });

  };

Next…

Now that we have our game loop, I can go into detail about our game object that implements the Boulderdash game logic, followed by a description of the render object that implements rendering to the HTML5 <canvas>. Then finally I’ll talk about where we get the Boulderdash cave data from that defines each of our 20 levels.

More information…

Game specs…