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

March 20th, 2011

Last time we added collision detection for our ship and a score to our game. But only having one life isn’t all that fun and the player has to refresh the browser to play again. So in this part, we’re going to give our player multiple lives and we’ll add a continue button to the game over screen so that it’s easier for the player to play another round.

First, we’ll add the lives to our game. Add this to the list of variables at the top, because, of course, we need to keep track of how many lives our player has:

1
lives = 3

Next, we need to add some text to the canvas so our player can see how many lives they have left, so add this to the scoreTotal function:

1
2
ctx.fillText('Lives:', 10, 30);
ctx.fillText(lives, 68, 30);

Since we’re already drawing text on the canvas with this function, we might as well add the lives to it and keep all over our text in one function. This way, if there’s a problem, it’s easier to find it and it’s also easier to change or update because we know exactly where to look. Next, we actually have to tie the lives into the game and set it up so that when the ship is hit, the player loses a life and if the player runs out of lives, the game ends. Currently, if the ship hits an enemy, the alive variable is just changed from true to false and the game ends. We can’t just replace that with lives -= 1 because all that will do is continuously subtract one from the score. What we need to do is create a new function called checkLives, which will see if the player has move than one life and if not end the game, otherwise, the game will just reset to the start position and the player will lose one life. Here’s the code for checkLives:

1
2
3
4
5
6
7
8
function checkLives() {
 lives -= 1;
 if (lives > 0) {
   reset();
 } else if (lives == 0) {
   alive = false;
 }
}

With this function, we subtract one live from the lives total right off the bat and then we check to see if the total of lives is greater than 0 and if it is, then we call the reset function which we’ll write next. If live is equal to zero, then we change alive to false, as we’ve been doing and it ends the game. Now in the shipCollision function we need to change the line:

1
alive = false;

to:

1
checkLives();

Now, whenever the player’s ship hits an enemy, the checkLives function will be called. But what about the reset function? It’s just setting the player’s ship and the enemy ships back to the same position they have when the game starts because it would be unfair to just throw the player back into the middle of the game. Here’s the reset function:

1
2
3
4
5
6
7
8
9
function reset() {
 var enemy_reset_x = 50;
 ship_x = (width / 2) - 25, ship_y = height - 75, ship_w = 50, ship_h = 57;
 for (var i = 0; i < enemies.length; i++) {
   enemies[i][0] = enemy_reset_x;
   enemies[i][1] = -45;
   enemy_reset_x = enemy_reset_x + enemy_w + 60;
 }
}

Everything is the same as when we start the game, including using a loop to position the enemies. The only difference this time is that we need to introduce a variable called enemy_reset_x in order to evenly space the enemies across the x axis. This function will also be used when we set up the continue button, we’ll simply call it when the button is clicked and all the work to reset the game is done.

The continue button is going to be a little more complex to set up. In Flash, something like this would be very easy to set up. All we’d need to do is create a button or movieclip on the stage, give it an instance name like continue_btn and write some ActionScript saying when continue_btn is clicked, then call the reset function. The canvas API hasn’t evolved this far yet, unfortunately, so to get the same functionality takes a bit more code. A lot of thanks goes out to Mark Pilgrim’s Dive into HTML5 for this, because as far as I can find, his is the only example of how to set up click events for the canvas.

To make this work, we need to do a few things, first draw the button on the canvas, next when the mouse is clicked within the canvas find the co-ordinates of the mouse’s position, then check those co-ordinates to see if they fall within our button’s boundaries. A bit more complicated than Flash, but I’m sure that it will either be simplified in then actually API or someone will write a plug-in that enables it for us to do quickly and easily.

Back to the code. First things first, we need to actually draw the button on the canvas, so we have something to click. Update the scoreTotal function to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function scoreTotal() {
 ctx.font = 'bold 18px Arial';
 ctx.fillStyle = '#fff';
 ctx.fillText('Score: ', 490, 30);
 ctx.fillText(score, 550, 30);
 ctx.fillText('Lives:', 10, 30);
 ctx.fillText(lives, 68, 30);
 if (!alive) {
   ctx.fillText('Game Over!', 245, height / 2);
   ctx.fillRect((width / 2) - 53, (height / 2) + 10,100,40);
   ctx.fillStyle = '#000';
   ctx.fillText('Continue?', 252, (height / 2) + 35);
   canvas.addEventListener('click', continueButton, false);
 }
}

Now, when the player runs our of lives, a white box with black text on top saying continue is added. As well as a new event listener. The event listener is added to the canvas element, not the document, like our keypress listeners. This is because we only care if the player clicks on the canvas, not if they click elsewhere on the page. So now when the player clicks on the canvas, a function called continueButton is called, so we’ll write that now:

