HTML5 Avoider Game Tutorial: Keeping Score

So far, we've got a never-ending stream of enemies that our avatar must avoid; one touch, and it's game over. But so what? Since there's no way to track the player's progress, they have no idea whether they did better in their latest round than they ever did before. In this tutorial, you'll learn how to keep score, how to display it on the canvas, and how to let the player know when they've beaten their own records.


Refresher

In the first and second parts of this tutorial, we've covered a number of concepts: drawing images to the canvas, detecting mouse actions, using if and while statements, storing variables in arrays, and the idea of variable scope.

You can download the source files from the series up to this point if you'd like to dive straight in here, though I recommend reading all parts in order.

Our game's JavaScript file initialises a bunch of variables (including two arrays to store enemy x- and y-coordinates) outside of all functions, so that their contents are available to all functions. It also contains a function called setUpGame() which is run when the player first clicks the canvas; this loads the images, starts listening for any mouse movements, and sets up a "tick" function to be run every 25 milliseconds.

When the mouse is moved, we move the position of the avatar, as stored in two variables - one for the x-coordinate and one for the y-coordinate - but we don't immediately redraw the avatar's image at this new location; all redrawing is handled in the tick function.

The tick function does four things:

  • There's a one-in-twenty chance that it'll add a new enemy by pushing new x- and y-coordinates onto the relevant arrays.
  • It increments the y-coordinates of each enemy by looping through that array.
  • It redraws the avatar and enemy images according to their current coordinates.
  • It checks for a collision between the avatar and each enemy, giving an alert if one occurs.

All clear?


Warm Up Challenge

If it's been a while since you read the second 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 series so far.

Easy

Swap the avatar and enemy images around so that the player controls a smiley face that's avoiding the falling skulls.

(How many different ways can you figure out how to do this? I can think of three, off the top of my head.)

Medium

At the moment, there's a flat one-in-twenty chance that a new enemy will be created in any given tick. I want you to make it so that there's a one-in-one chance on the first tick, a one-in-two chance on the second, a one-in-three chance on the third, and so on.

To make this more challenging, do it in reverse: a one-in-1,000 chance on the first tick, a one-in-999 chance on the second, a one-in-998 chance on the third, and so on. (After the thousandth tick, make it a steady one-in-one chance.)

Hard

Rather than waiting for the enemies to appear one by one, have the game start with twenty enemies already on screen.

To make this more challenging, make them spread out around the canvas, and don't allow them to overlap the avatar or each other.


Keeping Time

What's the simplest way to measure how well the player is doing this round? The easiest thing I can think of is to keep track of how long it's been since they hit an enemy. And since hitting an enemy means game over, we only need to keep track of how long they've been playing.

To do this, we'll just create a variable, set it to 0 when the round starts, and increment it every tick. Let's call this variable ticksSurvived. And think: since we need to access it over and over again, it needs to be defined outside of all the functions, at the top of the JS file:

Now, we'll give handleTick() yet another task to do: increment ticksSurvived. Put this after the collision detection; after all, if the avatar hits an enemy, it hasn't actually survived the tick:

To display this, for now, we'll just alter the alert that appears when the avatar hits an enemy:

That's a bit weird; we've used the + operator to add a string to a number. We can add another string to the end:

Maybe this doesn't seem odd to you, but some programming languages hate this. JavaScript doesn't care. It knows that "some" + "string" equals "somestring", and assumes that you want to treat ticksSurvived as a string in this situation.

(Some things to try out:

12 + 6
"12" + "6"
"12" + 6
12 + "6"

Do they all do what you expect them to do?)

Anyway, let's give this new code a try.

HTML5 avoider game tutorial
Click to try it out.

Great!

As before, when you click OK, the alert box appears again, because all it does is pause the ticks rather than actually stopping them. But note that the number in the box increases by one; this is proof that JavaScript doesn't turn the number into a string permanently; it only uses it as a string for the purposes of adding it to another string.

While we're on the subject of this alert box: don't you find it annoying?


Try Again

At the moment, the only way to start a new round is to refresh the page. Let's make it easier and less irritating to have another go, by making the "OK" button on the alert box reset the game.

Since the alert effectively pauses the game, whatever we put on the line after the alert will run once the player hits OK. Let's make it call a new function, startNewGame():

So what do we need startNewGame() to do?

Perhaps we should just call setUpGame() again - but no, nothing in that needs doing twice: we don't need to load the images or add the mouse event listener or set up the ticks again.

Have a think about what would be needed, and try out your ideas. My solution is below; take a look when you're ready.

That's all. You can do more if you wish, but this is the minimum required.

Note that we don't have to do anything with the canvas - we don't need to clear it, or draw anything to it, or manipulate the images - because this is all done in handleTick(). We don't have to go through all the elements in the arrays, one by one, either, because the arrays are simply a list of coordinates that we use to stamp the enemy images onto the canvas; the enemies don't exist as actual objects.

HTML5 avoider game tutorial
Click to try it out.

So this is great - the player can play the game over and over again to keep trying for a better score. Except... how do they know whether they've beaten their previous score? At the moment, they just have to remember they top score so far, or write it down. We can do better than that.


Remember the Best Score

How should we store the top score? In another variable, of course!

Naturally, it has to live outside of the functions; by now, I'm sure you understand why.

To begin with, we set it to 0, of course - the player hasn't even completed a round. When the avatar hits an enemy, let's update the new score as required:

Let's also tell the player that they beat their old high score:

Take a look:

HTML5 avoider game tutorial
Click to try it out.

Excellent. Now let's give the player a little more information on how much better they did:

Note here that ticksSurvived and mostTicksSurvived are treated as numbers when subtracting one from the other, but the resulting expression (ticksSurvived - mostTicksSurvived) is treated as a string when added to the other strings! This can get really confusing, if you're not careful.

HTML5 avoider game tutorial
Click to try it out.

All right, so now the player knows how well they were doing, after they get game over, we should give them an indication of how well they are doing, while they're still playing.


Drawing the Score to the Canvas

It's really easy to write text into a canvas - and no, we don't need to piece it together from different images of different letters!

Remember that to draw an image to the canvas we call canvas.getContext("2d").drawImage(). Writing text is very similar: we call canvas.getContext("2d").fillText().

We must pass three arguments to fillText():

  • A string containing the text to write.
  • The x-coordinate at which to write it.
  • The y-coordinate at which to write it.

There's an optional fourth argument:

  • The maximum width that the text is allowed to take up on screen.

...but we won't worry about that for now.

To test this out, head to handleTick(), find the line where we draw the avatar to the canvas, and draw a sample line of text just afterwards:

Try it out:

HTML5 avoider game tutorial
Click to try it out.

Note that, since this gets drawn after the avatar and before the enemies, the enemies get drawn on top of it, while the avatar goes underneath:

HTML5 avoider game tutorial
Click to try it out.

Like I say, the first argument required is a string, and - as we've seen - we can construct a string by adding a string to a number. So, this should work:

...and indeed it does!

HTML5 avoider game tutorial
Click to try it out.

But it's a bit of a mess, floating there underneath the enemies with that tiny font. Let's sort that out.


Tidy Up the Text

First, let's move the text so that the enemies move underneath it. This just means drawing it after all of the enemies are drawn, so move the relevant line down:

Now, let's move it to the top-left of the screen. That's, (0, 0) so this should work, right?

Hmm. Nope. We can't see the score at all. This is because it's using (0, 0) to place the bottom-left corner of the text. We need to move the text down a bit, then:

Check it out:

HTML5 avoider game tutorial
Click to try it out.

Okay, so far so good. Now, let's change the font itself. We do this by setting the canvas's context's font property to a CSS string representing the font. If you're not familiar with CSS, don't worry; at this stage, all you need to know is that it contains the font's size and the fontface:

("px" stands for "pixels".)

Note that we have to set the font before drawing the text!

Take a look:

HTML5 avoider game tutorial
Click to try it out.

It works - but the Impact font is really hard to read at that size. Let's make it bigger:

HTML5 avoider game tutorial
Click to try it out.

Unfortunately, this doesn't work so well, because the font is still 10px from the top of the canvas, but now it's 16px tall!

We could keep changing the y-position of the font to fix this, but there's an alternative: we'll make it so that the position we specify determines the top-left corner of the text, rather than the bottom-left corner. It's easy:

Of course, now, there's a 10px gap between the top of the text and the top of the canvas:

HTML5 avoider game tutorial
Click to try it out.

...but that's easy to fix now:

Well, actually, I like a bit of padding, so let's do this:

HTML5 avoider game tutorial
Click to try it out.

Much better.

Different Fonts

Perhaps you're wondering whether there's a list of fonts we can use. Well, yes and no. See, if you pick a font that the player doesn't have installed on their computer, then the text will just be displayed in the default font. Remember, JavaScript is drawing the text on the fly, using the player's computer's resources.

We're fine using Impact, because it's installed on every computer - but does that mean we can't use any font other than the few in this list?

Fortunately, no. We can use any font we like, as long as we give the user access to it somehow. And for this, we're going to use that CSS file - you know, the one we haven't touched since the start of the series.

Suppose we have a font called "Really-Awesome". This will exist on your computer, somewhere, as a file - probably a .TFF file ("True Type Font"). Let's suppose that file is called ReallyAwesomeFont.ttf.

Now suppose you upload this font to your website - reallyawesomewebsite.com - so that there's a direct URL to it: http://reallyawesomewebsite.com/fonts/ReallyAwesomeFont.ttf.

You can then let the player's browser know about it by adding this to your CSS file:

With this line in your CSS, you can alter your code like so:

...and it'll work, because their browser will look up the "Really-Awesome" font in the stylesheet, and find the URL to the TTF. Great!

I'm not going to demonstrate this because I don't own the redistribution rights to any fonts; if I upload some and give you the link to the TTF as part of this tutorial, that's not really fair. But there is an alternative...

Google Web Fonts

Google has assembled a large collection of fonts that you can use in your project in your CSS, using a similar principle as above. Take a look at the collection.

There are a few criteria by which you can search for fonts, and you can enter some sample text to see how it'll be displayed:

HTML5 avoider game tutorial
Google Web Fonts.

I'm going to choose Iceland. When I click Quick Use, it gives me this HTML:

If you load http://fonts.googleapis.com/css?family=Iceland in your browser, you'll see that it's the same kind of thing as we wrote from scratch before:

It has a few more details, and the font is in WOFF format rather than TTF, but you get the idea.

You can use whichever font you like (and it doesn't have to be a Google Web Font), but for the purpose of this tutorial I'm going to assume that you're using Iceland. So, edit game.html and paste the font reference into it:

Now, back in main.js, change the font from Impact to Iceland:

Take a look:

HTML5 avoider game tutorial
Click to try it out.

Cool!

Challenge: Show the Best Score

Now that the current score is on screen at all times, it's only natural that the player will want to see what they're trying to beat.

Using what you've learnt, draw their best score at the top-right of the canvas. This is a little trickier than it seems: you'll have to decide what to do (if anything) when the current score overtakes the current top score!


Saving Scores Between Sessions

You've probably noticed by now that the high score is reset when you reload the page. This makes sense - after all, we run this line right at the start:

...but even without that line, the high score still wouldn't persist. All variables get reset and unassigned when you leave the page.

However, there is an alternative: every browser sets aside 5MB of storage for each web site domain. You can store any string you like in this 5MB storage, and it'll stay there even if the user closes their browser and restarts their computer.

It's called local storage, and it's really easy to use! To save something to it, you just need to give it two strings:

  • a name for the item, and
  • the value of the item.

To retrieve it, you just need the name that you originally used.

A Quick Example

Here's a quick example: let's add something to the local storage right at the start of the setUpGame() function:

Save the file, and load your game. Then close the tab.

Now, edit your JS file again, delete the line you just added, and replace it with a line that should retrieve the item:

So, just to be clear, there is now nothing in the code that sets the value of "exampleItem".

When you load the game this time, you should see an alert with "This is a great example." - proof that the string has been saved between sessions.

We can remove this item from the local storage using localStorage.removeItem():

I recommend you do this now, and then delete the line entirely.

You can also clear everything in the local storage at once, using localStorage.clear() (no arguments required). Okay - not quite everything. Your page can only affect the local storage space assigned to the domain on which it is hosted; I can't clear your local storage for google.com from a page hosted at tutsplus.com, and vice-versa.

Saving the Best Score

Now that we've seen an example, let's put it into practice.

Whenever a new high score is set, let's save it to the local storage. It only gets set in one place: when the avatar collides with an enemy and the current score is higher than the best score. So, add the call to localStorage.setItem() in the appropriate place:

When the page is first loaded, we should check to see whether there already is a high score saved in the local storage, and assign mostTicksSurvived to that value, if so. (You don't need to repeat this check once the page is loaded, unless you're worried about the user playing the game in two separate tabs at once.)

How do we check whether a value exists? All we have to do is put it inside an if condition:

The alert() above never gets called because localStorage.getItem("thisItemDoesNotExist") does not exist. Easy, right? So at the start of setUpGame(), we can just write:

Have a go. Load the page, set a high score, then beat it. Reload the page, then get a lower score - does it tell you you beat your old score?

HTML5 avoider game tutorial
Click to try it out.

Great! By the way, if you took the challenge earlier then you'll be able to see your previous best score in the top-right corner of the canvas, and this will now carry over from session to session.


Sidenote: Strong and Weak Typing

We're about done for this part of the series, but I just want to point out that, once again, we've been treating a string as a number and a number as a string: local storage only saves string values, yet we save a number to it when we save the best score, and use the value we retrieve from it as a number later on.

This is acceptable, because JavaScript is what's called "weakly-typed". Other programming languages are "strongly-typed", which means that if you say that something is a number, then it stays a number; if you say something is a string, then it stays a string.

In a strongly-typed language, if you want to add the string "Score: " to the number 32, then you have to explicitly tell the language to treat 32 as a string, perhaps like so:

"Score: " + (32 as String)

Also, in a strongly-typed language, when you define a variable, you also specify what type it is:

But JavaScript doesn't worry about these things. This doesn't make it better or worse than a strongly-typed language, just different.


Wrapping Up

That's it for this part of the tutorial. Now your game has both a game over condition and a means of keeping score. Plus, you learned about drawing text to a canvas, choosing fonts, and using the local storage.

If you'd like to challenge yourself before the next part, have a go at making these changes:

  • Easy: Draw the best score on the canvas, before the player clicks it.
  • Medium: Store the player's top five scores.
  • Hard: Save the positions of all the enemies to local storage, and restore them in the next session (you may have to do extra research here! Hint: look into JSON.)

Enjoy!

Tags:

Comments

Related Articles