Build a vertical scrolling shooter game with HTML5 canvas – Part 3

February 20th, 2011

If you’ve been following along, you’ll know that in the first two parts, we built a shooter game using HTML5 canvas that has a ship that we can control and shoot lasers from and enemies that move. Now we’re going to check to see if a laser hits an enemy and then remove that enemy from the game. If you’re coming from building games using Flash and ActionScript 3.0, the first thing you’re going to miss is hitTestObject. There’s nothing like this in JavaScript and the canvas API, so we’ll have to set up the hit test ourselves.

Adobe recognized that people were using Flash to build games and made things easier by introducing hitTestObject that allowed you to just write something like this:

1
laser.hitTestObject(enemy)

That would check to see if the laser hit the enemy ship for you. Unfortunately, since the canvas API is still pretty new, we don’t have anything like that yet, so we’ll have to build our own hit test function. What we’ll need is to set up two loops, one for the lasers and one for the enemy ships. Then we check the position of each laser against each enemy ship and if a laser’s x position is greater than an enemy ship’s x position and the laser’s x position is less than the enemy’s x position plus it’s width, as well see if the laser’s y position is less than the enemy’s y position plus it’s height. If all of those are true then it means that the laser has hit the enemy ship.

Here’s that all translated into JavaScript:

1
2
3
4
5
6
7
8
9
function hitTest() {
  for (var i = 0; i < lasers.length; i++) {
    for (var j = 0; j < enemies.length; j++) {
      if (lasers[i][1] <= (enemies[j][1] + enemies[j][3]) && lasers[i][0] >= enemies[j][0] && lasers[i][0] <= (enemies[j][0] + enemies[j][2])) {
       
      }
    }
  }
}

We also need to call hitTest, so update the gameLoop function to this:

1
2
3
4
5
6
7
8
9
function gameLoop() {
  clearCanvas();
  hitTest();
  moveEnemies();
  moveLaser();
  drawEnemies();
  drawShip();
  drawLaser();
}

Right now, this function will check to see if the laser has hit the enemy ship but it won’t actually do anything when it does. Of course, we want to remove both the laser and the ship. You would think we could just do something like this:

1
2
3
4
5
6
7
8
9
10
function hitTest() {
  for (var i = 0; i < lasers.length; i++) {
    for (var j = 0; j < enemies.length; j++) {
      if (lasers[i][1] <= (enemies[j][1] + enemies[j][3]) && lasers[i][0] >= enemies[j][0] && lasers[i][0] <= (enemies[j][0] + enemies[j][2])) {
        lasers.splice(i, 1);
        enemies.splice(j, 1);
      }
    }
  }
}

This will remove both, but it will also give us an error. The problem is that for every time we run the loop for the lasers, we run the loop for the enemies for as many times as the length of the enemies array. So if our laser hits something before the enemies loop ends, it will still be checking to see if that laser has hit anything even though it’s already been removed. We can fix this by just using a boolean. If the laser hits, then remove the laser is true. And then we can move the lasers.splice outside of the enemies loop and we won’t get any errors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hitTest() {
  var remove = false;
  for (var i = 0; i < lasers.length; i++) {
    for (var j = 0; j < enemies.length; j++) {
      if (lasers[i][1] <= (enemies[j][1] + enemies[j][3]) && lasers[i][0] >= enemies[j][0] && lasers[i][0] <= (enemies[j][0] + enemies[j][2])) {
        remove = true;
        enemies.splice(j, 1);
      }
    }
    if (remove == true) {
      lasers.splice(i, 1);
      remove = false;
    }
  }
}

We’ve just added a variable called remove, it’s declared in the function because we don’t need it anywhere else in the program. We set it to false and then when a laser hits an enemy ship, we change remove to true and then outside the enemy loop but inside the laser loop, we have an if statement checking if remove is true and if it is, we remove that laser and then set remove back to false.

Now both the laser and enemy ship will be removed. But since we only have five enemy ships, this will be a pretty quick game. So we’ll just add one line to our hitTest function and we can set up the game to have a continuous stream of enemies. Update the hitTest function to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hitTest() {
  var remove = false;
  for (var i = 0; i < lasers.length; i++) {
    for (var j = 0; j < enemies.length; j++) {
      if (lasers[i][1] <= (enemies[j][1] + enemies[j][3]) && lasers[i][0] >= enemies[j][0] && lasers[i][0] <= (enemies[j][0] + enemies[j][2])) {
        remove = true;
        enemies.splice(j, 1);
        enemies.push([(Math.random() * 500) + 50, -45, enemy_w, enemy_h, speed]);
      }
    }
    if (remove == true) {
      lasers.splice(i, 1);
      remove = false;
    }
  }
}

