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

April 12th, 2011

If you have been following this series of tutorials, you‘ll know that we’re almost down building the basics of a game using the HTML5 canvas element and Javascript. We only have a few things to do. The black background is pretty boring, so we’ll add a star field. Arial doesn’t really work with our 8-bit feel, so we’ll use Google web fonts to find something that’s more appropriate. And it’s not really practical for the game to just start when the page is loaded, so we’ll add a start screen with some game play instructions.

First, we’ll add the star field background. To do this we’re going to use the exact same technique that we use for the enemy ships, it’ll just be slower and we don’t have to worry about anything affecting it. For the star field we’ll use an image and it will just scroll downwards continuously in the background. So, add this this to the variables at the top:

1
starfield, starX = 0, starY = 0, starY2 = -600

The starfield variable will hold our image and starX is our x position. And the reason we have starY and starY2 is because we’re going to make two instances of the star field and have it repeat over and over. We could dynamically generate all the stars in the background, but that would require a fair amount of processing power and I don’t think it’s worth it. We load the star field image the same way as the other two images, so add this to the init function:

1
2
starfield = new Image();
starfield.src = 'starfield.jpg';

Next we need a function that will actually create the movement of the stars:

1
2
3
4
5
6
7
8
9
10
11
12
function drawStarfield() {
  ctx.drawImage(starfield,starX,starY);
  ctx.drawImage(starfield,starX,starY2);
  if (starY > 600) {
    starY = -599;
  }
  if (starY2 > 600) {
    starY2 = -599;
  }
  starY += 1;
  starY2 += 1;
}

Pretty simple here, we’re drawing two instances of the star field, one is positioned on the y axis at 0 and the other one is positioned at -600. We then move both images downward and once the bottom image clears the bottom of the canvas, we then reposition it at the top of the canvas and it moves down again. With this cycling, it looks like the ship is slowly moving forward all the time. We need to just call the drawStarfield function, so add it to our gameLoop function but outside of the if statement so that the star field is moving even if they haven’t started to play yet, after we’ve added the start screen. It should look like this now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function gameLoop() {
  clearCanvas();
  drawStarfield()
  if (alive && lives > 0) {
    hitTest();
    shipCollision();
    moveLaser();
    moveEnemies();
    drawEnemies();
    drawShip();
    drawLaser();
  }
  scoreTotal();
  game = setTimeout(gameLoop, 1000 / 30);
}

Now, let’s give the game a more appropriate font. Lucky for us, Google’s web fonts has exactly what we’re looking for, a font called VT323. If you’ve never used a Google web font before, it’s really easy, even if you want to use it with the canvas element. The very first thing we need to do is at this at the top of the web page, right after the title tag:

1
<link href='http://fonts.googleapis.com/css?family=VT323' rel='stylesheet' />

Now we have access the VT323 font, as long as we have Internet access. And, in the scoreTotal function, we change this:

1
ctx.font = 'bold 18px Arial';

to this:

1
ctx.font = 'bold 20px VT323';

If you test the web page now, the font will look more 8-bit and fit a lot better with the game’s graphics style. We’re going to change one other thing, the position of our score text, because, unlike with Flash, we can’t just set it to right align so that it will move when the school gets too large for it’s space. So change this:

1
2
ctx.fillText('Score: ', 490, 30);
ctx.fillText(score, 550, 30);

to this:

1
2
ctx.fillText('Score: ', 10, 55);
ctx.fillText(score, 70, 55);

This will position the score right below the text and then we don’t have to worry about space, no matter how high the score gets.

Our final step will be set up a start screen so that our game doesn’t just automatically play when the page is loaded. The first thing we need is a new variable, so add this at the top with the other variables:

1
gameStarted = false

Our game hasn’t started yet so, of course, it’s going to be false. Next we need to set up the way for the player to start the game, so we’re going to add an event listener so that if they click anywhere on the canvas, the game will start. Add this to the init function with the other two event listeners:

1
canvas.addEventListener('click', gameStart, false);

When the player clicks the canvas, a function called gameStart will run, so let’s create that:

1
2
3
4
function gameStart() {
  gameStarted = true;
  canvas.removeEventListener('click', gameStart, false);
}

Wow, not much going on there, we’re just changing the gameStarted variable to true and removing the event listener. If you run the game now, you’ll see that absolutely nothing has changed. We need to make a few more changes before this will actually do anything. Our first change is to change this line in the gameLoop function:

1
if (alive && lives > 0)

to this:

1
if (alive && gameStarted && lives > 0)

