Please note: this blog has been migrated to a new location at https://jakesgordon.com. All new writing will be published over there, existing content has been left here for reference, but will no longer be updated (as of Nov 2023)

Boulderdash Rendering

Thu, Oct 27, 2011

Previously, I released an experimental javascript Boulderdash game and promised to write up some articles about how the game works:


The topics I wanted to cover include:

So, lets continue on by talking about rendering the game…

Decoupling the game logic from the rendering code

It is general good practice to try to minimize coupling between components of a software system.

For Boulderdash we instantiated 2 primary components

var game   = new Game(),           // provides the game logic
var render = new Render(game);     // provides the rendering engine

The Render object needs to have quite an intimate knowledge of the Game object if it is going to render its state to the HTML5 <canvas>, but it would be nice if this was a one way dependency - there should be no reason the Game object needs to have any knowledge of how it is rendered.

In order to avoid the Game having to call into the Render object when its state changes we can implement a very simplified publish/subscribe pattern:

Game.prototype = {

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

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

  // ...
}

Now, for example, when the Game decides to increase the score it can publish this event:

increaseScore: function(n) {
  this.score += n;
  this.publish('score', this.score);
},

… and the Render object can subscribe to that and trigger a re-render of the scoreline in the header:

function Render(game) {
  // ...
  game.subscribe('score', this.invalidateScore, this);
}

This is a very useful pattern that is simple to achieve with Javascript and means our Game object can stay completely unaware of our…

Render Object

Our Render object constructor simply subscribes to various Game events, and initializes the rest of its state when the main game loop asks it to reset

function Render(game) {
  game.subscribe('level', this.invalidateCave,  this);
  game.subscribe('flash', this.invalidateCave,  this);
  game.subscribe('cell',  this.invalidateCell,  this);
  game.subscribe('score', this.invalidateScore, this);
  game.subscribe('timer', this.invalidateScore, this);
}

Render.prototype = {

  reset: function(sprites) {
    this.canvas   = document.getElementById('canvas');
    this.ctx      = this.canvas.getContext('2d');
    this.sprites  = sprites;
    this.fps      = 30;
    this.step     = 1/this.fps;
    this.frame    = 0;
  },

  // ... etc
}

Optimized Rendering

Although Boulderdash is not a graphically complex game, we should still endeavour to minimize the amount of rendering we do in order to maintain acceptable frame rates in older browsers.

We can do this by only re-rendering changed elements, using a fairly common invalidate pattern to record the need to re-render either:

invalid: { cave: true, score: true },
invalidateCell:  function(cell) { cell.invalid       = true;  },
invalidateCave:  function()     { this.invalid.cave  = true;  },
invalidateScore: function()     { this.invalid.score = true;  },
validateCell:    function(cell) { cell.invalid       = false; },
validateCave:    function()     { this.invalid.cave  = false; },
validateScore:   function()     { this.invalid.score = false; },

Then in our render.update function (below) we can use this information to only re-render the items that have changed.

Rendering the Cave

The render.update function called by the game loop becomes very simple,

update: function() {
  this.frame++;
  game.eachCell(this.cell, this);
  this.validateCave();
},

cell: function(cell) {
  var sprite = cell.object.sprite;
  if (this.invalid.cave || cell.invalid || (sprite.f > 1)) {
    this.sprite(sprite, cell);
    this.validateCell(cell);
  }
},

Rendering a Sprite

Rendering a sprite is a simple <canvas> drawImage function call. The tricky part is getting the offsets correct and animating the sprite at the correct frame rate (if it has multiple frames)

sprite: function(sprite, cell) {

  // calculate the correct animation frame (if any) based on desired sprite.fps
  var f = sprite.f ? (Math.floor((sprite.fps/this.fps) * this.frame) % sprite.f) : 0;

  // draw the sprite frame
  this.ctx.drawImage(
    this.sprites,                               // source image
    (sprite.x+f) * 32, sprite.y * 32,           // source x,y
    32, 32,                                     // source w,h
    cell.p.x * this.dx, (1+cell.p.y) * this.dy, // destination x,y
    this.dx, this.dy);                          // destination w,h

},

Some points to note:

Time permitting, we should be recreating the source sprites at load-time and scaling them to the current target dimension just once, then using this dynamic spritesheet as the source of our drawImage calls.

Summary

The game rendering is surprisingly simple - that comes from the power of sprite sheets and well defined data - rendering simply becomes iterating over a 2 dimensional grid drawing the correct sprite in the correct cell. Of course it also helps that Boulderdash is a simple game!

There is obviously a few details left out here such as how we render.score, but you can view the source code for the gritty details.

Now we are almost complete. We have our game loop, our game logic, and now our game rendering, the only topic left to talk about next time is decoding the c64 cave data

More information…

Game specs…