You can check out the game so far here. Right now, our ship is invincible, so we’ll add another hit test to see if our ship has hit an enemy and we’ll also add scoring. Here’s the complete code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>HTML5 Canvas Shooter Game Tutorial Part 3</title>
<style>
body {
  padding:0;
  margin:0;
  background:#666;
}
canvas {
  display:block;
  margin:30px auto 0;
  border:1px dashed #ccc;
  background:#000;
}
</style>
<script>
var canvas,
    ctx,
    width = 600,
    height = 600,
                enemy,
    enemyTotal = 5,
    enemies = [],
    enemy_x = 50,
    enemy_y = -45,
    enemy_w = 50,
    enemy_h = 50,
    speed = 3,
    rightKey = false,
    leftKey = false,
    upKey = false,
    downKey = false,
                ship,
    ship_x = (width / 2) - 25, ship_y = height - 75, ship_w = 50, ship_h = 50,
                laserTotal = 2,
    lasers = [];

for (var i = 0; i < enemyTotal; i++) {
 enemies.push([enemy_x, enemy_y, enemy_w, enemy_h, speed]);
 enemy_x += enemy_w + 60;
}

function clearCanvas() {
 ctx.clearRect(0,0,width,height);
}

function drawEnemies() {
 for (var i = 0; i < enemies.length; i++) {
                ctx.drawImage(enemy, enemies[i][0], enemies[i][1]);
 }
}

function drawShip() {
 if (rightKey) ship_x += 5;
 else if (leftKey) ship_x -= 5;
 if (upKey) ship_y -= 5;
 else if (downKey) ship_y += 5;
 if (ship_x <= 0) ship_x = 0;
 if ((ship_x + ship_w) >= width) ship_x = width - ship_w;
  if (ship_y <= 0) ship_y = 0;
 if ((ship_y + ship_h) >= height) ship_y = height - ship_h;
        ctx.drawImage(ship, ship_x, ship_y);
}

function moveEnemies() {
  for (var i = 0; i < enemies.length; i++) {
   if (enemies[i][1] < height) {
     enemies[i][1] += enemies[i][4];
   } else if (enemies[i][1] > height - 1) {
      enemies[i][1] = -45;
    }
  }
}
function drawLaser() {
  if (lasers.length)
    for (var i = 0; i < lasers.length; i++) {
     ctx.fillStyle = '#f00';
     ctx.fillRect(lasers[i][0],lasers[i][1],lasers[i][2],lasers[i][3])
   }
}
function moveLaser() {
 for (var i = 0; i < lasers.length; i++) {
   if (lasers[i][1] > -11) {
      lasers[i][1] -= 10;
    } else if (lasers[i][1] < -10) {
     lasers.splice(i, 1);
   }
 }
}

function hitTest() {
 var remove = false;
 for (var i = 0; i < lasers.length; i++) {
   for (var j = 0; j < enemies.length; j++) {
     if (lasers[i][1] <= (enemies[j][1] + enemies[j][3]) && lasers[i][0] >= enemies[j][0] && lasers[i][0] <= (enemies[j][0] + enemies[j][2])) {
       remove = true;
        enemies.splice(j, 1);
                                enemies.push([(Math.random() * 500) + 50, -45, enemy_w, enemy_h, speed]);
      }
    }
    if (remove == true) {
      lasers.splice(i, 1);
      remove = false;
    }
  }
}

function init() {
  canvas = document.getElementById('canvas');
  ctx = canvas.getContext('2d');
        enemy = new Image();
  enemy.src = '8bit_enemy.png';
  ship = new Image();
  ship.src = 'ship.png';
  setInterval(gameLoop, 25);
  document.addEventListener('keydown', keyDown, false);
  document.addEventListener('keyup', keyUp, false);
}
function gameLoop() {
  clearCanvas();
        hitTest();
  moveEnemies();
        moveLaser();
  drawEnemies();
  drawShip();
        drawLaser();
}

function keyDown(e) {
  if (e.keyCode == 39) rightKey = true;
  else if (e.keyCode == 37) leftKey = true;
  if (e.keyCode == 38) upKey = true;
  else if (e.keyCode == 40) downKey = true;
  if (e.keyCode == 88 && lasers.length <= laserTotal) lasers.push([ship_x + 25, ship_y - 20, 4, 20]);

}

function keyUp(e) {
  if (e.keyCode == 39) rightKey = false;
  else if (e.keyCode == 37) leftKey = false;
  if (e.keyCode == 38) upKey = false;
  else if (e.keyCode == 40) downKey = false;
}

window.onload = init;
</script>
</head>

<body>
  <canvas id="canvas" width="600" height="600"></canvas>
</body>
</html>

One Response to Build a vertical scrolling shooter game with HTML5 canvas – Part 3

  1. jafar says:

    wow.

    this is an awesome tutorial. thank you for making this.

Leave a Reply

Your email address will not be published. Required fields are marked *