1
2
3
4
5
6
7
8
9
function continueButton(e) {
 var cursorPos = getCursorPos(e);
 if (cursorPos.x > (width / 2) - 53 && cursorPos.x < (width / 2) + 47 && cursorPos.y > (height / 2) + 10 && cursorPos.y < (height / 2) + 50) {
   alive = true;
   lives = 3;
   reset();
   canvas.removeEventListener('click', continueButton, false);
 }
}

A bunch of things are going on with this function, the first is variable called cursorPos equaling to something called getCursorPos. This is the function we’ll use to find the cursor’s position when the mouse is clicked and then return the values in cursorPos so we can check them against our button. If the mouse’s x and y position falls within our continue button, then we set alive back to true, the lives back to 3, we call the reset function and, importantly, we remove the event listener so that the player can’t accidentally click on the canvas and reset the game. So what’s going on with getCursorPos?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getCursorPos(e) {
 var x;
 var y;
 if (e.pageX || e.pageY) {
   x = e.pageX;
   y = e.pageY;
 } else {
   x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
   y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
 }
 x -= canvas.offsetLeft;
 y -= canvas.offsetTop;
 var cursorPos = new cursorPosition(x, y);
 return cursorPos;
}

This is a code that was written by Mark Pilgrim for his Halma game example, I’ve just modified the last two lines to fit better with our game. It’s using either the web standards way, pageX and pageY or the Microsoft way, with clientX and clientY. And then it subtracts the top and left offsets of the canvas, ie. subtracting the distance of the canvas’ left side with the left side of the browser and the same with the top. Then we create a local variable called cursorPos, which isn’t the same as the cursorPos in the continueButton function but it’s what we’ll return, so that cursorPos will equal this cursorPos, so it’s simpler to just name them the same. cursorPos is going to equal a new cursorPosition:

1
2
3
4
function cursorPosition(x,y) {
 this.x = x;
 this.y = y;
}

We’ll use cursorPosition to store our position info. So when we return cursorPos, in the continueButton function, we can check cursorPos.x and cursorPos.y instead of creating two variables for each one. Now, if the mouse cursor is within the continue button when the mouse is clicked, we can reset the game! You can check out the game so far here.

That’s a lot of work for a continue button, it was more work then setting up most of the games main functions, but, right now, that’s how we have to do things with the canvas tag. I think it’s worth it though, because who wants to reset a game by refreshing the browser?

Our game is getting there, but it still needs a couple of things before we can say we have a basic game built. Next time, we’ll add a moving background, change the text to something works better with our 8-bit feel and finally, we’ll add a start screen so that the game just doesn’t start when the page is loaded.

Complete code so far:

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Game with score</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,
    enemyTotal = 5,
    enemies = [],
    enemy_x = 50,
    enemy_y = -45,
    enemy_w = 50,
    enemy_h = 38,
    speed = 3,
    enemy,
    rightKey = false,
    leftKey = false,
    upKey = false,
    downKey = false,
    ship,
    ship_x = (width / 2) - 25, ship_y = height - 75, ship_w = 50, ship_h = 57,
    laserTotal = 2,
    lasers = [],
    score = 0,
    alive = true,
    lives = 3;

//Array to hold all the enemies on screen
for (var i = 0; i < enemyTotal; i++) {
 enemies.push([enemy_x, enemy_y, enemy_w, enemy_h, speed]);
 enemy_x += enemy_w + 60;
}

//Clears the canvas so it can be updated
function clearCanvas() {
 ctx.clearRect(0,0,width,height);
}

//Cycles through the array and draws the updated enemy position
function drawEnemies() {
 for (var i = 0; i < enemies.length; i++) {
   ctx.drawImage(enemy, enemies[i][0], enemies[i][1]);
 }
}

//If an arrow key is being pressed, moves the ship in the right direction
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);
}

//This moves the enemies downwards on the canvas and if one passes the bottom of the canvas, it moves it to above the canvas
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;
    }
  }
}

//If there are lasers in the lasers array, then this will draw them on the canvas
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])
   }
}

//If we're drawing lasers on the canvas, this moves them up the canvas
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);
   }
 }
}

//Runs a couple of loops to see if any of the lasers have hit any of the enemies
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);
        score += 10;
        enemies.push([(Math.random() * 500) + 50, -45, enemy_w, enemy_h, speed]);
      }
    }
    if (remove == true) {
      lasers.splice(i, 1);
      remove = false;
    }
  }
}

