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.pngto 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.pngto the canvas, at the mouse’s position. - Draws a copy of
enemy.pngto 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 < 5" 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 < avatarX + 30 < enemyX < enemyX + 30

Here, the avatar and the enemy are overlapping. We have: avatarX < enemyX < avatarX + 30 < enemyX + 30

Still overlapping. We have: enemyX < avatarX < enemyX + 30 < avatarX + 30

No longer overlapping. We have: enemyX < enemyX + 30 < avatarX < avatarX + 30
Taking all these together, we can see that the enemy and avatar are overlapping horizontally if:
avatarX < enemyX and enemyX < avatarX + 30
...or:
enemyX < avatarX and avatarX < enemyX + 30
In other words, for horizontal overlap, this condition must be true:
(avatarX < enemyX && enemyX < avatarX + 30) || (enemyX < avatarX && avatarX < enemyX + 30)
(Remember, && means "and"; || means "or".)
It's not hard to come up with a similar condition for vertical overlap:
(avatarY < enemyY && enemyY < avatarY + 33) || (enemyY < avatarY && avatarY < enemyY + 30)
(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 < enemyX && enemyX < avatarX + 30) || (enemyX < avatarX && avatarX < enemyX + 30) ) && ( (avatarY < enemyY && enemyY < avatarY + 33) || (enemyY < avatarY && avatarY < enemyY + 30) )
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() < 1/20 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!
View full post on Activetuts+









