In the first part of this series, you learned the basics of using JavaScript and the canvas
element to make a very simple HTML5 avoider game. But it's too simple - the single enemy doesn't even move - there's no challenge! In this tutorial, you'll learn how to create a never-ending stream of enemies, all falling from the top of the screen.
Refresher
In the first part of the tutorial we covered quite a few concepts: drawing images to the screen, interacting between HTML and JavaScript, detecting mouse actions, and the if
statement. You can download the source files here if you want to dive in to this part of the tutorial, though I recommend reading all parts in order.
Our game's HTML page contains a canvas element, which triggers a JavaScript function called drawAvatar()
when it is clicked. That function is inside a separate file called main.js
, and it does two things:
- Draws a copy of
avatar.png
to the canvas. - Sets up an event listener to call another function, called
redrawAvatar()
, whenever the mouse moves over the canvas.
The redrawAvatar()
function is also inside main.js
; unlike drawAvatar()
it accepts a parameter - called mouseEvent
- which is automatically passed to it by the event listener. This mouseEvent
contains information about the mouse's position. The function does four things:
- Clears the canvas.
- Draws a copy of
avatar.png
to the canvas, at the mouse's position. - Draws a copy of
enemy.png
to the canvas, at a specified position. - Checks to see whether the avatar and enemy are close enough to each other to be overlapping, and displays an
alert()
if so.
All clear? If not, try the warm up challenge.
Warm Up Challenge
If it's been a while since you read the first part of the series (or if you just want to check that you understand what's going on), have a go at these little exercises. They're completely optional and separate to the actual tutorial, so I recommend working on a copy of your project rather than the original. You can complete all of these exercises using only information from the first part of the series.
Easy
Remember that drawImage()
works like a potato stamp. Use it to create an unbroken ring of enemies around the edge of your canvas, like this:
(If you get bored of copying and pasting all those statements, feel free to make it a smaller ring - you could resize the canvas, too, if you like.)
Medium
Make the "you hit the enemy" alert appear whenever the avatar hits the edge of the ring. (To test this, remember that you can hit Enter to dismiss the alert; you don't have to click OK.)
Hard
That alert will come up when you try to move your mouse from outside the canvas to inside it, which is really annoying if you've already clicked the canvas. Make it possible to move into the canvas without triggering the alert - but once inside, make the alert appear whenever the avatar touches the ring of enemies.
Make the Enemy Move
We're going to make the enemy fall down from the top of the screen. For now, we'll focus on making it move rather than on detecting a collision, so "comment out" the lines in redrawEnemy()
that deal with collisions, like so:
//if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //}
Remember: two forward slashes tell the browser "ignore everything on this line from here on". Earlier on we used this to create comments - little reminders of what certain bits of code do - but here we're using it for another purpose: stopping certain bits of code from running without completely deleting them. This makes it easy for us to put the code back in later.
At the moment, the enemy is redrawn, in the same position, whenever we move the mouse:
gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150);
Do you remember the Math.random()
function from the first part of the tutorial? It returns a random number between zero and one; multiplying it by 300 (the height of the canvas) would give us a number between 0 and 300. What happens if we draw the enemy at a random y-position between 0 and 300 every time the mouse was moved? Let's try it out; modify that line like so:
gameCanvas.getContext("2d").drawImage(enemyImage, 250, Math.random() * 300);
It looks like the enemy is moving randomly up and down a certain line, but only when the mouse is moved. Of course, it's not actually moving; it's "teleporting" from one position to the next, but this gives the illusion of movement.
We'd get a better illusion if it only moved in one direction. We can achieve this by making sure the enemy's y-position only increases, and never decreases.
Consider the following code:
function redrawAvatar(mouseEvent) { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyImage = new Image(); var enemyY = 0; avatarImage.src = "img/avatar.png"; enemyImage.src = "img/enemy.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} }
See what I'm doing? I set the value of enemyY
to 0
at the top of the function, then increased it by one pixel before drawing the enemy. In this way, I'm aiming to make the enemy's y-position increase by one pixel every time redrawAvatar()
is run.
However, there's a flaw in my logic. The line var enemyY = 0;
will reset the enemyY
variable to 0
every time redrawAvatar()
is run, which means that it'll always be drawn at a y-position of 1
(because it'll be increased at line 12).
We need to only set it to 0
once. What if we do that in drawEnemy()
? After all, that function is only run once:
function drawAvatar() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyY = 0; avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", redrawAvatar); } function redrawAvatar(mouseEvent) { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyImage = new Image(); avatarImage.src = "img/avatar.png"; enemyImage.src = "img/enemy.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} }
Unfortunately, this doesn't work at all. The problem lies in a concept called scope. If you use the var
keyword to define a variable within a function, then the variable will only be accessible within that function. This means that our redrawAvatar()
function cannot access the same enemyY
variable that was defined in drawAvatar()
.
However, if we define a variable outside of all functions, it can be accessed by any one of them! So, try this:
var enemyY = 0; function drawAvatar() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", redrawAvatar); } function redrawAvatar(mouseEvent) { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyImage = new Image(); avatarImage.src = "img/avatar.png"; enemyImage.src = "img/enemy.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} }
It works - the enemy slides down the screen. However, it only does so while we're moving the mouse. That's an interesting game mechanic, but it's not what I was aiming for.
Make the Enemy Move on Its Own
It'd be much better if the enemy appeared to move of its own accord - meaning, it moves regardless of whether or not the player is moving the mouse. We can do this by triggering its movement (its "teleportations") based on time rather than on mouse movement.
We can do this by using the setInterval()
function. It works like this:
setInterval(functionName, period);
Here, functionName
is the name of a function we want to run over and over again, and period
is the amount of time (in milliseconds) we want to pass between each call to that function.
Let's see how this looks:
var enemyY = 0; function drawAvatar() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", redrawAvatar); setInterval(redrawEnemy, 1000); } function redrawAvatar(mouseEvent) { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} } function redrawEnemy() { var enemyImage = new Image(); enemyImage.src = "img/enemy.png"; enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); }
I've moved all the code that deals with moving and drawing the enemy to the new redrawEnemy()
function, and I've set it to be called every 1,000 milliseconds (every second) using a setInterval()
call in drawAvatar()
. (Unlike when using an event listener, no parameters automatically get passed to redrawEnemy()
when we call it from setInterval()
.)
Try it out here! Click the canvas, then don't move your mouse.
There are a few things wrong with this:
- The enemy leaves a trail - this is because the canvas isn't cleared in
redrawEnemy()
. - The enemy moves really slowly - perhaps 1000 milliseconds is too long to wait.
- When the avatar is moved, the enemy disappears - this is because the enemy is only drawn in
redrawEnemy()
; inredrawAvatar()
the canvas is cleared and the avatar is redrawn, but not the enemy.
Let's fix these one at a time. First, we'll clear the canvas in redrawEnemy()
:
function redrawEnemy() { var enemyImage = new Image(); enemyImage.src = "img/enemy.png"; gameCanvas.width = 400; //this erases the contents of the canvas enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); }
Hm. Now the avatar disappears whenever the enemy is drawn, and vice-versa. Of course, this makes sense; we clear the canvas in both redrawEnemy()
and redrawAvatar()
, but never draw both the enemy and the avatar at the same time.
What if we moved the enemy in redrawEnemy()
- by increasing the value of enemyY
- but actually drew it in redrawAvatar()
?
var enemyY = 0; function drawAvatar() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", redrawAvatar); setInterval(redrawEnemy, 1000); } function redrawAvatar(mouseEvent) { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyImage = new Image(); enemyImage.src = "img/enemy.png"; avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} } function redrawEnemy() { enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. }
It sort of works, but we're back to that problem where the enemy only moves while you're moving the mouse. However, this time it's slightly different; to make this more obvious, we can increase the enemy's speed by reducing the period
. Set it to 25 (that's 1/40th of a second, meaning redrawEnemy()
will run 40 times per second):
setInterval(redrawEnemy, 25);
Compare this with the earlier version where the enemy only moved when the mouse was moving. See the difference? In the new one, the enemy's position keeps changing, but it does so "behind the scenes"; the actual image of the enemy only moves when the mouse is moved. If you wait a second or so before moving the mouse, the enemy image jumps down the screen to catch up with its actual position.
Separating the enemy's actual position from the enemy's image's position like this is going to let us solve our problem.
Before we move on, are you getting confused by the function names? I am. redrawEnemy()
isn't actually drawing the enemy at all. Let's rename them to something a bit easier to keep track of.
-
drawAvatar()
is run when we start the game, and it sets everything up, so let's rename it tosetUpGame()
-
redrawAvatar()
is run whenever the mouse moves, so let's rename it tohandleMouseMovement()
-
redrawEnemy()
is run every fraction of a second; it's as if there's a clock that ticks 40 times a second, and each tick triggers the function. So, let's rename it tohandleTick()
Don't forget you have to rename all the references to the functions as well, in the event listener and the setInterval()
. Here's what it'll look like:
var enemyY = 0; function setUpGame() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", handleMouseMovement); setInterval(handleTick, 25); } function handleMouseMovement(mouseEvent) { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyImage = new Image(); enemyImage.src = "img/enemy.png"; avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} } function handleTick() { enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. }
(You'll also need to change the HTML page, so that the canvas's onclick
attribute is "setUpGame();"
rather than "drawAvatar();"
.
I think this makes it easier to see what's going on:
- When the mouse moves, we move the avatar's position, draw the avatar in its current position, and draw the enemy in its current position.
- When the clock ticks, we move the enemy's position.
- We need to draw the enemy and the avatar at the same time (i.e. in the same function).
- If we only draw the enemy when the mouse moves, then the enemy's movement is not smooth.
This makes it easier in turn to see a possible solution:
- When the mouse moves, move the avatar's position.
- When the clock ticks, move the enemy's position, draw the avatar in its current position, and draw the enemy in its current position.
Let's implement that. All we need to do is move the drawing code from handleMouseMovement()
to handleTick()
, right? Like this:
var enemyY = 0; function setUpGame() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", handleMouseMovement); setInterval(handleTick, 25); } function handleMouseMovement(mouseEvent) { //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} } function handleTick() { var avatarImage = new Image(); var enemyImage = new Image(); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. enemyImage.src = "img/enemy.png"; avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); }
Hmm. That's not right. We've got nothing left in handleMouseMovement()
. Ah - but that's because we haven't separated the avatar's image's position from the avatar's actual position, like we did with the enemy. So let's do that:
var enemyY = 0; var avatarX = 0; var avatarY = 0; function setUpGame() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", handleMouseMovement); setInterval(handleTick, 25); } function handleMouseMovement(mouseEvent) { avatarX = mouseEvent.offsetX; avatarY = mouseEvent.offsetY; //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} } function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); var enemyImage = new Image(); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. enemyImage.src = "img/enemy.png"; avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); }
We have to create new variables to store the avatar's actual x- and y-positions, and define those variables outside of any function, so that we can access them from anywhere.
This works! (If it's a little jerky, then try closing some tabs or restarting Chrome; that worked for me.) We now have a moving enemy. Took a while to get there, but the actual code we've ended up with isn't too complex, I hope you'll agree.
If you want a challenge, try re-introducing the collision detection alert box. Don't worry if you have troubles; we'll go through this again a bit later.
In the mean time, we'll look at a problem you probably haven't come across yet...
Loading the Images From a Server
As I mentioned in the first part of this series, if you put your game onto a web server as it is now, it won't work correctly, even though they work fine when running from your computer. My demos work because I've made a slight modification to the code; here's how the game runs without that code:
What's going on? Well, it's to do with the images. Every time the clock ticks, we create a new image and set its source to point to an actual image file. This doesn't cause problems when the image file is on your hard drive, but when it's on the Internet, the page might try to download the image before drawing it - leading to the flickering that we can see in the demo.
Perhaps you can already guess at a solution, based on what we've done in this tutorial so far. Just like with the enemy's and avatar's positions, we can move the enemy's and avatar's images outside of the functions, and re-use them over and over again, without having to define them and set their values in the handleTick()
function each time.
Take a look:
var enemyY = 0; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; function setUpGame() { var gameCanvas = document.getElementById("gameCanvas"); avatarImage = new Image(); enemyImage = new Image(); enemyImage.src = "img/enemy.png"; avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", handleMouseMovement); setInterval(handleTick, 25); } function handleMouseMovement(mouseEvent) { avatarX = mouseEvent.offsetX; avatarY = mouseEvent.offsetY; //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { // alert("You hit the enemy!"); //} } function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); }
Try it out here - no flickering!
In case you're wondering: we could have moved lines 9-12 outside of the functions as well. I chose to put them in setUpGame()
simply because they seemed to be more about, well, setting up the game.
Make Another Enemy
It's actually really easy to make another enemy appear on the screen. Remember that images are like potato stamps; that means there's nothing stopping us from drawing the enemy image onto the canvas in two different places within the same tick:
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); gameCanvas.getContext("2d").drawImage(enemyImage, 100, enemyY); }
Simple!
You can put them at different heights, like so:
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); gameCanvas.getContext("2d").drawImage(enemyImage, 100, enemyY - 50); }
This is a bit messy, though. Instead, how about just creating an enemyY2
variable?
var enemyY = 0; var enemyY2 = -50; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; //... function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. enemyY2 = enemyY2 + 1; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); gameCanvas.getContext("2d").drawImage(enemyImage, 100, enemyY2); }
Now you can set the initial positions of enemyY
and enemyY2
to whatever you want, without having to change the code in handleTick()
.
Make Five Enemies
Try extending what we've just done so that there are five enemies, all with different starting points. Take a look at my code if you need to. Here's a hint: you only need to add code outside of the functions and inside the handleTick()
function - no need to touch setUpGame()
or handleMouseMovement()
.
var enemyY = 0; var enemyY2 = -50; var enemyY3 = -75; var enemyY4 = -120; var enemyY5 = -250; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; //... function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); enemyY = enemyY + 1; //increase enemyY variable by one pixel. If enemyY is 10, then enemyY + 1 is 11, etc. enemyY2 = enemyY2 + 1; enemyY3 = enemyY3 + 1; enemyY4 = enemyY4 + 1; enemyY5 = enemyY5 + 1; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyY); gameCanvas.getContext("2d").drawImage(enemyImage, 130, enemyY2); gameCanvas.getContext("2d").drawImage(enemyImage, 300, enemyY3); gameCanvas.getContext("2d").drawImage(enemyImage, 50, enemyY4); gameCanvas.getContext("2d").drawImage(enemyImage, 190, enemyY5); }
Make Ten Enemies
Oh, this is going to get tedious, right? Maintaining all those enemies, and adding three lines of code for each one. Yuck.
Allow me to introduce arrays. Take a look at this:
var enemyYPositions = [0, -50, -75, -120, -250]; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; //... function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); enemyYPositions[0] = enemyYPositions[0] + 1; enemyYPositions[1] = enemyYPositions[1] + 1; enemyYPositions[2] = enemyYPositions[2] + 1; enemyYPositions[3] = enemyYPositions[3] + 1; enemyYPositions[4] = enemyYPositions[4] + 1; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyYPositions[0]); gameCanvas.getContext("2d").drawImage(enemyImage, 130, enemyYPositions[1]); gameCanvas.getContext("2d").drawImage(enemyImage, 300, enemyYPositions[2]); gameCanvas.getContext("2d").drawImage(enemyImage, 50, enemyYPositions[3]); gameCanvas.getContext("2d").drawImage(enemyImage, 190, enemyYPositions[4]); }
This gives us the exact same result as before, but:
- All of the enemies' y positions are defined in a single line, and
- We get an easy way to refer to any of these positions:
enemyYPosition[enemyNumber]
.
This type of variable is called an array; it's a way of holding a list of values (or even of other variables), and lets us retrieve any item from that list using a number. Note that the first element of an array is number 0, the second value is number 1, and so on - we call arrays "zero-based" for this reason.
Looping
Now take a look at this section of code:
enemyYPositions[0] = enemyYPositions[0] + 1; enemyYPositions[1] = enemyYPositions[1] + 1; enemyYPositions[2] = enemyYPositions[2] + 1; enemyYPositions[3] = enemyYPositions[3] + 1; enemyYPositions[4] = enemyYPositions[4] + 1;
We're doing the same thing, over and over again, to different items in the array. Each line of code is the same as all of the others, except that the number inside the square brackets changes. This is great, because we can write code to say "do this same thing five times, but changing one number each time". For example:
var currentNumber = 0; while (currentNumber < 5) { alert(currentNumber); currentNumber = currentNumber + 1; }
If you put this in your JS file (in setUpGame()
, for example), it would make the page display five alert boxes: the first would say "0"; the second would say "1"; and so on up to "4". In other words, it's equivalent to doing this:
var currentNumber = 0; alert(currentNumber); currentNumber = currentNumber + 1; alert(currentNumber); currentNumber = currentNumber + 1; alert(currentNumber); currentNumber = currentNumber + 1; alert(currentNumber); currentNumber = currentNumber + 1; alert(currentNumber); currentNumber = currentNumber + 1;
This is because the while
statement acts like a repeated if
statement. Remember, if
works like this:
if (condition) { outcome; }
"If condition
is true, then run outcome
."
while
works like this:
while (condition) { outcome; }
"As long as condition
remains true, keep running outcome
."
It's a subtle difference, but a really important one. An if
block will run just once, if the condition
is true; a while
block will run over and over again until the condition stops condition
stops being true.
For this reason, the outcome
- the code that's run inside the while
block - usually contains some code that will, eventually, cause condition
to stop being true; if it didn't the code would just repeat itself forever. In our alert box example, we increased the value of currentNumber
until it "currentNumber " was no longer true (when
currentNumber
reached 5, it was no longer less than 5, which is why we never see an alert box containing the number 5).
Running code over and over again like this is called "looping", and the while
block is called a "loop". Let's now take this code:
enemyYPositions[0] = enemyYPositions[0] + 1; enemyYPositions[1] = enemyYPositions[1] + 1; enemyYPositions[2] = enemyYPositions[2] + 1; enemyYPositions[3] = enemyYPositions[3] + 1; enemyYPositions[4] = enemyYPositions[4] + 1;
...and put it into a loop:
var currentEnemyNumber = 0; while (currentEnemyNumber < 5) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; }
Great! Or is it?
Actually, that's not quite right: we're not changing the value of currentEnemyNumber
. This means that we'll just increase the value of enemyYPositions[0]
over and over again, forever, without ever changing the other enemies' y-positions or ever exiting the loop.
So, we need to do this:
var currentEnemyNumber = 0; while (currentEnemyNumber < 5) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; currentEnemyNumber = currentEnemyNumber + 1; }
Now it's great.
Can we apply the same thinking to our other repetitive code? I'm referring to this:
gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyYPositions[0]); gameCanvas.getContext("2d").drawImage(enemyImage, 130, enemyYPositions[1]); gameCanvas.getContext("2d").drawImage(enemyImage, 300, enemyYPositions[2]); gameCanvas.getContext("2d").drawImage(enemyImage, 50, enemyYPositions[3]); gameCanvas.getContext("2d").drawImage(enemyImage, 190, enemyYPositions[4]);
Unfortunately, something like this won't work:
var currentEnemyNumber = 0; while (currentEnemyNumber < 5) { gameCanvas.getContext("2d").drawImage(enemyImage, 250, enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; }
...because not all enemies have an x-position of 250. But we can make it work, if we move all the enemies x-positions to another array:
var enemyXPositions = [250, 130, 300, 50, 190]; var enemyYPositions = [0, -50, -75, -120, -250]; //... var currentEnemyNumber = 0; while (currentEnemyNumber < 5) { gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; }
This has the added benefit of keeping all the enemies' x- and y-positions in one neat location, rather than spread out across several lines.
Let's look at the code in context:
var enemyYPositions = [0, -50, -75, -120, -250]; var enemyXPositions = [250, 130, 300, 50, 190]; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; //... function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; while (currentEnemyNumber < 5) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; currentEnemyNumber = currentEnemyNumber + 1; } gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); currentEnemyNumber = 0; while (currentEnemyNumber < 5) { gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; } }
Note that, on line 22 above, I've reset currentEnemyNumber
to 0; if I didn't, the loop for drawing the enemy images wouldn't run even once, as condition
would already be false from the earlier loop that moves the enemies. Also note that, when I do this, the loop that draws the enemies doesn't immediately detect that condition
is now true again and start moving all the enemies.
It all works just as it did before.
The biggest benefit to this is in how easy it is to add another five enemies. We only need to change four lines of code:
var enemyYPositions = [0, -50, -75, -120, -250, -280, -305, -330, -340, -400]; var enemyXPositions = [250, 130, 300, 50, 190, 200, 220, 60, 100, 110]; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; //... function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; while (currentEnemyNumber < 10) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; currentEnemyNumber = currentEnemyNumber + 1; } gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); currentEnemyNumber = 0; while (currentEnemyNumber < 10) { gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; } }
Simple!
Make Fifteen Enemies
Here's another exercise for you: modify the code so that it creates fifteen enemies. Again, you only have to alter four lines of code. Take a look at my code if you're not sure:
var enemyYPositions = [0, -50, -75, -120, -250, -280, -305, -330, -340, -400, -425, -450, -500, -520, -550]; var enemyXPositions = [250, 130, 300, 50, 190, 200, 220, 60, 100, 110, 30, 300, 150, 190, 90]; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; //... function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; while (currentEnemyNumber < 15) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; currentEnemyNumber = currentEnemyNumber + 1; } gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); currentEnemyNumber = 0; while (currentEnemyNumber < 15) { gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; } }
Obviously we could continue on like this. But I'm already finding it irritating to change lines 14 and 23 above, and have forgotten to do so a couple of times.
Fortunately we can automate this, in a way. The number - 5, 10, 15, or whatever - is equal to the number of items in either the enemyXPositions[]
or enemyYPositions[]
array. We call this the array's length, and can retrieve it from either array by using the .length
property - like so:
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; while (currentEnemyNumber < enemyXPositions.length) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; currentEnemyNumber = currentEnemyNumber + 1; } gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); currentEnemyNumber = 0; while (currentEnemyNumber < enemyXPositions.length) { gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; } }
Or, to be a bit neater:
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; var numberOfEnemies = enemyXPositions.length; while (currentEnemyNumber < numberOfEnemies) { enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1; currentEnemyNumber = currentEnemyNumber + 1; } gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY); currentEnemyNumber = 0; while (currentEnemyNumber < numberOfEnemies) { gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]); currentEnemyNumber = currentEnemyNumber + 1; } }
Now you can make as many enemies as you want, just by adding new numbers to the enemyXPositions[]
and enemyYPositions[]
arrays.
Re-Introduce Collision Detection
Remember how collision detection worked? We've had the code sitting in handleMouseMovement()
(though commented out) for a while:
if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { alert("You hit the enemy!"); }
It's basically checking whether two rectangles - one that moves with the cursor, and one that sits still on the canvas - are overlapping. But it's hard to understand from that code, so let's take a fresh look.
First, let's look at it in terms of horizontal overlap:
Here, the avatar and the enemy are not overlapping. We have: avatarX
Here, the avatar and the enemy are overlapping. We have: avatarX
Still overlapping. We have: enemyX
No longer overlapping. We have: enemyX
Taking all these together, we can see that the enemy and avatar are overlapping horizontally if:
avatarX and
enemyX
...or:
enemyX and
avatarX
In other words, for horizontal overlap, this condition must be true:
(avatarX
(Remember, &&
means "and"; ||
means "or".)
It's not hard to come up with a similar condition for vertical overlap:
(avatarY
(We use 33 here because the avatar is 33 pixels tall, but only 30 pixels wide.)
For the enemy and avatar to be overlapping, they must be overlapping both horizontally and vertically - so we use the &&
operator to combine these two conditions:
( (avatarX
That's a long condition! But it's actually quite simple, now that we've broken it down. Let's put it into our code. First, delete the old collision detection code from handleMouseMovement()
; we'll put this in handleTick()
, after we've moved and redrawn the enemy and avatar, so that it seems fairer.
To start with, we'll just check for a collision between the avatar and the first enemy:
function handleTick() { //... if ( ( (avatarX < enemyXPositions[0] && enemyXPositions[0] < avatarX + 30) || (enemyXPositions[0] < avatarX && avatarX < enemyXPositions[0] + 30) ) && ( (avatarY < enemyYPositions[0] && enemyYPositions[0] < avatarY + 33) || (enemyYPositions[0] < avatarY && avatarY < enemyYPositions[0] + 30) ) ) { alert("You hit the first enemy!"); } }
It looks messy (so you should probably add a comment to remind yourself of how it works later), but it's got everything we need. Does it work?
It does work! Now we should make it work for all the enemies - and of course we can use a loop to do that:
currentEnemyNumber = 0; while (currentEnemyNumber < numberOfEnemies) { if ( ( (avatarX < enemyXPositions[currentEnemyNumber] && enemyXPositions[currentEnemyNumber] < avatarX + 30) || (enemyXPositions[currentEnemyNumber] < avatarX && avatarX < enemyXPositions[currentEnemyNumber] + 30) ) && ( (avatarY < enemyYPositions[currentEnemyNumber] && enemyYPositions[currentEnemyNumber] < avatarY + 33) || (enemyYPositions[currentEnemyNumber] < avatarY && avatarY < enemyYPositions[currentEnemyNumber] + 30) ) ) { alert("You hit an enemy!"); } currentEnemyNumber = currentEnemyNumber + 1; }
Note that this goes inside handleTick()
, at the end, so we have to reset currentEnemyNumber
to 0. We also need to change the alert box text, since it might not be the first enemy that causes the alert to appear.
All right, this is really shaping into a game! Okay, sure, the alert box is kind of annoying, but it serves our purposes for now.
There's one more big addition I'd like us to make in this part...
Make Infinitely Many Enemies
We can add more and more enemy positions to our arrays - a hundred, a thousand, whatever - but eventually the supply will run out, and the player will have no more enemies to dodge. We need to be able to create new enemies and add their positions to the arrays while the game is running.
When we want to change the value of a specific item inside enemyXPositions
, it's easy: we just write enemyXPositions[3] = 100
, or whatever. But how can we add something to the array? Writing enemyXPositions = [100]
(or whatever) will just replace the array with a new one, containing just one item.
The answer is in the arrays' .push()
function; this allows us to add an item to an array without creating a new one. To demonstrate this, let's delete all the items from our arrays, and then use .push()
to add new ones:
var enemyYPositions = []; //empty square brackets means new empty array var enemyXPositions = []; var avatarX = 0; var avatarY = 0; var avatarImage; var enemyImage; function setUpGame() { var gameCanvas = document.getElementById("gameCanvas"); avatarImage = new Image(); enemyImage = new Image(); enemyImage.src = "img/enemy.png"; avatarImage.src = "img/avatar.png"; enemyYPositions.push(0); enemyXPositions.push(250); gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); gameCanvas.addEventListener("mousemove", handleMouseMovement); setInterval(handleTick, 25); }
It works fine; the enemy still moves and the collision detection still works. It's exactly the same as if we'd started the code with:
var enemyYPositions = [0]; var enemyXPositions = [250];
So what happens if we push the new values onto the arrays inside handleTick()
, rather than inside setUpGame()
?
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; var numberOfEnemies = enemyXPositions.length; enemyYPositions.push(0); enemyXPositions.push(250);
Hm. It's creating new enemies in the same positions at such a fast rate that they're overlapping. (The one at the top of the screen appears to be above all of the others because that's the last one to get drawn.)
Let's try to fix this by creating the enemies at random starting x-positions. Remember that Math.random()
gets us a random number between 0 and 1, so to get a random number between 0 and 400 - the width of the canvas - we can use Math.random() * 400
:
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; var numberOfEnemies = enemyXPositions.length; enemyYPositions.push(0); enemyXPositions.push(Math.random() * 400);
Argh!
All right, so maybe they're still coming in at too fast a rate...
Less Enemies Per Second, Please
Right now, the enemies are being created at a rate of one new enemy per tick - and since a tick is 25ms, there are 40 ticks per second, and therefore 40 new enemies per second.
Let's reduce this to something more manageable: about two enemies per second.
Since that's 1/20th of our current rate, we could achieve this by keeping track of how many ticks have passed, and creating a new one on tick number 20, tick number 40, tick number 60, and so on. But I think it'll be more fun if instead we make there be a 1/20 chance of a new enemy being creates on any given tick. This way, sometimes we'd create more than two new enemies in one second, and sometimes we'd create less, but it'd average out to two per second. The uncertainty involved would make the game a little more exciting (though perhaps "exciting" is a poor choice of words at this early stage of the game's development...).
How can we do this, then? Well, we know that Math.random()
create a random number between 0 and 1. Since these random numbers are evenly spread out between 0 and 1, that means there's a 1/20 change of the number generated being somewhere between 0 and... well, 1/20.
In other words, the chance of Math.random() being true is 1/20.
So, let's change our code to make use of this fact:
function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; var numberOfEnemies = enemyXPositions.length; if (Math.random() < 1/20) { enemyYPositions.push(0); enemyXPositions.push(Math.random() * 400); }
Much better! Feel free to experiment with that condition to find a value that works for you.
Wrapping Up
That's it for this part of the series. We've now built a rudimentary game - not a polished game, not a particularly fun game, but a game nonetheless.
If you'd like to challenge yourself before the next part, have a go at making these changes:
- Easy: The enemies 'pop' onto the top of the canvas; make them slide in, instead.
- Easy: Some of the enemies are created partially off the side of the canvas; stop this happening.
- Medium: The enemies all move at exactly the same speed, which is pretty dull; allow them to have different speeds.
- Hard: The avatar can move off the right edge of the canvas, making it impossible for any enemies to touch it (assuming you've completed the second easy challenge). Make sure the avatar stays inside the boundaries.
Enjoy!
Comments