HTML5 Avoider Game Tutorial: Multiple Moving Enemies

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:

HTML5 avoider game tutorial

(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:

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:

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:

Try it out here!

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:

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:

Try it out here!

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:

Try it out here!

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:

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:

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.

HTML5 avoider game tutorial

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(); in redrawAvatar() 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():

Try it out here!

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()?

Try it out here!

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):

Try it out here!

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 to setUpGame()
  • redrawAvatar() is run whenever the mouse moves, so let's rename it to handleMouseMovement()
  • 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 to handleTick()
  • 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:

(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:

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:

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.

Try it out here!

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:

Try it out here.

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:

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:

HTML5 avoider game tutorial
Click to try it out.

Simple!

You can put them at different heights, like so:

HTML5 avoider game tutorial
Click to try it out.

This is a bit messy, though. Instead, how about just creating an enemyY2 variable?

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().

HTML5 avoider game tutorial
Click to try it out.

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:

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:

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:

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:

This is because the while statement acts like a repeated if statement. Remember, if works like this:

"If condition is true, then run outcome."

while works like this:

"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:

...and put it into a loop:

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:

Now it's great.

Can we apply the same thinking to our other repetitive code? I'm referring to this:

Unfortunately, something like this won't work:

...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:

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:

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.

HTML5 avoider game tutorial
Click to try it out.

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:

HTML5 avoider game tutorial
Click to try it out.

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:

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:

Or, to be a bit neater:

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:

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:

HTML5 avoider game tutorial

Here, the avatar and the enemy are not overlapping. We have: avatarX

HTML5 avoider game tutorial

Here, the avatar and the enemy are overlapping. We have: avatarX

HTML5 avoider game tutorial

Still overlapping. We have: enemyX

HTML5 avoider game tutorial

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:

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?

Try it out here!

It does work! Now we should make it work for all the enemies - and of course we can use a loop to do that:

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.

Try it out here!

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:

HTML5 avoider game tutorial
Click to try it out.

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:

So what happens if we push the new values onto the arrays inside handleTick(), rather than inside setUpGame()?

HTML5 avoider game tutorial
Click to try it out.

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:

HTML5 avoider game tutorial
Click to try it out.

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:

HTML5 avoider game tutorial
Click to try it out.

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!

Tags:

Comments

Related Articles