In the first part of this series, you learned the basics of using JavaScript and the
canvaselement 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
ifstatement. 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 calledmain.js, and it does two things:avatar.pngto the canvas.redrawAvatar(), whenever the mouse moves over the canvas.The
redrawAvatar()function is also insidemain.js; unlikedrawAvatar()it accepts a parameter – calledmouseEvent– which is automatically passed to it by the event listener. ThismouseEventcontains information about the mouse’s position. The function does four things:avatar.pngto the canvas, at the mouse’s position.enemy.pngto the canvas, at a specified position.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);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:
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
enemyYto0at 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 timeredrawAvatar()is run.However, there’s a flaw in my logic. The line
var enemyY = 0;will reset theenemyYvariable to0every timeredrawAvatar()is run, which means that it’ll always be drawn at a y-position of1(because it’ll be increased at line 12).We need to only set it to
0once. What if we do that indrawEnemy()? 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!"); //} }Try it out here!
Unfortunately, this doesn’t work at all. The problem lies in a concept called scope. If you use the
varkeyword to define a variable within a function, then the variable will only be accessible within that function. This means that ourredrawAvatar()function cannot access the sameenemyYvariable that was defined indrawAvatar().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!"); //} }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,
functionNameis the name of a function we want to run over and over again, andperiodis 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 asetInterval()call indrawAvatar(). (Unlike when using an event listener, no parameters automatically get passed toredrawEnemy()when we call it fromsetInterval().)Try it out here! Click the canvas, then don’t move your mouse.
There are a few things wrong with this:
redrawEnemy().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); }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()andredrawAvatar(), 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 ofenemyY– but actually drew it inredrawAvatar()?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. }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, meaningredrawEnemy()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 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
onclickattribute is"setUpGame();"rather than"drawAvatar();".I think this makes it easier to see what’s going on:
This makes it easier in turn to see a possible solution:
Let’s implement that. All we need to do is move the drawing code from
handleMouseMovement()tohandleTick(), 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.
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:
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); }Click to try it out.
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); }Click to try it out.
This is a bit messy, though. Instead, how about just creating an
enemyY2variable?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
enemyYandenemyY2to whatever you want, without having to change the code inhandleTick().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 touchsetUpGame()orhandleMouseMovement().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); }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:
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:
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:
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:This is because the
whilestatement acts like a repeatedifstatement. Remember,ifworks like this:if (condition) { outcome; }“If
conditionis true, then runoutcome.”whileworks like this:while (condition) { outcome; }“As long as
conditionremains true, keep runningoutcome.”It’s a subtle difference, but a really important one. An
ifblock will run just once, if theconditionis true; awhileblock will run over and over again until the condition stopsconditionstops being true.For this reason, the
outcome– the code that’s run inside thewhileblock – usually contains some code that will, eventually, causeconditionto stop being true; if it didn’t the code would just repeat itself forever. In our alert box example, we increased the value ofcurrentNumberuntil it “currentNumber < 5" was no longer true (whencurrentNumberreached 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
whileblock is called a "loop". Let's now take this code:...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 ofenemyYPositions[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
currentEnemyNumberto 0; if I didn't, the loop for drawing the enemy images wouldn't run even once, asconditionwould 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 thatconditionis now true again and start moving all the enemies.It all works just as it did before.
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:
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; } }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:
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[]orenemyYPositions[]array. We call this the array's length, and can retrieve it from either array by using the.lengthproperty - 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[]andenemyYPositions[]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 < avatarX + 30 < enemyX < enemyX + 30Here, the avatar and the enemy are overlapping. We have:
avatarX < enemyX < avatarX + 30 < enemyX + 30Still overlapping. We have:
enemyX < avatarX < enemyX + 30 < avatarX + 30No longer overlapping. We have:
enemyX < enemyX + 30 < avatarX < avatarX + 30Taking all these together, we can see that the enemy and avatar are overlapping horizontally if:
avatarX < enemyXandenemyX < avatarX + 30...or:
enemyX < avatarXandavatarX < enemyX + 30In other words, for horizontal overlap, this condition must be true:
(avatarX < enemyX && enemyX < avatarX + 30) || (enemyX < avatarX && avatarX < enemyX + 30)(Remember,
&&means "and";||means "or".)It's not hard to come up with a similar condition for vertical overlap:
(avatarY < enemyY && enemyY < avatarY + 33) || (enemyY < avatarY && avatarY < enemyY + 30)(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 < enemyX && enemyX < avatarX + 30) || (enemyX < avatarX && avatarX < enemyX + 30) ) && ( (avatarY < enemyY && enemyY < avatarY + 33) || (enemyY < avatarY && avatarY < enemyY + 30) )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 inhandleTick(), 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?
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:
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 resetcurrentEnemyNumberto 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 writeenemyXPositions[3] = 100, or whatever. But how can we add something to the array? WritingenemyXPositions = [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); }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 insidesetUpGame()?function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; var numberOfEnemies = enemyXPositions.length; enemyYPositions.push(0); enemyXPositions.push(250);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 useMath.random() * 400:function handleTick() { var gameCanvas = document.getElementById("gameCanvas"); var currentEnemyNumber = 0; var numberOfEnemies = enemyXPositions.length; enemyYPositions.push(0); enemyXPositions.push(Math.random() * 400);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() < 1/20being 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); }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:
Enjoy!
It’s quiz time! This month, let’s see how well you know your acronyms. AS3, OOP, HTTP, OMG!
Let’s Get Quizzy
Just So You Know…
This quiz was built with the jQuizzy Quiz Engine by Siddharth, ace reviewer for Envato. jQuizzy is available for purchase over on Codecanyon
Thanks also to Orman Clark and MediaLoot for their graphical contributions to the Activetuts+ Coffee Break Quizzes.
What Would You Like To Be Tested On?
If you’ve got an idea for an Activetuts+-related quiz subject, let us know in the comments!
We love the community of readers we have at Tuts+ and your opinion is incredibly valuable. This week, we’re giving you the chance to have your say about Tuts+, and win a few fantastic prizes at the same time! Read on to find out how to get involved and stand a chance of winning a copy of Adobe Creative Suite…
The Prizes!
We have a few fantastic prizes up for grabs, and the winning entries will be chosen at random when the survey closes in one week. Here’s what you stand a chance of winning:
1st Prize: 1 x Adobe Creative Suite 5.5 Web Premium
This prize has been kindly donated by Business Catalyst. Adobe Creative Suite 5.5 Web Premium software provides everything you need to create and deliver standards-based websites with HTML5, jQuery Mobile, and CSS3 support. Design immersive digital experiences and apps across desktops, smartphones, tablets, and televisions.
You’ll find plenty of incredible software inside, including Photoshop CS5 Extended, Illustrator CS5, Flash Professional CS5.5, Dreamweaver CS5.5, and Fireworks CS5. It’s the perfect accompaniment to everything we teach at Tuts+!
Runner-Up Prize: 4 x Six Month Tuts+ Premium Memberships
Four lucky runners up will receive a six month subscription to our Tuts+ Premium membership programme, completely free of charge! This is a brilliant way to expand your knowledge in different creative areas, and access a regular selection of incredibly high-quality tutorials and resources. You’ll be a pro in no time!
Competition Sponsored by Business Catalyst
Incorporating web hosting, content management, eCommerce features, email marketing and built in customer management tools, Business Catalyst can help you accelerate your projects and increase your profit margins. Try it out for free at businesscatalyst.com.
Complete the Survey
It just takes a few minutes to complete the survey, and we really value your opinion. We’ll be using the information from our readers to make the Tuts+ network better than ever, so this is your opportunity to let us know what you’d like to see change on the sites!
Take the Survey & Enter the Competition
Competition Rules
Thanks for taking the time to share your thoughts about the Tuts+ network. We really appreciate it, and look forward to hearing what you have to say!
For this chapter of AS3 101, we will be diving into the mechanics of the Flash event system. If you’ve been following along so far, you’ll have seen events in use, dating all the way back to the first episode of the series. The editor and I felt that it was time to write up something to be formally included in the curriculum, so if you’ve ever seen those lines of code about adding event listeners or dispatching events, and not quite caught on, then this is the tutorial for you.
There already exists an Activetuts+ tutorial on the basics of Events, so the focus of this tutorial will be on dispatching events from your own classes, including creating custom event types and objects.
To be successful with this tutorial, you should feel comfortable with writing and using your own classes in ActionScript 3, as well as feeling secure with using the existing events provided by Flash, such
MouseEvent.CLICKorEvent.ENTER_FRAME. We will be focusing primarily on dispatching custom events from custom classes.Preview
We’ll spend a lot of time on the theory for this tutorial, but in the end we’ll build a simple slider control that dispatches its own events:
Step 1: Why Use Event Dispatching?
This example is actually a rather simple example of why you’d want to dispatch your own events. When you write your own classes, you are ideally keeping them in their own black boxes and encapsulated. But you still need to have the various objects interoperate in order to create a useful program.
The event model provided by ActionScript 3 is a rather good and convenient way to facilitate communication between your classes, while maintaining a separation of responsibilities in your classes. So if we write our own custom class, such as the
ActiveSliderclass showcases above, we have a need to enable other objects to be aware of when the slider changes its value. If the slider can dispatch its own change event, then other objects that need to know that information can easily subscribe and be notified.Personally, I find the need to dispatch my own events in my classes so common that my class template sets up each new class with the boilerplate it needs to be able to do so. As you learn to keep objects discrete, you’ll turn to event dispatching as the most common technique to do so.
Step 2: How to Dispatch Your Own Events
I have good news: dispatching your own events is actually very simple. It is one of the core tenets of ActionScript 3, built in to the Flash Player, and as such there is only one thing you need to do in order to gain the ability to dispatch events. This one thing is:
Extend
EventDispatcherThat’s it: when writing your class, use the line:
public class MyClass extends EventDispatcher {Of course, you need to import
EventDispatcher, which is in theflash.eventspackage. You will most likely need other classes in the package, so it might be most convenient to simply import the package with a wildcard.Step 3: Dispatch
Now you’re set up to dispatch an event. All you need to do now is call a method provided by
EventDispatchernameddispatchEvent. Easy to grasp, no?When you call
dispatchEvent, you must provide at least one argument, anEventobject. All built-inEventobjects are in theflash.eventspackage, so here’s where that wildcard import comes in handy. Each type ofEventobject will have its own requirements, but most often you simply need to pass it just one argument, as well. This argument is the event type, which is aStringthat names the event, like"click"or"complete". These are more commonly written asMouseEvent.CLICKorEvent.COMPLETE, but the end result is the same; it’s an identifier that separates one type of event from another, and allows oneEventobject to manage multiple event types.So, putting it all together, if you wanted to dispatch a
"complete"event, you could do so like this:Just drop that line (or one like it) in whichever method it’s appropriate in your class. Your class will then utilize its inherited event dispatching system and any listeners will be notified for you. Speaking of listeners, let’s take a brief look at those, as well.
Step 4: Listen
Any other object in your application can listen for your custom events now. More good news: this is no different than registering for events for the built-in classes. In the previous step, we set up our hypothetical class to dispatch a
COMPLETEevent. To listen for that event, we could write this line somewhere else in our program:var myObject:MyClass = new MyClass(); myObject.addEventListener(Event.COMPLETE, myCompleteHandler); function myCompleteHandler(e:Event):void { trace("My object completes me."); }And that’s it. This should look familiar to anyone who has hooked up a
COMPLETElistener to aLoader, for instance, so I won’t dwell any further on this.Step 5: Where to Dispatch
Where you actually place the
dispatchEventline of code requires some consideration. Typically, it should be the last line of code of the method in which it is written. This is so that any other code that also runs in that method can set properties or otherwise update the internal state of the object. By dispatching after this internal update is complete, the object is in a “clean” state at the time of the dispatch.Consider, for example, our working example. Let’s say the
COMPLETEevent is all about the processing of some data; a bunch of data so large that it will take several seconds to completely process, so the object’s purpose is to handle the processing asynchronously so as not to block the UI. And we’re dispatching theCOMPLETEevent as a way of saying that the data has been processed.Now suppose that the main method in question looks something like this:
private function processDataChunk():void { _data += someDataProcess(); if (done()) { closeData(); } }OK, not very realistic, but it illustrates the point. We keep building up the internal data until some other internal logic determines we’re done, at which point we then write out some final bits of data to close the operation.
Now, let’s add the
dispatchEventcall:private function processDataChunk():void { _data += someDataProcess(); if (done()) { dispatchEvent(new Event(Event.COMPLETE)); closeData(); } }What’s the issue with this approach? Any code that executes within listeners for the
COMPLETEevent will run before thecloseDatamethod is called. Therefore, the state of the dispatcher changes more than once within the span of theprocessDataChunkmethod, and is not “stable” until after thecloseDatacall. Yet, we tell all of our listeners that we’re complete before that call. This could lead to some hard-to-track-down bugs where one object claims to beCOMPLETEbut really isn’t. The obvious solution is to switch some lines around:private function processDataChunk():void { _data += someDataProcess(); if (done()) { closeData(); dispatchEvent(new Event(Event.COMPLETE)); } }Step 6: Custom Events
You’re all set up to dispatch your own events. Now, what should you dispatch? There are a few options to consider:
This first option we’ve already seen in our previous examples. We have a need to dispatch an event related to the completion of some process, and as it happens Flash provides an event type (
COMPLETE) associated with an event object (Event) that fits our criteria. We have no need to provide additional data with the event. Dispatching anEvent.COMPLETEevent is all we need.We’ll explore these other options in the forthcoming steps.
Step 7: Custom Event Types
As hinted at back in the “Dispatch” step, event types are simply
Stringidentifiers. They can technically be anyStringyou like. It’s usually sufficient to make it a single word (like “complete” or “click”) or very short phrase (like “ioError” or “keyFocusChange”); it only has to be unique within a given event dispatcher’s realm of available events.In addition,
Eventobjects (including subclasses, likeMouseEventorProgressEvent) don’t really care what event type they are given when instantiated. AnEventDispatcherwill gladly dispatch events of any type identifier and of any class (as long as it’s theEventclass or a subclass).The upshot of this is that you can make up your own event type
String, dispatch it, and set up listeners with it, and everything will be just fine. This is helpful when you want to dispatch an event but can’t necessarily find a good representation of the nature of the event in the built-in classes.As an example, you might have a class that acts as a coordinator for several things at once: load some XML, load some images based on the XML data, and create a layout based on the loaded images’ sizes, ready for an initial animation, at which point you’d like to dispatch an event. While the
COMPLETEevent might be suitable, you may feel that a “ready” event more appropriately encapsulates the meaning.This is as simple as deciding on the
Stringto use, and then using it. Use it both when adding listeners, and when dispatching the event. If theStringmatches, the event will get to where it needs to go. For example, this is a partial listing of a hypothetical class:public class MyClass extends EventDispatcher { // ... A bunch of other stuff not shown. private function determineReadiness():void { if (everythingIsReady) { dispatchEvent(new Event("ready")); } } }And code from elsewhere in the same program:
var myObject:MyClass = new MyClass(); myObject.addEventListener("ready", onObjectReady); function onObjectReady(e:Event):void { // Do stuff now that it's ready. }And that would work.
However, it’s worth mentioning while we’re here that typing in matching
Strings all over the place is not a good practice. The possibility for error is high, and the event system won’t tell you that you’ve typed"raedy"instead of"ready". The fact that the event system is flexible and simple to use – just pass it any oldStringfor the event type – is also something of a weakness. Your dispatcher will gladly accept a listener for anything, even a"raedy"event. It doesn’t really reconcile what event types are registered against what event types are actually dispatched.To help prevent this, the standard approach is to simply put the
Stringyou want to use in a static constant somewhere, and then never use that literalStringagain. Only use the constant. Of course, the possibility of typos is just a great as before, but if you’re using aREADYconstant and not the"ready"literalString, a mistype will trigger a compiler warning. You’ll be able to correct your error quickly and easily. A mistype with the literalStrings produces no compiler error, nor does it produce a run-time error. The only thing that happens is that the SWF does not seem to work properly, because the event listener doesn’t fire.With this in mind, it’s most common to store these constants on the related
Eventclass. We’ll get to customEventclasses in just a few steps. But in the situation outlined in this step (i.e., we’re reusing anEventclass but not an event type), I find it more convenient to simply store that constant on the dispatcher class. So we might choose to do this:public class MyClass extends EventDispatcher { public static const READY:String = "ready"; // etc. private function determineReadiness():void { if (everythingIsReady) { dispatchEvent(new Event(READY)); } } }And:
var myObject:MyClass = new MyClass(); myObject.addEventListener(READY, onObjectReady); function onObjectReady(e:Event):void { // Do stuff now that it's ready. }This gives us the safety of storing event types in constants, while not forcing the inconvenience of creating a whole
Eventclass that we don’t need. I must stress that this is a stylistic choice, and you may feel free to use this technique or not. I felt it warranted explanation, so that you could make your own informed decision. In either regard, you should definitely store your custom event typeStrings in static constants. Where those static constants are defined is up to you.Step 8: Re-Dispatching Events
There are several times when one class (let’s call it
ClassX) owns a property that is typed as another class (we’ll call that oneClassY), while itself being owned by a third class (how aboutClassZ?).ClassXis listening for an event fromClassY, but not only do we want to haveClassXrespond to the event, we also want to consider thatClassXshould dispatch a similar (or even the same) event so thatClassZcan also take further action.As a more concrete example, we have a class (this will be “
ClassX“) that is a sort of data manager. It loads an XML document, parses it, and stores data from the XML in its own properties. So it has anURLLoaderobject (this would be “ClassY“), and listens for theEvent.COMPLETEevent for when the XML document is loaded.Then we have a main document class that owns the data manager (the document class is “
ClassZ“). It’s coordinating the data loading with other UI elements, so it wants to know when the data is loaded and ready, so it can proceed to create and layout UI elements based on the data.public class DataManager extends EventDispatcher { private var _xmlLoader:URLLoader; public function DataManager() { _xmlLoader = new URLLoader(new URLRequest("some.xml")); _xmlLoader.addEventListener(Event.COMPLETE, onLoadComplete); } // ... A bunch of other stuff private function onLoadComplete(e:Event):void { // ... XML parsing // With the XML parsed, we'd like to dispatch another event to signal being done. } }We could do this:
But we could also do this:
Here we simply re-dispatch the existing event. Not only do we reuse the event type and the
Eventclass, but we’re actually reusing the entireEventobject as it was passed in to our own listener.It’s not rocket science, but it is a handy little technique that is surprisingly not so obvious.
“But wait,” you must be thinking, “if we redispatched an event that originated from the
URLLoaderobject, wouldn’t thetargetof the event still be_xmlLoaderwhen it gets back out to the document class?” And you would have a very good and thoughtful point, and I would be proud of you for thinking so studiously, but you would be wrong.A rather magical thing happens when redispatching events. The
targetproperty gets set to the current dispatcher. You can find a working example of the code in this step in the download package, entitled redispatch.Actually, it’s not all that magical. When calling
dispatchEvent, if theEventobject that is passed in already has atargetset, then theclonemethod is called on theEvent, creating an identical but discreet copy of the originalEvent, except for the value contained intarget.Step 9: Custom Event Objects
Everything mentioned so far is something you will want to know. But there will come a point when the best thing to do is to dispatch your own custom event. Not just a custom event type, but a whole custom
Eventclass.The process for doing this is straightforward, you just need to follow a few steps. We’ll discuss these in due course. Note that quite a bit of this is boilerplate code, and you could easily create a template for an
Eventsubclass and change just a few key pieces and be off and running. The general steps, in shortened-as-if-you-knew-what-I-was-talking-about form:Eventsuper(...)clonemethodtoStringmethodTo explain these process more deeply, we’ll get a start on our slider project and create the
SliderEventwe will need for that. So we need to start our project before we can write some code, so a quick diversion in the next step, then we’ll start writing a customEventclass.Step 10: Create the Project Structure
We’ll keep things pretty simple for this one, but we’ll nonetheless create packages for our classes.
Start by creating a folder for the entire project. Mine will be called slider.
Inside of this, create a com folder, and inside of that, an activetuts folder.
Now create two folders inside of activetuts: events and ui. Your final folder structure should look something like this:
Now back to our
Eventclass.Step 11: Subclass
EventFirst, create a new text file in the slider/com/activetuts/events folder, and call it SliderEvent.as. We’ll pop in the boilerplate for any class first:
package com.activetuts.events { public class SliderEvent { public function SliderEvent() { } } }There should be nothing surprising here, and if you have ActionScript templates for your text editor, you shouldn’t even have to type that much in.
Now, we’ll modify this so that it extends
Event.package com.activetuts.events { import flash.events.Event; public class SliderEvent extends Event { public function SliderEvent() { } } }As you can see, we simply import the
Eventclass, addextends Eventto the class definition.Step 12: Call
superOur superclass can handle a lot for us, which is great, but we do need to make sure we initialize the superclass properly when we initialize our subclass. We need to set up the constructor with arguments matching those found on
Event‘s constructor, and pass those along with a call tosuper.package com.activetuts.events { import flash.events.Event; public class SliderEvent extends Event { public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=true) { super(type, bubbles, cancelable); } } }This is, so far, basic subclassing techniques. In fact, depending on your editor’s prowess with templates, you may be able to specify a super class when you create the file and have all of this done for you. Flash Builder, for example, is able to do this.
Step 13: Store Event Types in Public Static Constants
Presumably, there will be one or more event types associated with this event class. Just like the
COMPLETEevent is associated with theEventclass, and theCLICKeven with theMouseEventclass, our custom event class will likely have custom event types.This is as simple as writing a line like the following for every event type you would like to add:
Let’s do that now for the
SliderEventclass.package com.activetuts.events { import flash.events.Event; public class SliderEvent extends Event { public static const CHANGE:String = "sliderChange"; public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=true) { super(type, bubbles, cancelable); } } }We could theoretically use our class now. We can use the
SliderEventindispatchEvent, and listen for and create events with theSliderEvent.CHANGEevent type.But we won’t stop there. There is more to consider. But before we do more code-writing, we need to take another detour into theory.
Step 14: Push vs. Pull
When an event is dispatched, sometimes it’s enough to simply know that the event has occurred. For example, most times that you are interested in
Event.ENTER_FRAME,Event.COMPLETE, orTimeEvent.TIMERevents, you probably just want to know that the event happened. There are other times, though, when you probably want to know more. When listening toMouseEvent.CLICK, you might be interested in whether the Shift key was held down, or the coordinates of the mouse at the time of the click. If you’re listening toProgressEvent.PROGRESS, you will most likely want to know the actual progress of the load; that is, how many bytes have loaded and how many there are to load in total.The difference between these two methodologies is sometimes known as “push” and “pull.” Those terms refer to how the event listener gets data related to the event. If the data is “pushed” then there is data stored within the event object, and in order to get the data the listener merely need to use properties on the event object. If data is to be “pulled,” though, generally the event object has very little information contained within – just the necessities: the type, the target, etc. This target, though, is indispensable, as it provides access to the event dispatcher to the event listener, allowing the listener to get the data it needs from the dispatcher.
In other words, you can either push a bunch of data to the listener inside the event object, or you can require the listener to pull the data out of the dispatcher as needed.
The pros and cons of each technique are somewhat balanced, in my opinion, and the path you choose for your event object depends on the situation at hand, and not a little bit on personal preference.
Exhibit A:
Eventclass to execute a pull event.Eventclass, the only required argument is the event type.KeyboardEventhas akeyCodeproperty to detail the key that was pressed to trigger the event.This might be a good discussion for the comments; I’m sure many of you reading have passionate feelings about which methodology is better. Personally, I try to find the method that works best for the situation.
Having said that, it’s worth noting that to this point our
SliderEventclass is rather “pull-ish.” For the sake of illustration, and because it’s not a terrible idea (though I did come up with several of those), we’ll continue on with making this an event that pushes data along with it; namely the value of the slider when it was changed.Step 15: Declare Private Properties to Hold Custom Data
To implement a push event, we need to have a place to store the data being pushed. We’ll add a private property for that purpose.
You should still have
SliderEventopen (if not… what are you waiting for?). Add the highlighted line:package com.activetuts.events { import flash.events.Event; public class SliderEvent extends Event { public static const CHANGE:String = "sliderChange"; private var _sliderValue:Number; public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=true) { super(type, bubbles, cancelable); } } }Next we’ll modify the constructor so that we can accept a value parameter, and set the private property with that:
public function SliderEvent(type:String, sliderValue:Number, bubbles:Boolean=false, cancelable:Boolean=true) { _sliderValue = sliderValue; super(type, bubbles, cancelable); }This way we can easily create the
SliderEventand set up its push data in one line.Why use private properties? In this case, we want to protect the data. In my opinion the data related to an event is immutable, so long as it’s associated with the event. Being able to change the data of an event object is like editing a history textbook. To be fair, this is my opinion and the standard used by Adobe with their built-in classes is to use writable properties (technically they use private properties and public getters and setters, but the end result is the same).
Step 16: Create Public Getters for the Custom Data
So the next step would be to make sure we can access the data being pushed. A private property by itself is not useful to this purpose. Therefore, we need to write a public getter for the
_sliderValueproperty. We will choose to not write a setter, so that the property becomes read-only (as discussed in the last step).Add this method to the class:
public function get sliderValue():Number { return _sliderValue; }This adds a getter so we can access the
sliderValuein property-like fashion. I’m choosing not to add the matching setter. You can add one if you feel it’s worthwhile.Step 17: Override the
clonemethodI mentioned the
clonemethod a little while ago. You probably won’t callclonemuch yourself, but it’s not a bad idea to override theclonemethod so that your custom event plays nicely with the event system.Add this method to your class:
override public function clone():Event { return new SliderEvent(type, sliderValue, bubbles, cancelable); }First, notice the signature of this method. We’re using
overridebecause this method is declared inEvent, and we’re inheriting it. It also returns an object of typeEvent. Make sure, when writing yourcloneoverride, that you put the correct return type in. It’s easy to forget and put the type of your class there, but that will cause an incompatible override error, because the return types need to match.All we’re really doing in the meat of the event is creating a new
SliderEventand passing in the same values that we have stored in the current event object. This creates an identical but discreet copy: a clone.This is an optional step, but it’s a quick win and ensures that your custom event plays well with the rest of the event system.
Step 18: Override the
toStringmethodOne last thing, and again this is optional. But it’s also very useful as a debug tool, so it usually pays for itself within a few uses.
In case you haven’t been told yet, the
toStringmethod exists on all objects (it’s declared and defined inObject, the über-class from which all other classes inherit, whether you like it or not). It can be called explicitly, but the handy thing is that it get called automatically in a number of cases. For example, when you pass object to thetracefunction, any object that is not already aStringwill havetoStringcalled on it to make sure it is formatted nicely for the Output panel. It even gets called when working withObjects together withStrings, as with concatenation. For example, if you write this:ActionScript is smart enough to convert
42into aStringrepresentation of theNumberbefore concatenating theString. Trying to add aStringand aNumberis bad news, but converting aNumberto aStringand then concatenating it with anotherStringis just fine.So when you write your own classes, you can provide a
toStringmethod, which takes no arguments and returns aString, and return whateverStringyou like.In the case of
Eventobjects, Adobe helpfully provides aformatToStringmethod to help allEvents look similar when traced. We’ll use it in the method we’re about to add to our class:override public function toString():String { return formatToString("SliderValue", "type", "sliderValue"); }First, note the method signature. Again, it’s an
overrideso we have that keyword. It’spublic, it takes no parameters, and returns aString(which should be obvious).Next, note the single line in the method body. We call
formatToString, which is defined inEvent, so it’s easy to use. The first argument we pass to it is theStringname of the class. After that, the arguments are open-ended. You can pass in one, 15, or none. We’re passing in two. No matter how many you pass in, they should all beStrings, and they should match property names on your class."type"is defined byEvent, but"sliderValue"is defined by our own class. Either way, what happens is that the name of the property is printed, followed by an equals sign, which is followed by the actual value of that property. In short, it will end up looking like this:This is entirely non-functional but very useful. It can provide a quick glimpse into the event when things aren’t working the way you think they should.
Step 19: Building the Slider
At this point, we’ve been through the key concept of this tutorial: writing a custom
Eventclass. But we really need to put it to the test. We’ll spend the remainder of our time building the simple slider application that was previewed at the beginning of the tutorial.We already have a project folder structure; we just need a few more files. We’ll start with the FLA file.
Create a new Flash file (ActionScript 3.0, of course) and save it as ActiveSlider.fla in the root of your project folder. I’m going to assume that you don’t need step-by-step details on how to put this simple FLA together, and instead I’ll lay out the key elements. You can use the FLA file found in the start folder of the download package for reference, as well, or even just copy that FLA to your project folder and call this step done.
There are three main objects on the stage.
track_mcgrip_mcoutput_tfOther than hooking up the document class, which we’ll write in two steps, the FLA is ready for business.
Step 20: The
ActiveSliderClassThe main UI class with which we’ll work is the
ActiveSliderclass. This will extendEventDispatcher, target the two Movie Clips on the stage, and set up mouse interactivity for slider behavior. Most exciting of all, it will dispatch aSliderEvent.Start by making a new class file called ActiveSlider.as in the com/activetuts/slider folder of your project. This class isn’t too intense (at least, not for our purposes here. A slider class could get much more involved), and I’ll just present the code in full and discuss as we go:
package com.activetuts.slider { import flash.display.*; import flash.events.*; import flash.geom.*;Nothing exciting, just setting up the package and imports.
public class ActiveSlider extends EventDispatcher { private var _track:Sprite; private var _grip:Sprite; private var _grabOffset:Point;We’ll need these three properties. The first two keep track of the
Sprites (orMovieClips) that make up the slider. The third is used while dragging the slider grip; it helps keep the grip’s position offset from the mouse by an amount relative to where the grip was clicked in the first place.public function ActiveSlider(track:Sprite, grip:Sprite) { _track = track; _grip = grip; if (_grip.parent != _track.parent) { throw new Error("The track and the grip Sprites are not in the same container.") } _grip.addEventListener(MouseEvent.MOUSE_DOWN, onDown); if (_grip.stage) { addStageListener(); } else { _grip.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); } }This is the constructor. It accepts two
Spritearguments, which get passed on to the first two of those properties for storage. It then does a simple check to make sure the twoSprites are in the same coordinate space by checking that theirparentproperties reference the same object. If they don’t, then our math for placing the grip might be unreliable, so we throw an error as a way of alerting the developer. The rest of the constructor is devoted to adding two event listeners. The first is aMOUSE_DOWNevent and straight forward. But the second is trying to add aMOUSE_UPevent to thestage, which might or might not exist depending on whether thegripSpriteis on the display list or not. The next two methods might make this a little clearer:private function onAddedToStage(e:Event):void { _grip.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage); addStageListener(); } private function addStageListener():void { _grip.stage.addEventListener(MouseEvent.MOUSE_UP, onUp); }The
onAddedToStagemethod is an event listener for theADDED_TO_STAGEevent, which was set up in the constructor, but only if the gripSpritedid not already have a reference to thestage. TheaddStageListenermethod simply adds theMOUSE_UPevent listener to thestage. So, if there is astagereference in the constructor, we calladdStageListenerdirectly. If there is not astagereference, we set up theADDED_TO_STAGEevent, and when the grip is added to the display list, and thus has astagereference, theonAddedToStagemethod fires which then in turn callsaddStageListener. It also removes theADDED_TO_STAGEevent listener, because we only need to do this once.private function onDown(e:MouseEvent):void { _grabOffset = new Point(e.localX, e.localY); _grip.addEventListener(Event.ENTER_FRAME, onFrame); } private function onUp(e:MouseEvent):void { _grip.removeEventListener(Event.ENTER_FRAME, onFrame); }Next we have our two mouse event listeners. In
onDown, the key line is to then add anENTER_FRAMEevent listener. InonUp, we remove that listener. Also inonDown, we make note of where on the grip the mouse click actually happened, and store that in_grabOffset. This will play into ouronFramemethod next.private function onFrame(e:Event):void { _grip.x = _grip.parent.mouseX - _grabOffset.x; var gripBounds = _grip.getBounds(_grip.parent); var trackBounds = _track.getBounds(_grip.parent); if (gripBounds.x < trackBounds.x) { _grip.x = _track.x; } else if (gripBounds.right > trackBounds.right) { _grip.x = trackBounds.right - gripBounds.width } trace(this.value); }This is the main meat of our slider logic. This method fires repeated on an
ENTER_FRAMEevent, but only when the mouse has been pressed down on the grip, and only for as long as the mouse remains pressed.First, we set the grip’s
xproperty to match the mouse position, only we need to offset it based on the original mouse position, so that it moves smoothly and doesn’t jump so that its left edge is at the mouse.The next two lines calculate the bounding rectangles of both the grip and track
Sprites. We’ll use these rectangles in the upcoming math, so we’ll keep things tidier by pre-calculating the rectangles and storing them in variables.Then there is the
ifblock. This just constrains our slider to within the track. It’s a simple check to see if thexof the grip, as calculated in the first line of the method, is lower than (to the left of) thexof the track. If it is, it would be too far, so we need to move the grip to that minimum value. Similarly, we check to see if the grip’s right edge is greater than (to the right of) the track’s right edge, and if so we need to reel it back in to that maximum value.Finally, we have a reliable grip position, and for now we just trace the current slider value, which is calculated in the final bit of code for the class:
private function get value():Number { return (_grip.x - _track.x) / (_track.width - _grip.width); } } }This is a simple getter, although the math being used for the return value might be a little confusing. It determines the current grip position as a percentage of the range of motion of the grip. It would be more obvious if it were just this:
…which is reasonable but doesn’t take into account that the grip doesn’t actually travel the entire width of the track. It goes all the way to the left edge, but only as far right as the right edge of the track minus the width of the grip. So this is more accurate:
This makes the width by which we divide the range of motion. However, there may still be a gotcha in that the track may be offset to the left or right from its container, meaning that the values we get aren’t quite right. We need to neutralize that by subtracting the
xposition of the track from the grip’sx, and we end up with this:For reference, here is the complete class, with my ramblings left out:
package com.activetuts.slider { import flash.display.*; import flash.events.*; import flash.geom.*; public class ActiveSlider extends EventDispatcher { private var _track:Sprite; private var _grip:Sprite; private var _grabOffset:Point; public function ActiveSlider(track:Sprite, grip:Sprite) { _track = track; _grip = grip; if (_grip.parent != _track.parent) { throw new Error("The track and the grip Sprites are not in the same container.") } _grip.addEventListener(MouseEvent.MOUSE_DOWN, onDown); if (_grip.stage) { addStageListener(); } else { _grip.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); } } private function onAddedToStage(e:Event):void { _grip.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage); addStageListener(); } private function addStageListener():void { _grip.stage.addEventListener(MouseEvent.MOUSE_UP, onUp); } private function onDown(e:MouseEvent):void { _grabOffset = new Point(e.localX, e.localY); _grip.addEventListener(Event.ENTER_FRAME, onFrame); } private function onUp(e:MouseEvent):void { _grip.removeEventListener(Event.ENTER_FRAME, onFrame); } private function onFrame(e:Event):void { _grip.x = _grip.parent.mouseX - _grabOffset.x; var gripBounds = _grip.getBounds(_grip.parent); var trackBounds = _track.getBounds(_grip.parent); if (gripBounds.x < trackBounds.x) { _grip.x = _track.x; } else if (gripBounds.right > trackBounds.right) { _grip.x = trackBounds.right - _grip.width } trace(this.value); } private function get value():Number { return (_grip.x - _track.x) / ((_track.width - _grip.width) - _track.x); } } }We have not added in our
SliderEventclass yet; we’ll take a separate step to do that. But first, we need our document class so we can actually use theActiveSlider.Step 21: The Document Class
We need one more file to make it work: our document class. Make a new class file named
SliderDemoin the project root folder. Add in the following code:package { import flash.display.*; import flash.events.*; import com.activetuts.slider.ActiveSlider; public class SliderDemo extends Sprite { private var _slider:ActiveSlider; public function SliderDemo() { _slider = new ActiveSlider(track_mc, grip_mc); } } }This is much simpler that our
ActiveSliderclass. It really just sets up anActiveSliderinto the property named_slider.Go back to the FLA file, and enter SliderDemo into the document class field, and you should be able to try this out. The slider should be able to move back and forth, and constrain itself to the width of the track.
Now for one last task. We need to dispatch, and listen for, a
SliderEvent.CHANGEevent. We’ll do that next.Step 22: Dispatching the
SliderEventGo back to the ActiveSlider class, and make a single-line change to the
onFramemethod:private function onFrame(e:Event):void { _grip.x = _grip.parent.mouseX - _grabOffset.x; var gripBounds = _grip.getBounds(_grip.parent); var trackBounds = _track.getBounds(_grip.parent); if (gripBounds.x < trackBounds.x) { _grip.x = _track.x; } else if (gripBounds.right > trackBounds.right) { _grip.x = trackBounds.right - _grip.width } dispatchEvent(new SliderEvent(SliderEvent.CHANGE, this.value)); }We’ve removed the trace and replaced it with a real, live event dispatch. For this to work, though, we need to import the
SliderEventclass:package com.activetuts.slider { import flash.display.*; import flash.events.*; import flash.geom.*; import com.activetuts.events.SliderEvent;Step 23: Listening For the
SliderEventFinally, go back to the SliderDemo class, and change it so it listens for and reacts to the
SliderEvent:package { import flash.display.*; import flash.events.*; import com.activetuts.slider.ActiveSlider; import com.activetuts.events.SliderEvent; public class SliderDemo extends Sprite { private var _slider:ActiveSlider; public function SliderDemo() { _slider = new ActiveSlider(track_mc, grip_mc); _slider.addEventListener(SliderEvent.CHANGE, onSliderChange); } private function onSliderChange(e:SliderEvent):void { trace(e); output_tf.text = e.sliderValue.toString(); } } }We’re again importing the
SliderEventclass, and after creating theActiveSlider, adding a listener calledonSliderChangeto the slider. That method is the biggest addition, but still is a regular event listener. It’s much like any other event listener, only we’re making sure to type the event argument asSliderEvent, because that’s what we’re getting.The first line of code is a bit superfluous, but I wanted to see what happens when you trace our
SliderEventobject. You’ll see, when you run this, a typical-for-Flash formatting of the event object.The second line does what we were after to begin with. It simply grabs the
sliderValueproperty, turns it into aString, then sticks thatStringinto theTextFieldon the stage, so we can see the slider value in the movie.Step 24: Wrapping Up
When you start rolling your own custom events, you start working with ActionScript 3 the way it was meant to be used. Events help you decouple classes from each other, and a well structured event flow in your application can really make the difference between something that is easy to work with and something that is buggy and temperamental. With this (theoretically) final instalment of AS3 101, you should be well on your way to becoming a ninja.
Thanks for reading, and I’ll see you back here on Activetuts+ before you know it!
In this two-part mini series, you’ll learn how to create a Flash spaceship game, where the main control is via the microphone: shout louder to make your ship fly higher, to try to avoid the obstacles that appear on the screen. In this first part, we’ll focus on the design of the game, by drawing the main game objects and putting together the (mouse-controlled) menu and screen interface.
Final Result Preview
Here’s a look at the design of the game, which we’ll be putting together:
Creating the Graphics
Don’t like ads? Download the screencast, or subscribe to Activetuts+ screencasts via iTunes!
In this section, we’ll draw our game’s main graphical objects: a spaceship, a laser beam, a diamond, a meteor, a planet, and a star – all in the pixel art style seen in the SWF above, and all using Flash Pro.
Designing the Menus and Screens
Don’t like ads? Download the screencast, or subscribe to Activetuts+ screencasts via iTunes!
Here, we’ll create our menu interface, help screen, game over screen, and HUD.
Links mentioned in the video:
(Apologies for the weirdness that appears at around the 8:20 mark!)
Coding the Menu Interface
Don’t like ads? Download the screencast, or subscribe to Activetuts+ screencasts via iTunes!
Now that we have all of the graphical elements, we’ll write the code to make our menus interactive, and handle the navigation between screens.
More info on AS3 Events: Taking a Closer Look at the ActionScript 3.0 Event Framework.
Check back in a couple of weeks for the second part of the series, where you’ll learn how to program the actual game mechanics – including the microphone-based interaction!
Twice a month, we revisit some of our readers’ favorite posts from throughout the history of Activetuts+. This week’s retro-Active tutorial, first published in October 2010, is an introduction to object-oriented programming in AS3. Don’t forget to check out the other AS3 101 tutorials!
Object-Oriented Programming is a bit of a hot topic. More and more programming languages are cropping up that support it (Ruby, for example), and more and more languages that previously didn’t support Object-Oriented Programming (OOP for short) have been released with versions that do, like PHP and ActionScript. Some languages only work if you utilize the whole OOP thing to begin with, like Java and Objective-C.
It’s actually been around for a long time, but came into the limelight in the 1990′s primarily due to its advantages with programming GUIs and the rise of C++ and Objective-C. Wikipedia has an interesting section on the history of OOP, which is a great place to start learning.
Introduction
If you’re new to Object-Oriented Programming, you have a bit of a learning curve ahead of you. However, embracing this technique isn’t just the “in” thing to do; it can decrease your development time while also decreasing the number of bugs in your code. It keeps you organized and prevents needless duplication. But perhaps more compelling than those lofty promises is the fact that Adobe has certainly put all of their weight behind the concept of OOP when it comes to ActionScript. ActionScript 1 was not Object-Oriented at all, and AS 2 was only Object-Oriented as a developer convenience. In contrast, AS 3 supports full-bore Object-Oriented features, and if anything, will only do more so in the years to come. So, if you like programming Flash and don’t want to get left behind, it would do you well to adopt OOP as a way of life.
In this tutorial we will gradually introduce the some of the core concepts involved in Object-Oriented Programming. The final product coming out of Flash will be a bit lackluster, but this is just Part 1. You will see some practical explorations of OOP techniques, even if this tutorial isn’t really about building a specific project.
You will, however, learn to make a reusable button which can be used in cases where a quick-and-dirty button is required, or even, with modification, as a key element to your next project.
Step 1: The Big Question
So what is this Object-Oriented Programming all about?
First, let me explain what it is not. Object-Oriented Programming (or OOP for short) is not the only or even necessarily the best way to program in ActionScript 3. There seems to be a myth floating around ever since ActionScript 3 debuted that, in order to use AS3, you need to use OOP.
While it’s true that AS3 is much better-suited to OOP than AS2 was, and that you are encouraged to move in that direction, it’s not true that you have to learn to write classes and instantiate objects in order to build real applications in Flash. If you’ve followed the AS3 101 series up until now, you may have noticed that every line of ActionScript 3 that we wrote in the process was in the script panel, not a class file. This should help illustrate that it’s perfectly acceptable to not utilize OOP as you work in Flash.
That being said, ActionScript 3 really does shine when you start to embrace OOP. And not just AS3, but any language that supports OOP can be utilized more effectively when OOP techniques are engaged. This is because OOP is really just a technique – a big, complex technique with lots of options to consider, but still, just a technique – and not a mandate. And it is a great technique, one that you should start mastering. And that’s why you’re here, right?
Step 2: Classes and Objects
Object-Oriented Programming really boils down to two fundamental units: classes and objects. The two are intrinsically related: a class begets an object.
Think of a class as a blueprint of a house, and an object as an actual house. The blueprint is more of a schematic for what the house will be like, not an actual house. When you write object-oriented code, you write the blueprints, the classes. Your code then creates the objects from these classes when it runs.
A blueprint can also be re-used to create more than one house. In certain housing developments, you can see this in action; a cost-saving technique that pays the architect for one blueprint, but generates several houses from that blueprint. These houses are all unique entities, and what goes on in one house is isolated from what goes on in another house. However, they share a lot of features in common, such as the layout of the plumbing and the electrical wiring, or where the stairs are located. The houses may all have a front door at the same location, but one house might have a metal door painted red, and another house might have an unpainted wooden door with a knocker.
A class, similarly, can create more than one object. These objects are all of the same type, but are all unique entities, like the houses. Again, they share many common features, but maintain separate identities. An object that loads data from an external source would have, as a common feature, the ability to load data and dispatch a complete event when it is ready. However, one instance of this loader would have a unique characteristic that loaded data from one URL, while another instance would load data from a different URL.
The term instance is a word that describes an object. When an object is created from a class, it is said to be instantiated, and thus an instance of the class is born. The word object is typically synonymous with instance.
If the terminology is troubling you, keep in mind the Library Symbol/Instance relationship that’s been around since Flash 1. A symbol is a single item in the library. An instance is a particular manifestation of that symbol. There can be more than one instance of a given symbol, and changes to a single symbol’s artwork will change all instance of that symbol. Yet each individual instance can maintain some unique properties, like position, rotation, color transformation, filters, and scale (to name but a few). This is directly analogous to class and objects (instances). In fact, it’s more than just analogous, as we’ll see in the next installment of this series.
Step 3: Meet the Document Class
Well, let’s jump in, shall we? I think the best way to get your feet wet in OOP AS3 is to become familiar with the document class. The document class is a class like any other in AS3, except that it has a very special relationship with a Flash file. It is the class that represents the SWF that you publish. The document class gets instantiated when the SWF starts up. This is analogous to the idea of a “main” function if you have a background in C, or the “main” method of the main class in Java.
Step 4: Make a Project Folder
Let’s make a document class, which will act as a basis for steps later in this tutorial. To do so, first create a folder on your filesystem that will house this particular project.
Now make a Flash file (ActionScript 3.0), and save it to this folder. The name isn’t terribly important, but I’ll be naming mine MeetTheDocumentClass.fla
Step 5: Document Class Text File
Now you need to make a text file. You can use Flash CS3+ for this, or you can use Flex/Flash Builder, or you can use any other capable text editor, really. FlashDevelop is a great (and free) choice, if you’re on Windows. There are many choices in text editors, and there are no right answers (although, as I tell my students, there is one wrong answer. The text editor built into Flash CS3/4 is actually quite abysmal, and the sooner you embrace that fact the sooner you’ll enjoy coding in a real editor). Personally, I’m fond of TextMate, but keep in mind that what matters is the text within the file.
All that being said, create a new file in the editor of your choosing (if you’re using Flash CS3+, go to the File menu, choose New, and then choose ActionScript File from the list. If you’re not, then I assume you’re familiar enough with your editor to be able to do this without direction from me).
Save this file as DocumentClass.as in the same folder as your Flash file. This is important: the location and the name matter very much. The name doesn’t have to actually be “DocumentClass.as” but it’s what I’ll be using and to prevent confusion, I recommend you just do what I do. The name can be anything you want (more or less), but my point is that the name plays an important role in AS3 OOP, so be paying attention. Same deal with the location of the file; technically it can be anywhere but for convenience just follow my lead. More options will be revealed by the end of this series.
To summarize, here’s my project folder as it stands:
Step 6: Write the Document Class
You are now faced with an empty file. If you’ve ever suffered writer’s block (I have, which regular readers might have guessed…), you can be comforted in the fact that the first thing you need to write is actually boilerplate. You’ll need to write the following:
package { import flash.display.MovieClip; public class DocumentClass extends MovieClip { public function DocumentClass() { } } }Wow, what is all that? Well, it’s a bit more than we need to explain right now. I’ll just point out a few places of interest.
First, note that the
packagewraps up the entire thing. Don’t worry about what this is right now, just remember that it needs to be there, and the class needs to be contained within it.Second, we have an
importstatement. You may not have seen a line of code like this before; if you’ve stuck to coding in the Script panel up until now and haven’t made use of third-party ActionScript libraries, you’ve never needed to write an import statement. In OOP-world, each class needs to import the various classes it needs in order to do its thing. More on imports later.Third, the next “wrapper” is the
classitself. You can see the keyword “class” in the third line. The anatomy of this line is more than we need right now, just note that the word following the word “class” is the name of the class. You may have noticed that the name I used is the same as the name of the file, sans extension. This is not a coincidence. For classes to work in AS3, the name of the class must match the name of the file. Don’t forget that.We’ll come back to the “extends MovieClip” bit in the next tutorial. For now, just know that this bit is required for a document class (you can also extend Sprite, a la “
extends Sprite“, if you don’t need the timeline, and if you change the import line to “import flash.display.Sprite;“)Finally, we have the constructor.
Step 7: The Constructor
In the previous example, there is an (almost) normal function defined within the class. Almost normal, except for that “public” in front, and the missing datatype. Again, don’t worry about “public” for now, that’ll come in time. However, note the name of the function. That’s right, it’s the same as the file and as the class name. Hopefully you’re not overly paranoid, because this isn’t a conspiracy. It’s just the way things work. By naming the function with the same name as the class, we create a special function called the constructor. The constructor gets executed during instantiation. Hopefully you’re keeping up with the new terminology, because we just used two new terms in the same sentence.
nerdCred++;An object will be able to have other functions in it, which you are welcome to define, but the constructor is the only function that gets called automatically during instantiation. The constructor, therefore, is where you would want to put code that should execute to get an object ready for initial use. For example, if you’re writing a class for an object whose job it will be to load some XML, parse it, then load thumbnail images based on that data, you might want to create a
URLLoader, set up its event listener, and perhaps even start loading, all when you create the object. That logic could then go into the constructor.Step 8: Hello, World!
As a practical example, we’ll do a little “Hello World” program. We won’t simply trace the words to the Output panel, no, that’s a little mamsy and definitely too pamsy. We’ll create a
TextField, add it to the stage, and put some text into it. Expand yourDocumentClass.asfile to look like this (changes are in bold):package { import flash.display. MovieClip; import flash.text.TextField; public class DocumentClass extends MovieClip { public function DocumentClass() { var tf:TextField = new TextField(); addChild(tf); tf.text = "Hello World"; } } }Note that in addition to the three lines added to the constructor, there is now an extra
importline in anticipation of the TextField.We could obviously take it further, positioning and styling the text, and you’re welcome to do that on your own, but for now this serves the purpose. This code creates a text box with the words “Hello Word” in it, and displays it on stage. Except for one thing: The Flash file doesn’t know that it’s supposed to have a document class. If you try to run your Flash file now, you’ll get an empty window. Let’s fix that next.
Step 9: Assigning a Document Class
As mentioned at the end of the last step, the Flash file (
MeetTheDocumentClass.flain my case) doesn’t yet know that it has a specific document class associated with it. Technically, it does have a document class, only the document class is just plain oldMovieClip, so it’s not going to do much.It’s good to know that, but what we really want right now is to let this Flash file know where the
DocumentClass.asfile we just wrote is. To do that, first make sure nothing is selected (you can click anywhere on the stage where there isn’t a visual asset, or you can choose Edit > Deselect All, or press Command-Shift-A or Control-Shift-A).Next, open up the Properties panel. If you’ve successfully deselected everything, the Properties panel should say “Document” at the top
Now, where it says “Class:” under the “Publish” section of the Properties panel, type in “
DocumentClass” (or whatever name you used for your version of the class, if you insist on forging your own path). Note that this is without the “.as” – it’s just the class name, not the file name.If you’ve followed along with the directions so far, you should be able to press Return/Enter and all will be well. If somehow you got the class name wrong, or didn’t save the files in the same folder, you won’t get an error when you publish. You should have gotten this error when typing in the class name:
If you ignored it, or didn’t see it because you had previously checked that “Don’t show again” option, then the SWF will publish without error, but you’ll get a plain white screen. If this happens, double-check the class name that you’ve entered into the Properties panel.
Once the document class is properly hooked up, you can verify that by clicking the little pencil icon next to the document class text field in the property panel. This will open up the file in Flash CS3+ for editing. Not that I’d recommend keeping it there, but verifying that you’ve got the right file can be a helpful troubleshooting tip.
Once everything checks out, go ahead and test your movie. You should see a plain window with the words Hello World in it:
Step 10: Properties
At this point, you are set up to put your application logic into the document class and start making a more interesting SWF. However, you will likely need more than just the constructor function to make said interesting SWF. Usually you need to create other objects, stick them in variables, use functions to listen for events and have these things interact in a meaningful way. Of course, in OOP you have such things as variables and functions, only they properly go by different names: properties and methods (we’ll tackle methods in the next step).
Properties are, at their essence, simply variables. There is a slight difference between a property and a variable, though. It’s subtle, and we’ll have to illustrate it with an example, but we have some ground to cover before we get there.
First, a property is written identically to a variable, with two caveats. Here’s an example:
The first caveat should be obvious: there’s a big old “private” in front of the
varkeyword. “private” is similar to the “public” I asked you to ignore a few steps ago. I’m going to ask you to continue ignoring it. We won’t get to those until the next tutorial.The second caveat won’t be obvious, because the snippet is out of context. This is more appropriate:
public class DocumentClass extends MovieClip { private var tf:TextField; public function DocumentClass() { // ... } }Notice the position of the property in relation to the structure of the class. The constructor function and the property are both at the same “level” and “belong” directly to the class (they are both nested into the class directly). This characteristic makes the variable a property (despite being declared by using the
varkeyword).In contrast, the variable named “
foo” in the snippet below is merely a variable:public class DocumentClass extends MovieClip { private var tf:TextField; public function DocumentClass() { var foo:String = "bar"; } }Why? Because it’s “owned” by the function, not by the class. When an variable or function gets declared within the confines of curly braces (which is technically every time), it exists for only as long the thing represented by the curly braces exists. So – and here is the subtle distinction – the property “
tf” exists so long as the object “DocumentClass” is around, and “belongs” to aDocumentClassobject, but the variable “foo” exists only as long as the function containing it runs, and ceases to exist once the function finishes executing.There are, naturally, exceptions to that “curly brace” rule. Loops, for instance, don’t affect the scope of a variable declared within, so take that rule with a grain of salt. You won’t find it mentioned in programming books.
You might be asking,
You have a point, however, the distinction is that the potential to run a function exists within the object, while the actually running function exists only at the moment it gets called. The variable gets created during the run, and therefore ceases to exist once the run is done. I did say the distinction was subtle. But it is important, as we’ll see soon.
Step 11: Methods
Before we get to that, I need to extrapolate what we just learned to functions and methods. Basically, methods are just functions that belong to an object, like the constructor function, only they aren’t elevated to any special status. Here, we added another method:
public class DocumentClass extends MovieClip { public function DocumentClass() { // ... } public function onButtonClick(e:MouseEvent):void { trace("click"); } }Now,
onButtonClick()is a pretty familiar function, except for that peskypublicat the beginning. Once again, put that off. Otherwise, you should recognize this as a regular function. The only difference is that the same concept of “ownership” applies to methods as it does to properties, versus regular variables and functions. Finally, let’s explore this with a working example.In your
DocumentClass, we’ll flesh out the snippets we’ve seen. Make your class look like this:package { import flash.display. MovieClip; import flash.text.TextField; import flash.events.MouseEvent; public class DocumentClass extends MovieClip { private var tf:TextField; public function DocumentClass() { tf = new TextField(); addChild(tf); tf.text = "Hello World"; stage.addEventListener(MouseEvent.CLICK, onButtonClick); } private function onButtonClick(e:MouseEvent):void { tf.text = "Hey, you clicked!"; } } }Notice the changes and additions: We’ve added a property called “
tf“. In the constructor, we still create aTextField, but instead of creating the variable in the constructor like we did before, we simply use “tf” – the property. OurTextFieldis now stored in the property instead of a variable.We also added a click listener to the stage (no, the stage isn’t really a button, but in this case it saves us from having to create a
MovieClipto act as a button). And that listener is a method calledonButtonClick(), and other than being a method instead of a function, it’s identical to any other event listener.What this method does is significant: it replaces the text in the textfield with another string. This can only happen if
tfretains it value over time. If the movie worked, then we knowtfstill references theTextFieldcreated in the constructor, even though several seconds probably passed between the creation of theTextFieldand the re-population of it.Step 12: Exploring Scope
Now, if you remove the property and return the TextField creation to be a variable, you actually won’t even be able to test it, as the compiler will give you an error saying it couldn’t find a property called
tf. That is, this code:package { import flash.display.MovieClip; import flash.text.TextField; import flash.events.MouseEvent; public class DocumentClass extends MovieClip { //private var tf:TextField; //we have removed this line! public function DocumentClass() { var tf:TextField = new TextField(); addChild(tf); tf.text = "Hello World"; stage.addEventListener(MouseEvent.CLICK, onButtonClick); } private function onButtonClick(e:MouseEvent):void { tf.text = "Hey, you clicked!"; } } }Produces this error:
What happened? Well, in ActionScript, a variable created in one function exists for the duration of the function (which is over a few lines later), and is gone once the second function runs. So if we create a variable
tfin the constructor function and try to use it in theonButtonClick()function, we have a problem.tfno longer exists whenonButtonClick()runs.However, properties persist throughout the life of an object, so if we store the
TextFieldin a property instead of a variable, we have access to thatTextFieldin other functions, at pretty much any time.In this context, variables are sometimes referred to as local variables because they are local to the function.
And this is the subtle point that started this marathon step. Hopefully it sunk in, because I find this point to be the cause of many “gotchas” for beginning OOP students.
Keep in mind that there is more going on with properties, methods, persistence and references. I feel I’ve belabored the point enough, and won’t be doing you any favors by dissecting the finer points of references and delaying other essential knowledge.
Step 13: About That Button…
OK, in the last step I skipped the creation of a button and just used the stage as a clickable area. We’ll change that, but not because clicking on the stage is wrong (it isn’t, in certain circumstances), but because we can create a Button class to further explore OOP.
Our goal will be to create a
Buttonclass, from which we can create multipleButtonobjects. EachButtonobject will have some core functionality that is universal across buttons:At the same time, there will need to be things unique to each button:
We’ll look at what it takes to create a second class involved in your project and incorporate these requirements as we go.
Step 14: Create a Button Class
We’ll create a class that, when instantiated, draws a rectangle, puts text into a label, handles roll overs and outs, and can respond to clicks. Start by creating a new text file in your text editor. Save it as “Button101.as” in the same folder as your Flash file. This is important. Don’t mess it up! I’m serious.
Step 15: Write the Boilerplate
In your new file, start stubbing in the class boilerplate:
package { import flash.display.Shape; import flash.display.Sprite; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFormat; public class Button101 extends Sprite { public function Button101() { } } }Step 16: Add Some Properties
Next we’ll add some properties to hold the background shape and the label:
// ... public class Button101 extends Sprite { private var bgd:Shape; private var labelField:TextField; public function Button101() { // ...The
bgdproperty will hold a reference to aShapewhich we’ll draw programmatically to be a filled rectangle, and add as a child of the Sprite.The
labelFieldproperty will hold a reference to aTextFieldwhich we’ll create with code, as well, and use to display a text label on top of the background.Step 17: Add the Logic
And now we’ll pop in the actual logic. For the constructor:
public function Button101() { bgd = new Shape(); bgd.graphics.beginFill(0x999999, 1); bgd.graphics.drawRect(0, 0, 200, 50); addChild(bgd); labelField = new TextField(); labelField.width = 200; labelField.height = 30; labelField.y = 15; var format:TextFormat = new TextFormat(); format.align = "center"; format.size = 14; format.font = "Verdana"; labelField.defaultTextFormat = format; addChild(labelField); addEventListener(MouseEvent.ROLL_OVER, onOver); addEventListener(MouseEvent.ROLL_OUT, onOut); mouseChildren = false; buttonMode = true; }That’s a lot of code, but it’s nothing surprising for the most part. We’re creating a new
Shapeand drawing a rectangle to into it. Then we create aTextField, set it up with some basic formatting. Next up, we add some rollover and rollout events (the listeners for which we’ll add in the next step). Finally we set some properties to make the object behave more like a button.The only part that might be causing confusion is the call to
addChild(), and where themouseChildrenandbuttonModeproperties came from. I’ll ask you to yet again wait for a more complete answer, as that will come in the next installment, but it has a lot to do with the “extends Sprite” we wrote earlier.Step 18: Add Some Methods
Now, we can continue working on our class. After the constructor, create a function called
setLabel:public function setLabel(label:String):void { labelField.text = label; }And finally, create the two event listeners:
private function onOver(e:MouseEvent):void { bgd.alpha = 0.8; } private function onOut(e:MouseEvent):void { bgd.alpha = 1; }The final code should look like this:
package { import flash.display.Shape; import flash.display.Sprite; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFormat; public class Button101 extends Sprite { private var bgd:Shape; private var labelField:TextField; public function Button101() { bgd = new Shape(); bgd.graphics.beginFill(0x999999, 1); bgd.graphics.drawRect(0, 0, 200, 50); addChild(bgd); labelField = new TextField(); labelField.width = 200; labelField.height = 30; labelField.y = 15; var format:TextFormat = new TextFormat(); format.align = "center"; format.size = 14; format.font = "Verdana"; labelField.defaultTextFormat = format; addChild(labelField); addEventListener(MouseEvent.ROLL_OVER, onOver); addEventListener(MouseEvent.ROLL_OUT, onOut); mouseChildren = false; buttonMode = true; } public function setLabel(label:String):void { labelField.text = label; } private function onOver(e:MouseEvent):void { bgd.alpha = 0.8; } private function onOut(e:MouseEvent):void { bgd.alpha = 1; } } }One thing that is very important is to make sure your methods are declared at the class level. This is another rookie mistake: sometimes people don’t pay attention to where their curly braces start and stop, and end up nesting what should be a method (that is, declared at the “root level” of the class) inside another method. This effectively turns a method (which should persist for the life of the object) into a local function (which only persists for the life of the running method in which it was declared).
Hopefully compiler errors will notify you of such a mistake, but the error itself might be cryptic. It would say “1114: The public/private/protected/internal attribute can only be used inside a package.” If you get that, check the nesting level of your methods. Another thing to help prevent these errors is to pay careful attention to whitespace and indentation. A “real” text editor can help you with this, but just make sure all of your method declarations are at the same indentation level (typically two tab stops).
What we have now is a class ready roll, which, when instantiated, will create a customized display object that has a gray rectangle with a label. The label can be set using a method and the button will automatically respond to mouse roll overs and roll outs. All we need to do is actually instantiate it.
Step 19: Create a Button
Step back to your document class, and we’ll add a button to the movie. This will be pretty easy compared to the previous step.
First, we’ll add a new property to hold the button. Stick it below the line with your existing property.
It’s not required to put all properties together, but for the organization of your file, it’s helpful to keep things in consistent places. Personally, I always list properties at the very top of the class, before the constructor. This way, I always know where to go to find a property declaration. Where they go in the file isn’t as important as coming up with your own standard and sticking to it.
Moving on, we can now instantiate the
Button101in the document class’s constructor:public function DocumentClass() { tf = new TextField(); addChild(tf); tf.text = "Hello World"; button = new Button101(); button.x = 10; button.y = 200; button.setLabel("Button 1"); addChild(button); button.addEventListener(MouseEvent.CLICK, onButtonClick); //stage.addEventListener(MouseEvent.CLICK, onButtonClick); //we have removed this line }Note that we modified the line that set up a listener for clicks on the stage. We’re shifting that interactivity to the button.
Now, you may be wondering how it is that we’re treating this
Button101object as if it were a Sprite or MovieClip (see the AS3 101 tutorial on the Display List if you weren’t wondering that). This has everything to do with thatextendsbusiness I’ve asked to gloss over for now. I promise, we’ll get to it soon (in the next part of this series).At this point, we should be able to test it out. It should work similarly as before, except that you’ll see the button appear, and instead of clicking anywhere on the stage, you need to click on the button.
For reference, here is the complete document class at this point, with additions and changes in bold:
package { import flash.display.MovieClip; import flash.text.TextField; import flash.events.MouseEvent; public class DocumentClass extends MovieClip { private var tf:TextField; private var button:Button101; public function DocumentClass() { tf = new TextField(); addChild(tf); tf.text = "Hello World"; button = new Button101(); button.x = 10; button.y = 200; button.setLabel("Button 1"); addChild(button); button.addEventListener(MouseEvent.CLICK, onButtonClick); } private function onButtonClick(e:MouseEvent):void { tf.text = "Hey, you clicked!"; } } }So, what did that accomplish us? The big thing is that we got a pretty nifty button that we could use with very little effort (not counting the effort put into writing the
Button101class in the first place).To accomplish the same functionality without classes and OOP, we’d have to write considerably more code in our main script. To illustrate this further, let’s create a second button.
Step 20: Create Another Button
OK, now let’s let OOP really shine. We’ll create a second instance of the button, merely by adding a few more lines of code to the document class.
The changes involved will be extremely similar to the previous step. First, add a property:
Then set it up in the constructor:
public function DocumentClass() { tf = new TextField(); addChild(tf); tf.text = "Hello World"; button = new Button101(); button.x = 10; button.y = 200; button.setLabel("Button 1"); addChild(button); button.addEventListener(MouseEvent.CLICK, onButtonClick); button2 = new Button101(); button2.x = 220; button2.y = 200; button2.setLabel("Button 2"); addChild(button2); button2.addEventListener(MouseEvent.CLICK, onButtonClick); }You can, in fact, just copy and paste the first button’s lines of code and just change
buttontobutton2, and also thexposition of the second button (otherwisebutton2will sit right on top ofbutton1).We’ve doubled the complexity of our application in just a few short lines of code, thanks to the reusability of the
Button101class. We have the class, which, if you remember, is the blueprint from which the actual instances are created. We now have two distinct buttons, yet they share that common “heritage.” Even though there are many characteristics that are the same between these two buttons, like the rollover effect and the size, there are still aspects of individuality, namely the position.Hopefully this starts to illustrate the usefulness and power of Object-Oriented Programming, in which we facilitate the reuse of code very easily.
Go ahead and test it out; you should have two buttons that do the exact same thing.
Naturally, you’ll probably want to tweak the
Button101class so that it looks fancier, or has a slicker roll over effect, or what-have-you. By all means, take theButton101class and make it more useful for your own needs. Our goal here was to demonstrate how a button class can be made, not to make the ultimate button class.Step 21: Summary
There were two main themes in today’s tutorial. To recapitulate, these are: the idea of blueprints (classes) and houses (instances); and the notion of scope.
Classes are the code files that we write, as well as the concept of an entity that serves as the blueprint for the objects that will get created from them. One class can beget any number of instances. Objects are the instances of classes, and tend to be the things that, when put together, comprise our application. Instances of the same type share functionality in common, but retain unique identities and can possibly contain variances in the data contained within.
Scope is an important concept through all of programming, and no less so in Object-Oriented Programming. Scope determines the “life span” of a variable or function, and also a “place to live.” A variable at the object scope has a different life span than a variable at the method scope.
Until Next Time
This is, however, just scratching the surface of the tip of the iceberg. It’s all we have time for right now, but in the soon-to-be-published Part 2 of the AS3101 OOP series, we’ll dive a little deeper and finally get an explanation on all of things I was asking to put aside, like
extendsandpublic. Things will begin to make more sense. But when it comes to a complicated topic like Object-Oriented Programming, it’s best to take things slow.Fans of the Activetuts+ Facebook page can now access a new bonus tutorial – in this month’s Facebook Fan Bonus, you’ll learn how to remove the background color from a sprite sheet using AS3, and blit the result to a bitmap canvas.
Final Result Preview
Here’s a look at the kind of effect you’ll be working towards in this tutorial:
Download This Fan Bonus Now!
All you have to do is Like us…
Not On Facebook?
Don’t worry, the tutorial will be posted on Activetuts+ in a month’s time!
I can’t tell you how many articles and tutorials I have come across on how to use symbols in Flash that immediately dismiss the graphic symbol as having no practical purpose, relegating it as just a step above grouping items. This article will attempt to dispel this myth by showing that the graphic symbol actually has some pretty cool and convenient features and knowing how and when to utilize them is a nice tool to have when you’re creating animations in Flash.
I first began using Flash with version MX. And through all the enhancements and added features in every release, one thing that has remained constant is the graphic symbol. But what has also remained constant, surprisingly enough, is how many Flash users don’t know what the graphic symbol actually does. Somewhere along the line, this symbol has received a bad rap as being totally useless. If you’ve ever wondered what exactly the purpose of the graphic symbol is and why the heck Adobe continues to keep it in Flash, this article is for you.
The Basics
We’ll start off with the most known and basic of things you can do with the graphic symbol that are inherent to all Flash symbols. Just like movie clips and buttons, a graphic symbol can have color effects applied to it like alpha and tint, get broken apart into its comprising elements via CMD/CTRL-B, and of course have its properties motion-tweened.
I know that’s far from a revelation, but I mention these attributes because I think it’s important to keep in mind that a graphic symbol at its core is just like any other Flash symbol but with its own unique set of features.
Animation Misconceptions
One of the biggest misconceptions about the graphic symbol is that it cannot contain animation – or that, if it does, Flash will just ignore it at runtime. Not only is that completely untrue, but in actuality graphic symbols are very powerful and flexible when it comes to creating timeline animation.
Just like a movie clip, a graphic symbol can contain nested animations on top of nested animations of other graphic symbols and movie clips. It is the symbol of choice used by most professional Flash animators, especially when it comes to creating complex timeline animation with lots of vector assets. Let’s see why.
Animation and the Properties Panel
The true power of the graphic symbol lies within the Properties panel. This is where you can control the animation inside a graphic symbol, which is one of the hidden, unknown jewels in Flash. The approach that I am about to detail is really an animation technique referred to by many as the Chris Georgenes method.
If you’re not familiar with Chris Georgenes, he is a well-respected Flash animator who innovated this approach to animating with graphic symbols and I was lucky enough to take a character animation course he was the instructor of. I highly recommend you read up on many of his Flash animation techniques and tricks.
That’s enough background information so let’s get cracking with the good stuff. When you create a graphic symbol (Modify > Convert to Symbol), you have three options for handling how the symbol will play back its animation; to see them, with the symbol selected, go over to the Properties panel and look under the Looping category.
When you click on the Options drop-down, you will be presented with three options to choose from: Loop, Play Once and Single Frame. Also below the drop-down, you’ll see a small input field labeled ‘First’. Before we get to that, let’s understand the three looping options – but keep that little input field in mind as we go through each option as it will all tie together.
Note: If you don’t see the Options field, just click on the arrow to the left of Looping to reveal the drop-down.
Loop: Loop is the default setting for every new graphic symbol that is created. Basically this option will constantly loop any and all animation that is nested inside it. So in other words, the symbol’s timeline will play through its duration and then start over again, just like a movie clip.
Play Once: This setting is pretty self-explanatory as well. It will play the symbol’s timeline once through and then stop. Think of it as the graphic symbol’s way of adding a stop() action on the final frame of its timeline.
Single Frame: Single frame allows you to display only a certain frame within the symbol’s timeline. Whereas the symbol’s timeline is actually playing when it’s set to Loop or Play Once, the Single Frame option just jumps the timeline to a specific frame within the symbol and stays there. I like to think of it as the graphic symbol’s version of using
gotoAndStop(frame). So how is this done? If you’re recalling that tiny input field labeled ‘First’ that I told you to keep in mind a few paragraphs back, you’re heading in the right direction.First Frame Input Field
Though very ambiguous and easily unnoticed, this input field is where all the fun happens for controlling the timeline of a given symbol. This is one of my favorite features of the graphic symbol because you can target a specific frame on the symbol’s timeline by typing a frame number in the input field.
When a graphic symbol is first created, the first frame input field is set to 1 by default – as denoted by the 1 inside the field. This means frame 1 on the symbol’s timeline will be the first frame the animation will begin on. That’s where the label First comes in; think of it as the first frame you want to display or the first frame you want to play from. So if you have a symbol set to Single Frame and type 6 in the first frame field, it will display whatever is on frame 6. If the supplied frame number is blank or if the symbol doesn’t even have that many frames in its timeline, then nothing will display.
You can also use the first frame field with the Loop and Play Once options. If you set a symbol to Play Once and type in a certain frame number in the input field it will start from that frame. The same applies if you set it to loop. The symbol would begin to loop starting with the frame number you supplied in the input field. Keep in mind though that if you typed in 6, it will start to loop from frame 6 but once it reaches the end of the timeline it will continue to frame 1 in its usual manner.
But It Still Isn’t Animating
Now, if you took it upon yourself to try any of this out within Flash, you’re probably wondering why your animation isn’t playing in the manner I’ve been describing. Open up the file named looping_basics.fla in the source package. Double-click the graphic symbol that is on the stage to check out the animation that’s inside of it. You’ll see two layers spanning 239 frames on the timeline, with a new keyframe on both layers every 30 frames (i.e. every second). So if you scrub the play head, you’ll see we basically have an 8-second animation in which on every second the number in the center increments by one and the circular background changes color. By all means nothing fancy.
After you have the gist of the animation, return to the main timeline, click on the graphic symbol and check the properties panel to make sure the symbol is set to Loop and 1 in the first frame field. Now test out the movie.
What’s going on here? Our symbol isn’t animating even though we have it set to Loop. Right about now you might want to say you told me so about Flash just ignoring a graphic symbol’s animation. Before you do so, let’s get to the bottom of this.
It’s All About the Timeline
This is the common pitfall most people experience when they create an animation inside a graphic symbol. And it’s probably the reason why the symbol is thought of as useless. You create an animation on the graphic’s timeline then position the symbol on the main timeline just like you would with a movie clip. But when you go to export your movie, the symbol just sits there static on the stage. This is where the misconception lies: we are expecting the graphic symbol to behave just like a movie clip.
The trick to the graphic symbol is that it is timeline driven – meaning its own timeline coincides with the timeline the symbol is placed on. So if it’s placed on the stage, it needs just as many frames on the main timeline as it has on its own timeline in order to play the animation in its entirety. In other words, whereas a movie clip will play independently of the timeline, a graphic symbol will be played only as far as its parent’s timeline will allow it.
In a nutshell, a graphic symbol needs frames.
Give It Some Frames
If you go back to our example in Flash, the reason our graphic symbol isn’t playing is because there is only one frame on the main timeline. So let’s add some frames. Just for demonstration purposes, go to frame 30 and add frames. Now go to frame 60 and do the same. Do you see what’s happening here? Then go to frame 90. Very cool, huh? Keep adding as many frames as you want and test the movie. You should see the animation begin to play, depending on how many frames you have on your main timeline. Remember our symbol has 239 frames on its timeline, so in order to see the entire animation, there needs to be the same amount on the main timeline.
Note: Keep in mind this principal we’ve been discussing doesn’t just apply for the main timeline. The timeline that the graphic symbol is actually placed on will dictate its playback. So for example, if your graphic is inside a movie clip, you need frames on the movie clip’s timeline to render the graphic’s animation. Try it out yourself.
Automatic Playback
As you probably noticed from the example file, you can actually see the graphic symbol animate as you scrub the timeline from the stage. Yes, very cool. This brings us to another unique feature that differentiates a graphic symbol from a movie clip and you’ll probably find it to be its most useful. Since the graphic symbol is timeline driven, a benefit of that is having your animation automatically update within Flash. As long as you have ample amount of frames on the timeline where your symbol is located, you can always see your graphic symbol animate live.
Case Study: Facial Expressions
Let’s explore these techniques a little more to give you another look on how you can incorporate a graphic symbol into a project. Open up the file named facial_expressions.fla that’s in the source package. You will be greeted with a nice smiling face on the stage that was part of an illustration for an animated character I created a few years back.
If you click on the symbol, you’ll see it’s a graphic with a library name of expressions, and that it is set to Single Frame 1. Go inside the symbol and you’ll see three layers: eyes, mouth and head, each of which contain graphic symbols. The head layer contains all the other elements that comprise this little guy’s smiling face, and if you unlock the layer and double-click the symbol you’ll see are all graphic symbols as well. For our purposes, though, we are going to be focusing on the graphic symbols for the mouth and eyes. Go inside the mouth symbol and you’ll see it contains various different mouth shapes at various points on the timeline, and the same goes for the eyes. Let me point out that the left and right eyes are instances of the same exact symbol.
Go inside both the mouth and eye symbols and take a look around. You’ll notice that I have a frame labels layer corresponding to each facial expression and that I chose to put each of them on a keyframe that are 10 frames apart. There’s no reason for having the timeline set up like this other than it’s my own way of trying to keep it clean and organized. I probably wouldn’t use the frame labels in ActionScript (though I could if I converted my graphic to a movie clip) but having a label for every new expression lets me know at a glance where a certain expression is on the timeline. And besides giving me some space so the labels can actually be read, the 10 frame gap between each keyframe simply makes it easy for me to remember where a new mouth or eye expression occurs on the timeline.
Now jump back one level to the expressions graphic where we just have our three layers of the mouth, eyes and head, and start using these symbols to create some animation. There’s a lot of different ways you can set things up here, depending on what you want to do, but let’s just keep it simple and make our guy blink.
Insert a new frame on all three layers. Check that both the mouth and eye symbols are set to Single Frame 1 and then insert a new keyframe on the eyes layer and set both eyes to Single Frame 10.
Go back to the stage, give yourself about 45 or so frames, make a new keyframe on frame 30 and change the 1 in the first frame field to a 2. Remember your keyframes should be set to Single Frame. Now test it out and your guy should be blinking. I know that’s nothing fancy but the point here isn’t so much about what can be created with the graphic symbol, but rather about the convenience and flexibility it provides you. We’ll get to more on this in a bit but hopefully this example has your gears turning.
When Would You Use a Graphic Over a Movie Clip?
Personally, any time I am working on a project that calls for creating numerous timeline animations, I consider using the graphic symbol. Any Flash user who has ever spent countless hours tinkering with numerous movie clips that all contain their own animations has probably run into a situation where they wish they could pinpoint where something was occurring on a certain movie clip’s timeline from another spot in the movie. From my own experience, there have been many times in which I have an animation inside one movie clip and want to use it within another clip but needed to know when a certain point in my animation was reached so I can have something else happen. I would have to resort to a tedious process of jumping back and forth between movie clips and counting frames so I could figure out where to place it on that second clip’s timeline. Using my animation as a graphic would allow me to see everything without having to export my entire movie.
Tip: Even when I am using a movie clip for a timeline animation, I sometimes temporarily set it as a graphic in the properties panel just so I can view the animation playback while other objects are animating around it. I can then get a better sense of the overall timing and do some tweaks; when I’m done, I set it back to act as a movie clip.
To do this, just click on your symbol, go to the top of the Properties panel and click on the drop down. This is where you can change your symbol’s behavior on the fly, toggling it between a movie clip and a graphic. This is very useful when you want to use the same symbol as both a movie clip and a graphic throughout your project. Note: This technique will not work if you try to change between a movie clip and a button or between a graphic and a button.
Practical Uses
Here are some other practical uses of when you might want to consider using a graphic symbol:
Animated Presentations: Presentations are the projects where I find myself using graphic symbols the most. Most flash presentations tend to be heavy on animation and syncing visuals to a script. The graphic symbol lends itself well to projects like these that are in linear in nature and have limited user interaction.
Syncing audio with an animation: This is an extension of the previous point. Anytime you want pinpoint synchronization between a sound and an animation, running your audio alongside your animated graphic symbol will give you that type of control. Many voiceover presentations that I have been involved in have employed animated graphic symbols.
Character animation: Anytime you are going to create intricate animations with vector artwork that has lots of layers, the graphic symbol is the way to go. Many cartoon animations have been made with flash and I would bet anything that if you were to examine the FLA they were made in, the library would be packed to the brim with graphic symbols. Just like facial expressions, creating sequences for walk cycles and lip syncing sequences are ideal jobs for using the graphic symbol.
Creating multiple variations of a symbol: We already touched on this notion throughout the article but I think it’s worth further explanation. If you recall in our facial expressions example from earlier, the mouth and eye graphic were used to house all our different mouth and eye shapes for easy animating. Well, sometimes I use the graphic symbol to just hold a bunch of similar movie clips that are just slightly different, or what I like to refer to as poses.
In some cases, I’m not looking to sequence these poses for an animation like in the facial expressions example but to act as a holder of my elements, which will simplify the process of accessing them later on. I find it much easier to refer to each pose if they are all on their own frame inside a graphic instead of having to sift through one long movie clip timeline with numerous layers. All I have to do is target the frame number of the one I want inside the first frame field like we did in the facial expressions example. And if I need to control any of my poses with actionscript, I just turn my graphic into a movie clip.
Limitations
The graphic symbol is not without its limitations and drawbacks. While the graphic symbol can be very useful in certain situations, by no means am I advocating it as a replacement to the movie clip. In most situations, a movie clip will be your symbol of choice and will do just fine even for creating most timeline animations. Here are some things to keep in mind when using the graphic symbol:
Can’t be controlled by ActionScript: This is an obvious point but it’s a biggie. Unlike a movie clip, you can’t assign a graphic symbol an instance name and any script you place on its timeline will be ignored. Keep in mind though you can still use a movie clip that has ActionScript on its timeline as a graphic and it will play back normally.
Can’t apply filters: For some reason, Flash doesn’t allow you to apply any of the filters or blending modes to a graphic symbol. To get around this, you can simply put your graphic symbol inside a movie clip and then apply the filter to the clip.
Classic tweening graphic symbols with nested animation can be problematic: This is one I learned the hard way and it’s an issue that can happen quite easily without you even realizing. Let’s say you have a graphic symbol with a pretty involved animation inside it, and you want to tween the symbol on the main timeline from left to right across the stage as your nested animation plays. You create your first keyframe on the timeline and you set it to Play Once starting on frame 1 in the Properties panel. When you create your second keyframe, Flash will automatically set it to Play Once and update the first frame field accordingly with the correct frame number in sequence.
So using the above example if your first keyframe is on frame 10 on the main timeline and your next keyframe is on frame 40, the graphic symbol on this frame would be set to Play Once with the first frame field already set to 30. Get it? It inserted the frame number you left off from? Now if you were to add or delete frames within the tween, the symbol on that second keyframe will still have 30 set as its first frame to play from thus completely throwing off the order of your nested animation. It’s just something to keep in mind; when you initially create keyframes, the first frame number will update correctly, but once you start moving keyframes up and down the timeline, things will get thrown off.
Note: This only applies to classic tweens. If you are using the method for creating motion tweens that was introduced in Flash CS4, this won’t be an issue.
Conclusion
Hopefully after reading this article you have a different opinion of the graphic symbol. Though it may not be something you can see yourself using all that frequently, the thing to keep in mind with the graphic symbol is that it’s about knowing when to use it. As a developer, I don’t use it all that often but I know I have it as an option if the situation calls for it. Like with most things in Flash, it comes down to a matter of personal preference. If you pick and choose your spots wisely, using the graphic symbol can be convenient and a time-saver.
I know a lot was covered in this article, but the best way to really understand what the graphic symbol can do is to spend some time playing around with it in Flash. Change its looping options, target different frame numbers to start your animation on and toggle back and forth between using it as a graphic and a movie clip to really see what works and what doesn’t. So maybe the next time you hit F8 and you are planning on creating a timeline animation, remember the graphic symbol might not be that useless after all.
In this Quick Tip, you’ll learn how to construct a reference to a class from a String, and then create an instance of that class, using a built-in AS3 function called
getDefinitionByName(). You’ll also learn the best methods for using this in different situations.Why Is getDefinitionByName() Useful?
getDefinitionByName()is very useful if you need to make new instances of classes by using aString. For example, if you had seven different tiles – each represented by a class calledTile1,Tile2, etc – and you needed to create an instance of each, you would have to write the following code:private function createTiles():void { var tile1:Tile1 = new Tile1(); var tile2:Tile2 = new Tile2(); var tile3:Tile3 = new Tile3(); var tile4:Tile4 = new Tile4(); var tile5:Tile5 = new Tile5(); var tile6:Tile6 = new Tile6(); var tile7:Tile7 = new Tile7(); stage.addChild( tile1 ); stage.addChild( tile2 ); stage.addChild( tile3 ); // You get the idea, it is very lengthy! }getDefinitionByName()allows you to solve this problem!How to Use It
Now, the above code was a little messy and we had to type many lines just to make a few different tiles. This is how we could achieve the same goal using
getDefinitionByName():private function createTiles():void { for( var i:int = 1; i < 8; i++ ) { var tileRef:Class = getDefinitionByName( "Tile" + i ) as Class; var tile:Sprite = new tileRef(); stage.addChild( tile ); } }In line 6,
getDefinitionByName()returns a reference of the class called “Tile+ the current value ofiin the for-loop“. So, wheniis equal to 1,getDefinitionByName("Tile" + i);returns a reference to the classTile1. We then create the tile and add it to the stage.(We can’t write
var tile:tileRefbecausetileRefdoes not refer to anything at compile time; if you try, you’ll get a compiler error.)However, when you run this code, it will not work! You’ll get a
variable is undefinederror message, in most cases, because “Tile1″ might not be enough information for Flash to find the class. Let’s look at some workarounds.Make It Work
There are a few commonly-used methods to solve the problem of the
variable is undefinederror you’ll get when you run the above code, and I am going to teach you what they are. I would also like to give credit to Gert-Jan van der Well of Floorplanner Tech Blog for this blog post.Here are some of the methods you can use:
Using a Dummy Variable
In this method, you just create some dummy variables with references to the classes you want to refer to with
getDefinitionByName()later:private var dummyTile1:Tile1; private var dummyTile2:Tile2; //etc private function createTiles():void { //Create the tiles }This works, but it is very ugly. If you have the Tile classes in another package, you would also have to import them!
Short Notation
This is much like the Dummy Variable method, but you don’t bother setting up a dummy variable for each class; you just drop a few explicit references to the classes themselves:
Tile1;Tile2;Tile3;Tile4;Tile5;Tile6;Tile7; //etc private function createTiles():void { //Create the tiles }Now, this may look neater, but the fact that you will have to update this list everytime you make a new
Tileremains.Including the Full Path Name
Another method, which is the tidiest (if you have classes in another package) is to include the full path name in your String:
//Let's say my Tiles are all in the package 'project.Tiles' private function createTiles():void { for( var i:int = 1; i < 8; i++ ) { var tileRef:Class = getDefinitionByName( "project.Tiles.Tile" + i ) as Class; var tile:tileRef = new tileRef(); stage.addChild( tile ); } }Much tidier! However, this only works if the classes are in a separate package from this class.
Using a SWC
If the Tiles are held in an SWC, you can make this much easier, without needing to use any imports or dummy variables. I would like to give credit to v0id from Dreaming in Flash for this blog post that explained to me how to use this method:
include-libraries PATH_TO_SWCThe PATH_TO_SWC must be the absolute path and not the relative path!
Great, all these methods have now been explained. Unfortunately, there are no fantastic magical methods to use if you have all the tiles in the same package as all the other AS Files. I would recommend you make a new package called
Tilesor something if you want to use the good methods!Conclusion
Well, today you should have learnt how to use
getDefinitionByName()and the best methods of using it. I hope this will help you in any future projects and if you have any questions, leave them in the comments section below!Hey folks! I’m Jordan McNamara, the Community Manager for Envato and I wanted to share some exciting news with you about the Envato Marketplaces.
The 12th of September was an exciting day for me as a member of our thriving Marketplace community. I along with everyone here at Envato HQ watched eagerly as the global Marketplace member count steadily grew higher and higher and higher…
We were close, very close to the 1,000,000 member milestone. With every page refresh the count grew higher and I began to reflect on just how staggering having 1,000,000 members really is and on just what a fantastic community everyone has helped to build here.
I remember starting at Envato in 2009 and being blown away by the 250,000 or so accounts that were registered at the time. I bragged to all of my friends “Can you believe there’s a quarter of a million members!?”. Little did I know in just under two and a half years I would be writing this post!
View the Infographic!
To celebrate this exciting occasion we wanted to do something interesting and fun that celebrates the Marketplace community and the sites themselves. So with that in mind, we are proud to present the 1,000,000 Members Infographic! We’ve packed it full of fascinating facts and data about our members and our nine Marketplaces.
Every day we continue to be amazed at the astonishing speed our community of creative members grows. We’ve enjoyed every minute of the journey and are proud and thankful of the achievements our members have made. Here’s to the next million!
View the Infographic!