//Similar to the laser hit test, this function checks to see if the player's ship collides with any of the enemies
function shipCollision() {
  var ship_xw = ship_x + ship_w,
      ship_yh = ship_y + ship_h;
  for (var i = 0; i < enemies.length; i++) {
   if (ship_x > enemies[i][0] && ship_x < enemies[i][0] + enemy_w && ship_y > enemies[i][1] && ship_y < enemies[i][1] + enemy_h) {
     checkLives();
    }
    if (ship_xw < enemies[i][0] + enemy_w && ship_xw > enemies[i][0] && ship_y > enemies[i][1] && ship_y < enemies[i][1] + enemy_h) {
     checkLives();
    }
    if (ship_yh > enemies[i][1] && ship_yh < enemies[i][1] + enemy_h && ship_x > enemies[i][0] && ship_x < enemies[i][0] + enemy_w) {
     checkLives();
    }
    if (ship_yh > enemies[i][1] && ship_yh < enemies[i][1] + enemy_h && ship_xw < enemies[i][0] + enemy_w && ship_xw > enemies[i][0]) {
     checkLives();
    }
  }
}

//This function runs whenever the player's ship hits an enemy and either subtracts a life or sets the alive variable to false if the player runs out of lives
function checkLives() {
  lives -= 1;
  if (lives > 0) {
    reset();
  } else if (lives == 0) {
    alive = false;
  }
}

//This simply resets the ship and enemies to their starting positions
function reset() {
  var enemy_reset_x = 50;
  ship_x = (width / 2) - 25, ship_y = height - 75, ship_w = 50, ship_h = 57;
  for (var i = 0; i < enemies.length; i++) {
   enemies[i][0] = enemy_reset_x;
   enemies[i][1] = -45;
   enemy_reset_x = enemy_reset_x + enemy_w + 60;
 }
}

//After the player loses all their lives, the continue button is shown and if clicked, it resets the game and removes the event listener for the continue button
function continueButton(e) {
 var cursorPos = getCursorPos(e);
 if (cursorPos.x > (width / 2) - 53 && cursorPos.x < (width / 2) + 47 && cursorPos.y > (height / 2) + 10 && cursorPos.y < (height / 2) + 50) {
   alive = true;
    lives = 3;
    reset();
    canvas.removeEventListener('click', continueButton, false);
  }
}

//holds the cursors position
function cursorPosition(x,y) {
  this.x = x;
  this.y = y;
}

//finds the cursor's position after the mouse is clicked
function getCursorPos(e) {
  var x;
  var y;
  if (e.pageX || e.pageY) {
    x = e.pageX;
    y = e.pageY;
  } else {
    x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  }
  x -= canvas.offsetLeft;
  y -= canvas.offsetTop;
  var cursorPos = new cursorPosition(x, y);
  return cursorPos;
}

//Draws the text for the score and lives on the canvas and if the player runs out of lives, it's draws the game over text and continue button as well as adding the event listener for the continue button
function scoreTotal() {
  ctx.font = 'bold 18px Arial';
  ctx.fillStyle = '#fff';
  ctx.fillText('Score: ', 490, 30);
  ctx.fillText(score, 550, 30);
  ctx.fillText('Lives:', 10, 30);
  ctx.fillText(lives, 68, 30);
  if (!alive) {
    ctx.fillText('Game Over!', 245, height / 2);
    ctx.fillRect((width / 2) - 53, (height / 2) + 10,100,40);
    ctx.fillStyle = '#000';
    ctx.fillText('Continue?', 252, (height / 2) + 35);
    canvas.addEventListener('click', continueButton, false);
  }
}

//The initial function called when the page first loads. Loads the ship, enemy and starfield images and adds the event listeners for the arrow keys. It then calls the gameLoop function.
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';
  document.addEventListener('keydown', keyDown, false);
  document.addEventListener('keyup', keyUp, false);
  gameLoop();
}

//The main function of the game, it calls all the other functions needed to make the game run
function gameLoop() {
  clearCanvas();
  if (alive && lives > 0) {
   hitTest();
    shipCollision();
    moveLaser();
    moveEnemies();
    drawEnemies();
    drawShip();
    drawLaser();
  }
  scoreTotal();
  game = setTimeout(gameLoop, 1000 / 30);
}

//Checks to see which key has been pressed and either to move the ship or fire a laser
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]);
}

//Checks to see if a pressed key has been released and stops the ships movement if it has
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 5

  1. Pinky says:

    This is amazing and thankyou, I have spent months looking for a tutorial this comprehensive I do have two questions, if I want to increase the players difficulty by changing size of ship, making it bigger and easier for enemies to hit how do I do that ? And if I want enemies to kill ship by landing on it simultaneouly, basically am I adding to var or am I adding function ?

Leave a Reply

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