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

Lesson 12) Bullets and shooting

Putting the shoot into this shoot ’em up

Below is how we’ll allow the player to shoot, in a nutshell (and by that I mean that 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. Determine 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, save in the appropriately-named mouseX and mouseY variables. 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 then let’s 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
  });
}

The maths at the top, essentially figures out what number you need to add to the starting x and y positions of the bullet in order to move it towards the target. 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 those values, plus 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 more resource-heavy than updating, this could speed things up a lot. Or you could set your game to only do this when there are a lot of entities on screen.

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 xtarget and ytarget can be negative. Since speed is always going to be higher than these variables, we would always be adding positive numbers to the bullet’s coordinates. Bullets would always move down and to the right. We get around this by multiplying.

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 will involve checking for collisions and splicing things from our 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:

  • Experiment with different bullet speeds
  • Give the player bonus points for accuracy / penalties for missing too many shots
  • Give the player 10 more seconds for every 5 kills
  • Increase the number of baddies that spawn
  • Have some baddies shoot at the player
  • Create a loop where the player can walk off the edge of one side of the screen and appear on the opposite side
  • Have a “boss” bad guy that appears very rarely but is harder to fight

Another thing we could do is improve the graphics. 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:

Atari Tanks game

Nowadays you kids with your fan dangled 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 to the game next.

Leave a Reply

Your email address will not be published. Required fields are marked *