Rendering Performance in Breakout

Sun, Jun 12, 2011

My previous pong game was simple enough that the entire view could be redrawn 60 frames per second. That can be pretty wasteful if only a small part of the view is actually changing, but it works for simple games.

For Breakout, using the native <canvas> drawing API to render the court, the score, the paddle, the ball and each individual brick starts to approach the limit of what you can render and still maintain 60 frames per second.

For 60fps, you need to be able to update and render the scene in less than 16.67ms (100060). If you are using setTimeout or setInterval for your game loop you also have to take into account the coarse resolution of these timers (here and here), typically at least 4ms. So you really need to keep your game loop under 10ms.

Building breakout in the simplest way, by rendering everything in every frame, was pushing this limit. The update() method was always fast (1ms), but depending on the browser and the hardware the draw() method could take up to 15ms, causing the frame rate to dip below 60fps.

Cached Rendering

Performance can be significantly improved by rendering component pieces offscreen and simply blitting them to the canvas with drawImage() at 60fps, then only re-render the offscreen image when necessary, e.g. when a brick is removed, or the score changes.

You can find a great description of offscreen canvas rendering at kaioa.com

This is fairly simple to setup with a helper method that creates an offscreen canvas and calls a render function to draw on it.

  renderToCanvas: function(width, height, render) {
    canvas = document.createElement('canvas');
    canvas.width  = width;
    canvas.height = height;
    render(canvas.getContext('2d'));
    return canvas;
  },

Using this helper method, we can move the actual drawing code into a new method called render() and then change the game loop draw() method to call renderToCanvas only when some internal state has changed, otherwise it simply blits the offscreen canvas to the screen:

  draw: function(ctx) {
    if (this.rerender) {
      this.canvas = Game.renderToCanvas(this.game.width, this.game.height, this.render.bind(this));
      this.rerender = false;
    }
    ctx.drawImage(this.canvas, 0, 0);
  },

  render: function(ctx) {
    var n, brick;
    for(n = 0 ; n < this.numbricks ; n++) {
      brick = this.bricks[n];
      if (!brick.hit) {
        ctx.fillStyle = brick.color;
        ctx.fillRect(brick.x, brick.y, brick.w, brick.h); 
        ctx.strokeRect(brick.x, brick.y, brick.w, brick.h);
      }
    }
  },

  hit: function(brick) {
    brick.hit = true;
    this.rerender = true;
  },

If we follow this pattern when rendering anything that changes infrequently, such as the score, the number of lives, the court, the bricks… even the paddle. That leaves only the ball itself that really gets re-rendered at 60fps.

In this way, our draw() method now only takes 1 or 2ms, keeping our frame rate locked at 60fps.

You can find the game here and the code is here