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

Lesson 12) Bullets and shooting

Putting the shoot into shoot 'em up

I know, I know, I reused that line from the end of the last tutorial. Below is how we'll allow the player to shoot, in a nutshell (and by that I mean I'll explain it in brief, not that we're going to trap the player in a nutshell and only then allow him to shoot):
  1. Determin the x and y coordinates of the mouse
  2. Add a listener to detect left-button clicks
  3. Create a bullet if we do detect one
  4. Draw the bullet and update it's position along it's trajectory every game loop
  5. Check for collisions with bad guys, if there is one splice both the bad guy and the bullet

1) Getting the mouse coordinates

Add the following event listener and function to your code (remember to declare mouseX and mouseY):
function mouseMove(e) {
if(e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
else if (e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}
console.log("mouseX = " + mouseX + ", mouseY = " + mouseY); }

canvas.addEventListener('mousemove', mouseMove, true);
The offset and layer bits are built into JavaScript, and allow us to get the coordinates of the mouse relative to the canvas, as opposed to the document as a whole (note that you must have the position:relative; CSS property on the canvas for this to work, which we do).

This gives us the x and y coordinates of the mouse, put into the appropriately-named mouseX and mouseY. If you load up the console, you'll see that this is true.

2) Detecting left-button clicks

A mouse click event listener looks very similar to the keypress ones we created earlier, but we're going to call a new function, createBullet:
canvas.addEventListener("click", function() {
createBullet(mouseX, mouseY, Player1.x, Player1.y);
});
We're passing the mouse coordinates and the player's coordinates to the function, so when you create it make sure you add parameters that allow the function to receive them:

function createBullet(targetX, targetY, shooterX, shooterY) {

}
You may be wondering why I renamed mouse to target and player to shooter. Well it's because we might want to use this function for other game characters, for example to make a bad guy shoot, or to make a turret shoot. To avoid confusion I've made the parameters general - target and shooter.

3) Creating the bullet

So we need to fill in createBullet with the code that will, yes that's right, create a bullet. But first let's define some variables (I know you love it when I say that):
var deltaX = 0;
var deltaY = 0;
var rotation = 0;
var xtarget = 0;
var ytarget = 0;
var theBullets = [];
And we'll fill createBullet with the following:
if (!gameOver) {
deltaX = targetX - shooterX;
deltaY = targetY - shooterY;
rotation = Math.atan2(deltaY, deltaX);
xtarget = Math.cos(rotation);
ytarget = Math.sin(rotation);

theBullets.push({
active:true,
x: shooterX,
y: shooterY,
speed: 10,
xtarget: xtarget,
ytarget: ytarget,
w: 3,
h: 3,
color: 'black',
angle: rotation
});
}
I won't explain the maths at the top, but essentially what this does is figure out what number you need to add to the starting x and y positions of the bullet in order to move it towards where the mouse was when the click happened. These figures are saved in the variables xtarget and ytarget. The rotation variable stores the angle you need to turn the bullet to make it face the target.

Then, we push an object to our theBullets array, which contains these values, a speed, size and colour.

4) Draw the bullets

As noted previously, it's good practice to separate the movement of entities from the rendering. There are a few reasons for this, and although it might seem like a waste of resources to loop through everything twice (once to update, once to render), it can speed things up in some situations - for example, you could have the game calculate movement every loop, but render only every two loops. Since rendering is far more resource-heavy than just doing a few sums to calculate movement, this could speed things up a lot. Or you could set your game to only do this when there are so many entities on screen, maybe.

It really won't make a difference either way in a game like this, where at most we'll have maybe 50 entities on screen. Some physics programs can have thousands of entities and still run smoothly. But it's good to be aware that this is generally how it's done.

So then, let's call two new functions for this purpose in mainDraw:
bulletsMove();
bulletsDraw();
bulletsMove
For bulletsMove, we've actually done all the hard work. Each object in our theBullets array contains an xtarget and ytarget property, and a velocity.

function bulletsMove() {
theBullets.forEach( function(i, j) {
i.x += i.xtarget * i.speed;
i.y += i.ytarget * i.speed;
});
}
Why do we multiply by the speed instead of adding it on? Because if we added, the bullets would always go in one direction (down and to the right). Our xtarget and ytarget variables can be negative, which will move them upwards and to the left. To keep them negative, we need to multiply them by the speed.
bulletsDraw
I expect you know what's coming, since this is the same way we've drawn pretty much everything else in this game!:
function bulletsDraw() {
theBullets.forEach( function(i, j) {
c.beginPath();
c.save();
c.fillStyle = 'black';
c.rect(i.x, i.y, i.w, i.h);
c.fill();
});
}

Check for collisions

Killing bad guys involves checking for collisions and splicing things from arrays as needed. We already know how to do this, so I won't labour the point. For the sake of simplicity and compartmentalisation I'll call a new function for this from mainDraw, and we'll loop through everything again. If this impacted performance we could try something fancier, like checking during badGuysMove function, since we're already looping through the bad guys and checking collisions anyway. Here we go:
function checkBulletHits() {
if (theBullets.length > 0 && theBadGuys.length > 0) {
for (j = theBullets.length - 1; j >= 0; j--) {
for (k = theBadGuys.length - 1; k >= 0; k--) {
if (collides(theBadGuys[k], theBullets[j])) {
console.log("collides");
theBadGuys.splice(k, 1);
theBullets.splice(j, 1);
Player1.points += 1;
}
}
}
}
}
So, step-by-step:
  1. We check if there's at least one item in both arrays - if not, there's no point checking for collisions.
  2. We loop through theBullets. For the first bullet (which is actually the last item in the array)....
  3. We loop through theBadGuys, and for each baddie, we call the collides function and see if it's touching the bullet in question. If it is...
  4. We splice the bullet and the baddie, and give the player 1 point.
  5. Then we go back to step 2, and check the next bullet for collisions (which will actually be the second-to-last item in theBullets).
  6. We do this until we've checked all the bullets.
And that my friends, gives us a working, if very simple top-down shoot 'em up:


If you want some extra practice, there's a lot of tweaking you could do to this which wouldn't require a great deal of extra coding: Now personally, I quite like how retro the graphics are in this game. But then again I was brought up on games that looked like this:

A picture of an atari 2600 game screenshot
Nowadays you kids with your fandangled 3D graphics and controllers with more than one button probably expect a little more from your video game experience.

Let's try adding some graphics and music to the game next.

Go to Lesson 13 - Graphics