So now alive and gameStarted need to be true and lives needs to be greater than zero for the game to run. Now the rest of our changes are going to be in the scoreTotal function because it’s easier if we keep all the text in the same place in case changes need to be made. Now, add this to it:

1
2
3
4
5
6
7
8
if (!gameStarted) {
  ctx.font = 'bold 50px VT323';
  ctx.fillText('Canvas Shooter', width / 2 - 150, height / 2);
  ctx.font = 'bold 20px VT323';
  ctx.fillText('Click to Play', width / 2 - 56, height / 2 + 30);
  ctx.fillText('Use arrow keys to move', width / 2 - 100, height / 2 + 60);
  ctx.fillText('Use the x key to shoot', width / 2 - 100, height / 2 + 90);
}

With this if statement, we are checking to see if the game has started yet, and if it hasn’t then display this text, which includes instructions on how to play. When you click on the canvas, the game starts and all this text goes away. Because we changed the font, it’s moved the positioning of our continue text a bit, so update it to this:

1
2
3
4
5
ctx.fillText('Game Over!', 245, height / 2);
ctx.fillRect((width / 2) - 60, (height / 2) + 10,100,40);
ctx.fillStyle = '#000';
ctx.fillText('Continue?', 250, (height / 2) + 35);
canvas.addEventListener('click', continueButton, false);

Alright, now if you test the game, there will be a start screen and you need to click on the canvas to start the game. You can check out the demo here.

And that’s it. We’ve created a basic shooter game using the canvas element and it really wasn’t the difficult. The canvas API is still pretty young, but I’m sure as it matures and as more and more libraries and frameworks are created, building things with canvas will be easier and easier. I’m done with this as an actual series of tutorials now, but I think I might come back to it once and a while to use as an example for other features that we might want to add to games.

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
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>HTML5 Canvas Game Part 6 || Atomic Robot Design</title>
<link href='http://fonts.googleapis.com/css?family=VT323' rel='stylesheet' />
<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,
    starfield,
    starX = 0, starY = 0, starY2 = -600,
    gameStarted = false;

//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 20px VT323';
  ctx.fillStyle = '#fff';
  ctx.fillText('Score: ', 10, 55);
  ctx.fillText(score, 70, 55);
  ctx.fillText('Lives:', 10, 30);
  ctx.fillText(lives, 68, 30);
        if (!gameStarted) {
    ctx.font = 'bold 50px VT323';
    ctx.fillText('Canvas Shooter', width / 2 - 150, height / 2);
    ctx.font = 'bold 20px VT323';
    ctx.fillText('Click to Play', width / 2 - 56, height / 2 + 30);
    ctx.fillText('Use arrow keys to move', width / 2 - 100, height / 2 + 60);
    ctx.fillText('Use the x key to shoot', width / 2 - 100, height / 2 + 90);
  }
  if (!alive) {
    ctx.fillText('Game Over!', 245, height / 2);
    ctx.fillRect((width / 2) - 60, (height / 2) + 10,100,40);
    ctx.fillStyle = '#000';
    ctx.fillText('Continue?', 250, (height / 2) + 35);
    canvas.addEventListener('click', continueButton, false);
  }
}

//Draws and animates the background starfield
function drawStarfield() {
  ctx.drawImage(starfield,starX,starY);
  ctx.drawImage(starfield,starX,starY2);
  if (starY > 600) {
    starY = -599;
  }
  if (starY2 > 600) {
    starY2 = -599;
  }
  starY += 1;
  starY2 += 1;
}

//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';
  starfield = new Image();
  starfield.src = 'starfield.jpg';
  document.addEventListener('keydown', keyDown, false);
  document.addEventListener('keyup', keyUp, false);
        canvas.addEventListener('click', gameStart, false);
  gameLoop();
}

function gameStart() {
  gameStarted = true;
  canvas.removeEventListener('click', gameStart, false);
}

//The main function of the game, it calls all the other functions needed to make the game run
function gameLoop() {
  clearCanvas();
  drawStarfield()
  if (alive && gameStarted && 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>

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

  1. cbarklow says:

    Hey, I just wanted to say thanks for posting these tutorials! For someone who knew very little about javascript and nothing about the canvas api this was a ton of help.

  2. Schenn says:

    A very helpful tutorial. You could go a bit further with it and use it to cover some OOP as well. Great tutorial anyway!

  3. Dan says:

    Amazing tutorial and easy to understand, as a student this was very helpful. Thanks.

  4. Stephen says:

    Great tutorial. Thanks for taking the time to make this available. You help the development community a lot when you share your knowledge.

  5. Thangadurai says:

    thanks for this great tutorial

Leave a Reply

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