Tutorial: How to make a top-down shooter in JavaScript

Lesson 7) Collision detection

Two worlds collide

This lesson's a little longer than the previous ones. We'll go over some handy things you can do with arrays, and then move on to collision detection.

So, we've previously written code allowing us to move a rectangle around the screen with the w, a, s, and d keys. To turn this into a simple game, we can just put other rectangles around the screen and have our player collect them in exchage for points.

Admittedly, that might not sound too enthralling, but throw in bad guys to kill and obstacles to navigate, and you've got the basis for a lot of classic games. In Sonic, for instance, you're a blue hedgehog that goes around killing baddies and collecting rings. If it helps, you can think of yourself as a square red hedgehog that goes around collecting square coins!

Heads up big guy!

First let's create a little HUD for the player. Every game has one of these, to display things like points, health, ammo, a map, and so on. Ours will just show points and the game time remaining.

First we need to define a variable for the time. Let's make this a countdown game - the player will collect as many points as they can in a given time limit, say, 30 seconds:
var timeRemaining = 30;
And we'll update the Player constructor to have a points property:
function Player () {
this.x = 395;
this.y = 295;
this.w = 10;
this.h = 10;
this.points = 0;
}
Now let's make the time tick down. At the end of the mainDraw function add the following line:
timeRemaining -= 0.02;
This deducts 0.02 from timeRemaining every loop, which works out as -1 per second at our present setInterval rate (We're looping every 20 milliseconds. One second = 1000 milliseconds. That's 1000 / 20 = 50 loops per second. We deduct 0.02 every loop, so that's 50 * -0.02 = -1 from timeRemaining every second).

Now to write text onto the canvas. We use the c.font method to choose our font and text size, c.fillStyle to choose a colour, and c.fillText to indicate what we want to write and where. So let's use these to make a little HUD:

function drawHUD() {
c.font = '18pt Calibri';
c.fillStyle = 'black';
c.fillText("Points:", 10, 25);
c.fillText("Time remaining:", 120, 25);

if (timeRemaining < 10) {
c.fillStyle = "red"
}
c.fillText(Math.ceil(timeRemaining), 290, 25);

c.fillStyle = 'yellow';
c.fillText(Player1.points, 85, 25);

}
Let's walk through this. Our function is called "drawHUD". We set the font to Calibri at 18pt size with c.font. We use c.fillStyle to make the text black and we write "Points:" and "Time Remaining:" at the x and y coordinates specified after the text.

Now look at the if statement. If the timeRemaining is less than 10, that is, there's less than 10 seconds left, we set the font colour to red, warning the player to get a move on.

Then we write the variables that we declared earlier onto the screen. We've put timeRemaining inside Math.ceil() - this rounds the figure we write on the screen up to the nearest whole number, so we're not showing all the decimal places to the player.

Next, we switch the font colour to yellow and write Player1's current points score to the screen.

The result will look like the screenshot below:

Image of what the HUD looks like

Gotta collect 'em all!

Now, let's add in the coins. To add in a tiny bit of skill to the game, we'll draw three coins, gold, silver and bronze, giving 10, 5 and 2 points to the player on collection. The gold one will be smaller, making it a little harder to collect, and the bronze one will be the biggest as it gives the fewest points.

Everytime the player collects a coin, the three coins will be drawn again in new random locations. The aim is to get as many points as you can in 30 seconds. So, this is a game where success is highly based on luck - basically, whether gold coins happen to spawn near to you or not.

However, there is a slight element of strategy here, an aspect of decision making required from the player: do you always go for the lucrative gold coin, even if it's further away? Do you always go for the closest coin, regardless of value? Or do you make that decision on the fly?

There exists an optimal strategy for this game, but you're never guaranteed to get a high score - the random element means all three coins might continuously spawn at opposide edges of the screen every time, rendering all strategies useless. If this game was played in tournaments, players would have to face off against each other many times, and average all their scores, to truly find out who was playing better.

Does that make this a bad game?

Well it may indeed be a bad game, but not for this reason, I'd argue. Just because a game is highly luck-based like this doesn't meant it's not a good game. Well, OK, "good" may be the wrong word (and highly subjective) - "addictive" might be a better choice. Even if your score is mostly down to chance, if you feel like it's skill, and you feel like you could do better next time, you'll keep coming back for more.

This is the basis of a lot of games. Take Candy Crush - HUGE game, but success in it is mostly random. Yes, the decisions you make each turn are objectively good or bad in that moment, but even a perfectly played game can end in failure. Success largely depends on which new candies enter the screen when you clear some away. But you know when you've played well, you know when you've made good choices. That gives you a feeling of competence - you feel like you can do better next time. That's what makes it so addictive.

Anyway, I digress, this tutorial is about game programming, not game design. Back to our coins!

We'll hold them in an array, so let's define it first:
var theCoins = [];
...and then make a function to create some coins:
function createNewCoins() {

theCoins.push(
{
x: Math.floor(Math.random() * 800),
y: Math.floor(Math.random() * 600),
w: 10,
h: 10,
points: 10,
color: 'yellow'
}
);

theCoins.push(
{
x: Math.floor(Math.random() * 800),
y: Math.floor(Math.random() * 600),
w: 25,
h: 25,
points: 5,
color: 'grey'
}
);

theCoins.push(
{
x: Math.floor(Math.random() * 800),
y: Math.floor(Math.random() * 600),
w: 50,
h: 50,
points: 2,
color: 'brown'
}
);
} // createNewCoins
We're using the "push" method to, yes, push something into an array. The curly brackets indicate we're pushing an object. It's has x and y properties which are randomly generated, w and h properties for width and height, a points property (the reward for collecting it, and a color.

You'll notice the syntax is a bit different from how we defined the player constructor earlier. This is an object literal - note the colons instead of equals signs, commas instead of semi-colons, and there's no "this" needed here.

I preferred to use object literals here because you don't need to define a new variable to use them. Remember we used "var Player1 = new Player()" earlier? We don't need that here. We can just push a nameless object to the array, and refer to it based on it's position in the array.

Test this youself. Try adding "console.log(theCoins[1]);" and "createNewCoins();" to mainDraw and check the console. You'll see the details of the silver coin. Why silver? Why not the gold coin? After all, that was the first thing we pushed to the array. Remember, an array's index starts at 0, not 1. So asking for whatever is at index position 1 actually returns the second thing you pushed to the array. If this is the first time you've come across that concept, you'll probably think it's nuts. But it makes certain types of arithmetic easier, and from a games point of view it's very handy whenever you have to link objects in an array with screen coordinates, since they also start at zero. For example, level maps can be coded by breaking a 2D map into tiles, and holding the contents of each tile in an array.

Coins, forEach and every one of you!

We've created the coins, so now we need a function to draw them on the screen. Let's be really creative and call it "drawCoins":
function drawCoins() {
theCoins.forEach( function(i, j) {
c.beginPath();
c.fillStyle=i.color;
c.strokeStyle=i.color;
c.rect(i.x, i.y, i.w, i.h);
c.lineWidth=1;
c.stroke();
c.fill();
});
}
Arrays come with som useful built-in methods. You saw the "push" one in action earlier on. Now you're getting a look at "forEach", which is one of my favourites. forEach loops through every object in the array in turn, and executes a bit of code you specify each time.

Note the "function(i, j)" bit. Here's what that means:

So we have three things in the array, at index positions 0, 1, and 2.

First, forEach goes to position 0, and runs our bit of code. At this point, i equals our gold coin object and j equals 0. So all the following lines of code would return "yellow":

console.log(theCoins[j].color);
console.log(theCoins[0].color);
console.log(i.color);
So when we're saying draw a rectangle at i.x, i.y with height i.h and width i.y, it will take these values from the coin objects we defined earlier. Therefore this function will draw all of our coins on the screen without us having to write out the code three times.

Calling new coins, come in, new coins

So from where shall we call these functions?

We can't call them both from mainDraw. If we did we'd be pushing three new square coins to the array and drawing them all every loop. The screen would fill up and the game would quickly become very easy (try it if you like... it's kind of cheating though).

Let's call createnew Coins just before setInterval. That way our program will run once, just before it gets stuck in the game loop:

// set the game loop interval
createNewCoins();
setInterval(mainDraw, 20);
Then we just add drawCoins(); to mainDraw, and the code will make our coins appear. Whenever the player collects a coin, we simply delete the other two, run createNewCoins again, and the chase continues.

Which leads us to our main of the lesson - collision detection!

Do I detect a hint of collision in your program?

Collision detection gets pretty complicated when you're dealing with polygons of varying shapes, circles, not to mention 3D objects. But as this is a beginner's tutorial we'll stick with the simplest type - collision between two rectangles. As our game is 2D, a collision occurs when two rectangles overlap, no matter by how much. How do we check for this?

Have a look at the collision scenarios below:

Collision scenarios

Here we see blue colliding with red by overlapping one of red's corners each time, and in the middle blue is completely inside red. Let's label each edge of the boxes top, right, bottom, and left. In each of the above collision scenarios, ALL of the following are true:

On the x axis:
red.left < blue.right
red.right > blue.left

On the y axis:
red.top < blue.bottom
red.bottom > blue.top

If even one of those statements is not true, the boxes do not overlap, and there is no collision:
No collision scenarios

So let's put this little rule into a function:

function collides(a, b) {
return a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y;
}
We've called it "collides", and told it to accept two parameters, a and b. Anything that we pass to the function will be renamed as a and b within it.

The function checks whether those four conditions of collision are all true. For example in the first line, it checks whether a's left side is less than b's right side (i.e., a's x coordinate vs b's x coordinate plus it's width).

Putting all these statements (together we call them an expression) after a "return" commmand makes JavaScript evaluate the expression to see if it is true or false. For example if we put "return 1=1" it would return "true", because 1 does equal 1. If we put 1=2 it would return "false", because that expression is false. We're just expanding this concept by using "&&" to check whether four statments are true.

So, if we wanted to see if the player has collided with the gold coin, we'd use the following line:
collides(theCoins[0], Player1);
Get it? I hope this makes sense.

So when do we check for collision? Well because the player can move in every cycle of the game loop, we also need to check for collision every cycle. Let's once again call upon our useful friend, forEach:

function checkCollision() {
theCoins.forEach( function(i, j){
if ( collides(i, Player1) ) {
Player1.points += i.points;
theCoins.splice(0);
createNewCoins();
}
});
}
This function cycles through each of our coins, and runs an "if" statement each time - if ( collides(i, Player1) ). JavaScript runs the collides function and checks what it returns. If collides returns "true", it will run the code inside the if statement. If not, it will skip that code and move on to the next coin.

And what code does it run, if true? It adds the coin's point's value to the player's points score, then runs another very useful array method - splice. Splice is used to remove items from arrays. If you use "splice(0)", it clears the whole array, which is what we've done here. Then we run createNewCoins again.

You might think it's a bit wasteful to have to cycle through each coin (technically, each edge of each coin) in every single game loop, but computers are fast enough to handle it, even with many more obejcts on the screen than the four we have here. That said, there are ways to speed it up. For instance, you could break the screen up into four quadrants, and only check for collisions between objects that are in the same quadrant. But we won't need that for our simple game.

Now we just call checkCollision to the game loop (you should know how to do this by now), and we have a fully functioning collision detection system in our game.

It's about time

Let's wrap this up by going back to our timer variable. Remember that? When we added timeRemaining to our HUD, displaying the timer left at the top of the screen? I know, it seems like so long ago! We've been through a lot since then, you and I.

As you recall we added "timeRemaining -= 0.02;" to mainDraw. Let's update this, putting everything in mainDraw - except clearRect - into an if statement that checks whether timeRemaining is greater than 0. If it is, we'll run our game functions as usual. If not, we'll run another function, basically a "Game Over" screen:

if (timeRemaining > 0){
playerMove();
playerDraw();
drawCoins();
drawHUD();
checkCollision();
timeRemaining -= 0.02;
} else {
endStats();
}
(As you see I tidied this up a bit by moving the code that draws the player into a separate function called playerDraw.)

Once we've created endStats, we're done.

This is the end

c.font = '80pt Calibri';
c.fillStyle = 'black';
c.fillText("Your Score:", 70, 240);
c.fillStyle = 'yellow';
c.fillText(Player1.points, 580, 240);

c.font = '30pt Calibri';
c.fillStyle = 'black';
c.fillText("Press enter to play again!", 180, 400);

if ( keys[13] ) {
Player1.points = 0;
theCoins.splice(0);
createNewCoins();
timeRemaining = 30;
}
The first bit of endStats is pretty basic, it just writes the player's score in big letters. If you wanted you could have custom messages for different scores.

But what's this last bit? The key with code 13 is the enter key - I didn't want to use "Press any key" as the player might accidentally restart the game without seeing the end screen.

If the player presses enter, the points are set back to zero, theCoins is emptied with a splice, new coins are created, and the timer is set back to 30 seconds. Because timeRemaining is now greater than 0, endStats isn't called and the normal game functions run as usual.

Alright, this has been a long lesson. Sorry about that, I thought about splitting it up, but couldn't decide on a decent half-way point to stop at.

As always, here's the end result. My best score so far is 113 - beat that if you can.



If you want to test yourself, try adding additional coins that do certain things when they're collected. For example, a blue coin could give the player 10 more seconds, a red one could increase his speed temporarily, or make him bigger. You could add a gamble coin, which either adds or deducts 20 points from the player. These coins could appear at random times, or whenever the player does something special such as collect three coins in 5 seconds. They're your power ups - it's up to you what to do with them.

Go to Lesson 8 - Bounding the game area