Building a Simple HTML5 Canvas Game

This tutorial walks through creating a basic space shooter using Canvas and HTML5, assuming an intermediate grasp of JavaScript. You can try the finished demo or download the complete source to follow along.

Setting Up the Canvas and Game Loop

First, create a canvas element with jQuery. Then, set up a game loop using setInterval() so the game updates and redraws faster than the eye can perceive. For now, leave the update and draw methods empty.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');
var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);
function update() { ... }
function draw() { ... }

First Draw and Motion

Populate the draw method to render text on screen. Because the game loop is already running, we can make the text move easily:

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}
var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Notice the text leaves trails behind it. This happens because the canvas isn't cleared between frames. Add screen clearing at the start of draw:

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Creating the Player Object

Hold the player's data and drawing responsibility in a plain object literal. For now, a simple colored rectangle represents the player:

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

The game's draw routine clears the canvas and then draws the player:

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Handling Keyboard Input

Use the jQuery Hotkeys plugin to avoid cross-browser keyCode and charCode headaches. Bind key events directly:

$(document).bind("keydown", "left", function() { ... });

JavaScript keyboard events are event-driven with no built-in key-state query. Relying only on events makes gameplay vary because keyboard repeat rates differ across systems and aren't tied to the game loop's timing. A 16-line wrapper, key_status.js, provides state querying via checks like keydown.left.

With key-state access, the player's movement updates cleanly:

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Clamp the player's position to the screen bounds and increase speed:

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Adding Bullets

Store projectiles in a collection, then build a constructor for bullet instances:

var playerBullets = [];
function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Spawning a bullet on fire adds it to the collection:

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Update each bullet inside the game's step function. Filter the collection to remove inactive bullets, which also cleans up after collisions:

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

Draw them in the render pass:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Enemies follow the same pattern as bullets:

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Images and Sound Utilities

Real sprites are nicer than boxes. A small utility class makes image loading and drawing painless:

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Sound follows suit with a simplified helper API:

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

Audio is the most fragile part of the stack; effects can drop out or crash the tab, so test carefully.

Collision Detection

Everything flies around independently, so add rectangular collision detection to trigger explosions:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Check two collision pairs: player bullets against enemy ships, and the player against enemy ships. A dedicated method runs these checks from the update loop:

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Flag hit objects for removal and spawn an explosion effect:

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Going Further

These fundamentals—canvas setup, a fixed-tick game loop, input state, object collections, and collision handling—are the core of countless HTML5 games. A good reference is the HTML5 Canvas Cheat Sheet, and this list of HTML5 game engines is worth a look when you scale up.