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)

Javascript Game Foundations - Rendering

Sun, Dec 8, 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

Rendering

It’s hard to imagine a video game with no visuals. It might be an interesting challenge to create an audio-only game, but in most games we’re going to need to draw stuff, and for browser based javascript games we are going to draw on an HTML5 <canvas> element.

Happily, the canvas element is supported in all modern browsers:

Canvas Primitives

The <canvas> drawing primitives are fairly self explanatory.

Start off with a <canvas> element in our html page…

<canvas id='tutorial' width='300' height='300'></canvas>

Obtain a reference to the rendering context in our javascript…

var canvas = document.getElementById('tutorial'),
    ctx    = canvas.getContext('2d');

Set stroke and fill styles…

ctx.fillStyle   = '#FF0000';  // red
ctx.strokeStyle = '#000000';  // black

Draw a Rectangle…

ctx.fillRect(x, y, width, height);
ctx.strokeRect(x, y, width, height);

Draw an Image…

ctx.drawImage(img, x, y, width, height);

Draw a Line…

ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.stroke();

Fill a Path…

ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.closePath();
ctx.fill();

Draw a Circle…

ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle, clockwise);
ctx.closePath();
ctx.fill();

Draw Text…

ctx.font = "20pt Arial";
ctx.fillText("Hello World", x, y);

… and so on.

Performance

On most modern desktop browsers, the <canvas> element is hardware accelerated by the GPU, making the performance more than adequate for simple games. We can render hundreds, possibly thousands of sprite images onto a canvas and still hit 60fps.

However, benchmarks are typically skewed and misleading. We should always measure the performance of our actual game running under real-world conditions. Start with a simple embedded FPSMeter so that we can keep an eye on performance during development. Once we identify a problem with a drop in the frame rate we can turn to more detailed analysis.

On Mobile browsers it’s a completely different story. The performance of mobile browsers will vary dramatically from device to device. It can be a very complicated battle to make our game perform well on mobile devices. We should choose our target audience and platform wisely. Simple games will likely perform just fine, but more complex action-oriented games might be better off targeting native mobile platforms instead of HTML5.

Cached Rendering

As always, the fastest rendering is that which is not performed. If we can avoid rendering, we should avoid it. There is no need to re-render a score if it hasn’t changed, or the background if it hasn’t moved, or entities that are idle.

We can make caching possible by rendering components to an off-screen canvas, then using a simple, and fast, ctx.drawImage call to render that component. We need only invalidate the off-screen canvas if the state of the component changes.

Here is a helper method to render to an off-screen canvas:

function renderToCanvas(width, height, render, canvas) {
  canvas = canvas || createCanvas(width, height, canvas);
  render(canvas.getContext('2d'));
  return canvas;
}

function createCanvas(width, height) {
  var canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  return canvas;
}

Given this helper method, our component can maintain its own off-screen render cache and can invalidate it whenever the state changes. A (superficial) example of this is shown below:

var x, y, w, h,       // component position and size
    invalid = true,   // component requires redrawing ?
    cache   = null;   // cached off-screen canvas

function render(ctx) {
  if (invalid) {
    cache = renderToCanvas(w, h, renderForReal, cache);
    invalid = false;
  }
  ctx.drawImage(cache, x, y);
}

function renderForReal(ctx) {
  ctx.fillStyle   = '#FF0000';
  ctx.strokeStyle = '#00FF00';
  ctx.beginPath();
  ...
}

function invalidate() {
  invalid = true;   // call this whenever the component state changes
}

More information about this technique can be found at kaioa.com

Rendering a Game

Beyond the primitives, what we render is going to be entirely dependent on our game requirements. Is it a simple 2-d platformer? A grid-based puzzle game? A pseudo-3d racing game? Do we need particle effects? parallax backgrounds ?

If you need a few hints to get started, you can read more details about how I’ve rendered some simple javascript games in the past: