In this tutorial (the first of a series), you'll learn the basics of HTML5 game development with JavaScript and the canvas
element. You don't need to have any programming experience, or even any HTML experience (apart from this one article). Let's get started!
Introduction
It would be difficult to have missed the "HTML5 vs Flash" articles that have sprung up all over the web, particularly since Steve Jobs's Thoughts on Flash last year, and Microsoft's announcement this week that Windows 8's web browser won't support Flash on tablets by default. I'm not going to get into that debate here; whatever your opinion, there's no harm in learning HTML5. Even if you know Flash, it doesn't hurt to have another tool in your kit.
This tutorial does not require you know know Flash, or to have had any experience of programming before. In fact, everything that you need to know before you get started is explained in this single article: Get Up to Speed With HTML. That's it! If you can follow that, you can follow this.
I'm basing this series on my Flash tutorial series, which in turn was based on an even older Flash tutorial by a guy named FrozenHaddock (to whom I am very grateful for letting me use his ideas). This isn't a direct port of either tutorial, however; I'll be completely rewriting the code and the explanations to suit HTML5.
A couple of notes:
- Cross-browser compatibility is a real and important issue in web development, but we're going to be a little selfish and focus on making sure our game works in exactly one browser: Chrome. Rest assured, we will deal with other browsers (including mobile) in other Activetuts+ tutorials, but for the time being, we'll stick with Chrome, so that we don't have to split our focus.
- Clean code is more important in HTML5 than in a lot of other platforms, because the underlying programming language (JavaScript) will let you get away with a lot. So, we're going to make sure that you get in the habit of writing decent code... eventually. We'll be a little messy at the start, just to get things rolling and avoid making you scroll through thousands of words on "best practices" before actually getting to do anything.
In this first part of the tutorial, we'll just be setting everything up and putting in some very basic game mechanics. Future parts will add multiple spawning enemies, high scores, menu screens, multiple levels, and all that stuff.
Enough talk - let's get started!
Setting Up
First thing to do is create a .html file. You can use a basic text editor for this, or spend a few hundred dollars on software specifically designed for HTML development; personally, I'd stick with free software for now. Here are three recommendations: TextEdit (for Mac), Notepad++ (for Windows), and Sublime Text 2 (for Windows, OS X, and Linux). Take your pick.
Create a new file, and enter the following:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> </head> <body> </body> </html>
If you don't understand what any of that does, read my basic guide to HTML.
Create a new folder on your hard drive called AvoiderGame, and save this HTML file inside it as game.html. If you load it right now, it'll just show a blank white page (as you know), so put a paragraph of text in there just to make sure everything's okay. I'll add a link to this tutorial, but you could enter anything you like - your name and website, perhaps?
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> </head> <body> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
JavaScript
Okay, now, you will not be surprised to hear that we will soon be writing some JavaScript - remember, JavaScript lets web pages do things, and that's exactly what we need for making games. We'll put all our JavaScript in an external file, in order to keep things tidy, and put this file in a separate folder, to keep things tidier still.
So, create a new folder, called js inside your AvoiderGame folder. Then, create a new, empty text file, and save it as main.js inside this new AvoiderGame/js/ folder.
Alter your HTML to refer to this JS file:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> </head> <body> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
Note that I haven't written src="http://active.tutsplus.com/...whatever.../js/main.js"
, or src="C:\AvoiderGame\js\main.js"
; this way, we're telling the HTML page, "look for a js folder in the same directory as you, and then use the main.js file that's inside it." It's called a relative path.
If you want to test that this is working, put alert("Working!");
in your JS file, then load the HTML page. If you get a dialog box, everything's okay.
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> </head> <body> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
CSS
While we're at it, let's link an external CSS file as well; we can use it to make the text look nicer, and we might need to use CSS in the game later.
Create a new folder inside AvoiderGame called css, and then create a new, empty text file called style.css inside that. Modify your HTML like so:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
I'm going to modify my CSS file to match some styles we often use on demo pages here at Activetuts+; feel free to copy it, come up with your own, or leave yours blank:
body { background: #ffffff; text-align: center; padding: 20px; color: #575757; font: 14px/21px Arial,Helvetica,sans-serif; } a { color: #B93F14; } a:hover { text-decoration: none; } ol { width: 600px; text-align: left; margin: 15px auto }
This tutorial isn't about CSS, so if you don't understand that, don't worry about it. (If you're curious, you can look up the meaning of those CSS attributes on W3Schools.com.)
Okay, that's the dull setup out of the way. You can see how the page looks by clicking here, and you can download the entire source so far in a ZIP file here. Let's create our avatar!
Get Your Head in the Game
We need an image that will represent our player's character in this game. Use whatever you like - a photograph of your face, your Twitter avatar, a picture you've drawn - but make sure it has a transparent background, that it's roughly circular, and that it's about 30x30px.
The original tutorial on which this one is based used a skull. I'm not sure why, but I suspect it was an attempt to subvert games' typical anti-skeleton stance; after all, under our skin, doesn't every one of us have a skull?
I'm not one to break with tradition, so I'll use a skull here too. You can download mine by right-clicking it, if you don't want to make your own:
And before you ask: yes, I am available for commission.
Whatever you choose, save it as avatar.png inside a new folder, called img inside AvoiderGame. Your folder structure now looks like this:
/AvoiderGame/ game.html /css/ style.css /js/ main.js /img/ avatar.png
So how do we get this into our game? If you've been paying attention, you'll probably suggest this:
<img src="img/avatar.png" alt="Avatar" />
And, it's true, that would put the avatar in our page! But it's not what we're going to use.
Canvas
An img
element shows a single image, loaded from a PNG or JPG (or whatever) file. The canvas
tag, new to HTML5, can generate an image dynamically, made up of other, smaller images, text, primitive shapes, and so much more, if you desire. Its contents can be modified at any point, so you can give the illusion of motion - or of interaction, if you make the contents change according to what the user does.
We create a canvas in the same way that we create any other HTML element:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
...though, if you look at this, you won't be able to see anything there. It's invisible, so the only effect it has is to move the text down a little.
With CSS, we can give it an outline so that we can tell it apart from the background. Add this to your CSS:
canvas { border: 1px solid black; }
Check it out. It's kinda small, though; let's make it 400px by 300px (old-school TV dimensions):
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas width="400" height="300"></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
That's better. Now, I said we could add images to the canvas dynamically, so let's do that next.
Functions
Remember in the HTML guide I showed you how to make things happen when you clicked HTML elements? Here's a quick recap:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas onclick="alert('Clicked the canvas');" width="400" height="300"></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
If you click the canvas, you'll get a dialog box message. This is because alert()
is a JavaScript function: it's a shortcut to a few lines of code. We can write our own functions in our JS file. Open main.js and enter the following:
function alertSeveralTimes() { alert("Hello!"); alert("Look, we can run several messages in a row."); alert("Annoyed yet?"); }
(Delete the original alert("Working!");
if you haven't already.)
Do you see how this works? We have created a new function
called alertSeveralTimes()
, whose contents are inside the curly braces ({
and }
). When we tell the browser to run this alertSeveralTimes()
function, it will run each of the alerts in turn, one after the other.
Let's try it out:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas onclick="alertSeveralTimes();" width="400" height="300"></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
Try it! We've effectively bundled up several alert()
functions into one bigger function called alertSeveralTimes()
, and told it to run whenever the canvas is clicked.
You might be wondering why the alert("Working!")
ran as soon as we opened the page, but the alertSeveralTimes()
didn't, even though they were both in the same place (at the top of main.js). It's because of that magic keyword function
; when the browser sees this, it doesn't think, "aha, this is some code I must run immediately!", it thinks, "aha, this is some code I must bundle up into a new function, which I can run later!"
Anyway. Now let's make our function do something to the canvas. Making it load an image is a little tricky, so we'll start with something a bit simpler: changing its size.
Modifying the Canvas
One of the most amazing features of JavaScript is its ability to change the HTML of the page. Check this out; modify your JS file so that it contains this:
function changeCanvasSize() { gameCanvas.width = 600; gameCanvas.height = 800; }
You can probably guess what this is doing: it takes the canvas element, and modifies its width
and height
attributes (we don't need to use quotes around numbers in JavaScript, unlike in HTML attributes).
Except... how does it know that gameCanvas
refers to the canvas that we have in our page?
Well, it doesn't... yet. We have to make it realise that.
First, we have to give the canvas element an id
(short for "identification") attribute; this is just a name that we use so that we can refer to it in JavaScript later:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas id="gameCanvas" onclick="alertSeveralTimes();" width="400" height="300"></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
While we're at it, let's make the onclick
attribute point to our new changeCanvasSize()
function:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas id="gameCanvas" onclick="changeCanvasSize();" width="400" height="300"></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
This still isn't quite enough. We have to let the JavaScript know that it's dealing with an element from the HTML page (or 'HTML document', as it's more correctly known):
function changeCanvasSize() { document.getElementById("gameCanvas").width = 600; document.getElementById("gameCanvas").height = 800; }
Now, I know, this doesn't seem entirely logical. Why is gameCanvas
suddenly in quotes? Why do we use document.getElementById("gameCanvas")
instead of just, say, getDocumentElement("gameCanvas")
, or even document.gameCanvas
? I promise, this will all become clear during the tutorial series, but for now, just go with it, please.
Test out your new code. The canvas resizes itself as soon as you click on it. Awesome!
Now, I should warn you: programmers are lazy. We hate writing the same code over and over again, and if there's any way we can reduce the typing required, we'll take it. So, let me introduce you to a nice shorthand way of referring to the canvas:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); gameCanvas.width = 600; gameCanvas.height = 800; }
See how that works? Just as the function
keyword says, "hey, wrap all this code up under the name changeCanvasSize()
, please", the var
keyword says, "hey, use the word gameCanvas
to refer to the HTML element with an ID of "gameCanvas", please". Then (in lines 3 and 4, above), we can use this new shorthand gameCanvas
in please of the longer document.getElementById("gameCanvas")
- because they refer to the same thing.
That's important: we haven't created a new canvas; we've just made gameCanvas
refer to the existing canvas element.
However, it is possible to use var
to create something new...
Click to Skull
Like I said, we're moving towards adding an image to the (currently empty) canvas. But before we can do that, we have to load the image. And before we can do that, we have to have something to load the image into.
Modify your JS like so:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); gameCanvas.width = 600; gameCanvas.height = 800; }
(I've added a blank line to clearly separate the var
statements from the rest.)
Here, we're using the var
keyword again - but this time, it says something subtly different: "hey, create a new Image
object and use the word avatarImage
to refer to it from now on, please." The Image
object is basically like a img
element; the crucial difference here is, it's not in the HTML. We've created this brand new element, but it's nowhere in the HTML; it's just floating around in the JavaScript aether. I find that a bit weird.
Just like an img
element in the page, this Image
is pretty much useless without setting its src
, so do that next:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); gameCanvas.width = 600; gameCanvas.height = 800; avatarImage.src = "img/avatar.png"; }
(Once again I'm using a blank line to keep bits of code that do different things separated from each other (like paragraphs in text), and once again I'm using a relative path to refer to a file's location.)
So this is now loading the image, but you'll have to take my word for it at the moment, since it's still out there in the aether where we can't see it. However, we can check its other attributes:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); gameCanvas.width = 600; gameCanvas.height = 800; avatarImage.src = "img/avatar.png"; alert(avatarImage.width); }
We're telling it to show us a dialog box containing the value of the width
attribute of our image. Check it on your code and see what you get; I get 29, which is exactly right.
With just one more line of code, we can draw the avatar on the canvas:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); gameCanvas.width = 600; gameCanvas.height = 800; avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, 0, 0); }
Let's break this down:
-
gameCanvas.getContext("2d")
: We don't actually draw directly on the canvas, we draw onto what's called a "drawing context"; this lets us determine whether we want to draw in 2D or 3D. Okay, there's no 3D context at the moment, but this is letting us plan for the future. -
drawImage()
: Pretty straightforward. This is a function that lets us draw an image onto the context of a canvas. -
avatarImage
: This is the image object we've got floating around in the aether, remember? -
0, 0
: These are the coordinates at which we want to draw the image. In school, you're taught that (0, 0) is the bottom-left of the page; on a computer, it's the top-left (the x-axis points to the right, and the y-axis points downwards).
Take a look. It works! (If it doesn't, remember that you should be viewing this in Chrome; I don't guarantee that this will work in any other browser.)
Multiple Skulls
The drawImage()
function works like a potato stamp:
It just takes the contents of the image object and clones them onto the canvas; of course, we're dealing with pixels, not paint, but you get the idea.
This means we can add multiple skulls to the canvas, like so:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); gameCanvas.width = 600; gameCanvas.height = 800; avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, 0, 0); gameCanvas.getContext("2d").drawImage(avatarImage, 100, 50); gameCanvas.getContext("2d").drawImage(avatarImage, 200, 130); gameCanvas.getContext("2d").drawImage(avatarImage, 300, 270); }
Check it out, skull party. We can also make the skull appear in a random place each time:
function changeCanvasSize() { var gameCanvas = document.getElementById("gameCanvas"); var avatarImage = new Image(); gameCanvas.width = 600; gameCanvas.height = 800; avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100); }
Math.random()
give you a random number between 0 and 1, so Math.random() * 100
gives you a random number between 0 and 100; this means that the coordinates of the new skull are anywhere between (0, 0) and (100, 100). Take a look!
But hold on - why is there only one skull at a time now? Is it something to do with it being a new function? Does the canvas get cleared every time you run a function?
No. The canvas is cleared every time you modify its height or width, even if you don't change either. So, if we change our JS like so:
function changeCanvasSize() { 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); }
...then we can keep adding new skulls.
In fact, let's change the name of the function to drawAvatar()
, and tidy things up a bit:
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); }
Don't forget to change the onclick
attribute of canvas
in your HTML:
<!DOCTYPE html> <html> <head> <title>HTML5 Avoider Game</title> <script src="js/main.js"></script> <link rel="stylesheet" href="css/style.css" /> </head> <body> <canvas id="gameCanvas" onclick="drawAvatar();" width="400" height="300"></canvas> <p>From <a href="http://active.tutsplus.com/tutorials/html5/learn-html5-with-this-simple-avoider-game-tutorial" rel="external">Learn HTML5 With This Simple Avoider Game Tutorial</a>.</p> </body> </html>
Okay, now let's get that avatar moving.
Adding Interactivity
I want to make the avatar follow the mouse. We can use the same principle that animators do: if we keep erasing the contents of the canvas, and then re-drawing the avatar at a different position, the avatar will appear to move. So all we have to do is keep redrawing the avatar at the mouse's coordinates, and we're set!
How do we do that, though?
A Grand Event
Judging by what we've done so far, you might guess that we'd add a onmousemove
event attribute to the canvas (which would be triggered every time the user moved their mouse), then make it run drawAvatar()
, but specifically at the mouse's current coordinates. This is inspired, but unfortunately doesn't really work, simply because it doesn't offer an easy way to obtain the mouse's coordinates.
However, it is very close to what we want to do! Take a look at this:
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); }
This does roughly the same thing as the above suggestion; the redrawAvatar()
function (which we haven't written yet) will be called whenever the mouse moves over the canvas. But there's one big difference.
Notice how we write redrawAvatar
, rather than redrawAvatar()
in the code above, whereas in our HTML page, we put drawAvatar()
- with "parentheses" (round brackets) - in the onclick
event attribute of our canvas. The full reason for that is a little complicated to go into now (though you'll understand by the end of the series), but it has one very important upshot: it allows us to obtain the mouse's coordinates.
When the mouse moves, the browser creates a new object - just like when we created a new Image in our JavaScript earlier. This object has certain attributes that have something to do with the thing that triggered its creation; in this case, because the mouse moved, it contains the coordinates of the mouse. Brilliant!
So how do we access it? Well, this new object (which is called a MouseEvent
, for reasons that you might be able to guess) gets passed to the redrawAvatar()
function. Er, but we haven't written that yet, so let's do that now. Add this code to your JS file:
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) { }
Aha - this time, the way we define the function is a little different: we've added the word mouseEvent
in-between those parentheses. This is because we are expecting the browser to pass a MouseEvent
object to our new function, just like when we passed the coordinates to the drawImage()
function.
Since we've given it a name, we can access the attributes of this new object:
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) { alert(mouseEvent.x); }
Test this out; you'll have to click the canvas before anything will happen, because it's inside that function that we make the browser start paying attention to mouse movements.
You'll notice that the dialog box only appears when the mouse moves over the canvas element. You might also notice something odd about the number: it's too big! I'm getting numbers of over 900, even though the width of the canvas is only 400.
This is because mouseEvent.x
gives the mouse's x-position from the edge of the page, rather than the edge of the canvas. We can use mouseEvent.offsetX
to get the mouse's x-position from the edge of the canvas:
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) { alert(mouseEvent.offsetX); }
So, to recap: addEventListener()
makes the browser listen for certain events - such as the movement of the mouse - and then run a function when this event is "heard". The browser creates a new object (like a MouseEvent
), and passes it to that function.
It's a little hard to wrap your head around, but don't worry; we'll be using it a lot, so you'll get the hang of it!
Move Your Head
We've nearly got movement. In fact, I recommend you have a go at making the avatar follow the mouse on your own before reading further. You'll probably come very close!
There's one big thing that'll trip you up, though: the word var
- which, you'll remember, you can use to set a shorthand - only "lasts" within the function in which it was defined.
This means that if you try to do, say:
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) { gameCanvas.width = 400; alert(mouseEvent.offsetX); }
...it won't work, because gameCanvas
means nothing outside of drawAvatar()
!
So, if you didn't get it the first time, have another go.
My code is here if you want to check yours:
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(); avatarImage.src = "img/avatar.png"; gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); }
Tada! Oh, wait, dang, I forgot to erase the canvas by changing its width or height. Cool effect, but let's try that again:
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(); avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); }
Try it out now. Success!
Wait, What Was That For?
When you take another look at your code in a few days' time, you're likely to have forgotten what a lot of it is for. In particular, I suspect you'll forget why you need to resize the canvas.
Fortunately, there's a way to remind yourself: comments.
Look at this:
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(); avatarImage.src = "img/avatar.png"; gameCanvas.width = 400; //this erases the contents of the canvas gameCanvas.getContext("2d").drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY); }
The browser ignores everything on the line after a //
. This means you can write whatever you like there, and it won't be run as code. It's called an inline comment and is very important. To help get yourself into the habit of commenting your code, go through it now and write comments after each line that you think you might have trouble understanding after a few days' break.
Commenting may seem like a waste of time. I think a lot of new programmers assume that when they first start out; it only takes one bad experience with trying to update your own, uncommented code (or worse: someone else's!) to convince you of its worth, though :)
Hide the Mouse
At the moment, you've got a big stupid mouse cursor hovering over your avatar:
We can fix this with a little CSS. Modify your stylesheet like so:
canvas { border: 1px solid black; cursor: none; }
In most browsers, this will make your cursor disappear when it's on top of the canvas... but not in Chrome.
(Update 18/11/2010: Chrome now supports this! I'll leave the rest of the text here anyway, or things will get very confusing.)
Chrome doesn't support cursor: none;
, but it does allow you to replace the cursor with a PNG file of your choice. So, you can create a PNG that's completely transparent, put it in your img folder, and use that, like so:
canvas { border: 1px solid black; cursor: url("../img/transparentcursor.png"), none; }
(I've had to put ../ at the start of the URL because, in CSS, relative paths are relative to the folder of the CSS file, not the HTML file, and ".." says, "the folder above this one". Also, I've put , none
after this, because it means that if any browsers don't support using PNGs for cursors, they'll use the none
attribute instead. Can you see why I wanted to avoid focusing on cross-browser compatibility?)
Unfortunately, this doesn't work either, because if you use a completely transparent PNG, Chrome just shows a solid black rectangle instead. Thanks, Chrome.
So, instead, I've made a 1x1px PNG that's almost transparent (the single pixel is white, with an opacity of 1%). You can download it here. Copy it to your img folder, then modify your CSS stylesheet:
canvas { border: 1px solid black; cursor: url("../img/almosttransparent.png"), none; }
Test it out. It does work, after all that effort.
Make an Enemy
We've accomplished a lot so far, but our avoider game still doesn't have anything to avoid! The last thing we'll do, in this part of the series, is create an enemy.
We need an image to represent this. Draw whatever you like, but make sure that it's roughly circular, and about 30x30px. I'm going to take my cue from FrozenHaddock again. He picked a smiley face for his game's enemy; I'm not sure why, but I suspect it was a comment on the over-pervasiveness of smileys in modern conversation; a yearning for the days of emotions over emoticons, where poets would pour their hearts into a single sentence of text, rather than simply typing semicolon close-parenthesis ell oh ell. Or maybe it's just because smiley faces are easier to draw. Regardless, here's mine:
Call yours enemy.png and put it in the img directory.
You can probably figure out how to draw this enemy (unmoving) on the canvas - if so, give it a shot! Once again, I recommend you do this before reading on.
Here's my solution:
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); gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150); }
Your method might be different to mine - that's okay, as long as it works! But for consistency, please copy my method into your code.
Note that I keep the var
statements at the top of their respective functions. This isn't strictly necessary, but it keeps things tidy, so I recommend it.
Besides that, nothing here should surprise you. There's no particular significance to the coordinates (250, 150) that I've chosen.
Putting the "Avoid" in "Avoider Game"
We're not going to worry about making the enemy move in this part of the tutorial; that topic deserves more space than I can afford it here. But we will check for a collision between the avatar and the enemy!
First, we'll do something simpler: we'll consider a certain area of the screen "off-limits" and pop up a dialog box if the avatar moves into that area.
Modify your redrawAvatar()
function like so:
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); gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150); if (mouseEvent.offsetX < 100) { alert("Too far left!"); } }
Try it out. If your avatar goes too far to the left side of the screen, the dialog box appears.
Let's take a closer look at the code:
if (condition) { outcome; }
The if
statement is a way of checking whether something has happened. It's made up of two parts: a condition, inside the parentheses, and a result, inside the curly braces. If the condition is true, then the outcome is called.
In our case, the condition is mouseEvent.offsetX . The
<
symbol means "less than", and remember that mouseEvent.offsetX
is the cursor's horizontal distance from the left edge of the canvas. So, this is checking whether the cursor is within 100 pixels of the left edge of the canvas. If it is, then...
...the outcome is run. And in our case, the outcome is alert("Too far left!");
, the dialog box function we've used a number of times.
Make sense? Okay, good, because I'm going to make it more complicated:
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); gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150); if (mouseEvent.offsetX < 100 || mouseEvent.offsetY < 100) { alert("Too far left, or too far up!"); } }
In this code, we've introduced a new operator: ||
. ||
means (and is pronounced) "or". The if
statement therefore reads:
"If the mouse is within 100 pixels of the left edge of the canvas, OR the mouse is within 100 pixels of the top edge of the canvas, show the dialog box."
Try this out, and you'll see that you can't get anywhere near the top or the left of the canvas.
How about this:
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); gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150); if (mouseEvent.offsetX > 150 && mouseEvent.offsetX < 250) { alert("Stay out of the middle!"); } }
The &&
operator means (and is pronounced) "and", and >
means "greater than". So, our if
statement now reads:
"If the mouse is more than 150 pixels away from the left edge of the canvas AND the mouse is less than 250 pixels away from the left edge of the canvas, show the dialog box."
Test it out, and you'll see that we effectively have an invisible "stripe" down the middle of the canvas where we can't put our mouse.
We're not restricted to just using two clauses at once; check this out:
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); gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150); if (mouseEvent.offsetX > 150 && mouseEvent.offsetX < 250 && mouseEvent.offsetY > 100 && mouseEvent.offsetY < 200) { alert("Stay out of the center!"); } }
We've now effectively drawn a 100x100px box, in the centre of the canvas, where we can't put our mouse.
Can you see where we're going with this?
Rather than a 100x100px box in the middle of the canvas, we should use a 30x30px box positioned where our enemy is.
This is the last thing we're going to do in this part of the tutorial, so once again I recommend you have a go yourself. It's pretty fiddly - you'll probably want to get some paper out to draw the avatar and the enemy and label some coordinates - but you can do it if you use what you've learned so far.
My solution is below.
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); gameCanvas.getContext("2d").drawImage(enemyImage, 250, 150); //my avatar is 30px wide and the enemy is at x=250, so I have to check whether mouseEvent.offsetX is within 30px //either side of x=250 (i.e., from 220 to 280) //similarly, since my avatar is 33px tall, I have to check whether mouseEvent.offsetX is within 33px ABOVE y=150 //but since enemy is only 30px tall, I also check whether mouseEvent.offsetX is within 30px BELOW y=150 //therefore, I check from (117 to 180) if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) { alert("You hit the enemy!"); } }
Next Time
That's it for the first part of the tutorial. In the next part, we'll get that enemy moving, and we'll add more of them, so that this can start to be a real game!
In the meantime, why not experiment with what you've learned? You could try adding multiple (unmoving) enemies yourself, perhaps with different graphics. Or you could add several avatars on screen at once, all following the mouse in different ways. What happens if you use something like mouseEvent.offsetX + 100
or 300 - mouseEvent.offsetY
in your call to gameCanvas.getContext("2d").drawImage()
?
I hope you've enjoyed this so far. If anything's not clear, please ask about it in the comments :)
(One other quick note: at the moment, your code should work just fine on your own computer, but won't work if uploaded to the web. My demos work online, because I did a sneaky trick. Don't worry, I'll explain how to solve this in a future part of the tutorial.)
Comments