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

Lesson 13) Graphics

Appearance matters, these days

Now, I'm not the most aesthetically-minded person in the world (you should see my haircut), but even I know that moving a red square around a green background isn't going to impress anyone. Let's try to improve the game with some graphics.

You can get a lot of stuff for free (often you just have to credit the creator) on public domain art websites. Personally I like Open Game Art. Note that you can't just use any old image you find and use it - you have to abide by the rules set out by the creator.

License to Copy

As soon as anyone creates create something, including you, it is copyrighted to them. You don't need to apply to copyright, you don't need to put a little c in a circle, it's automatic. You can't use a copyrighted piece of work without the copyright holder's permission (yes, even if you write "I do not own this work no copyright infringement intended" on it, you still need permission).

But if you want to give away your work to other people, all you have to do is meet them all one-by-one and anoint them with the authority to use your work (maybe get some wine and cheese and make a night of it). No just kidding, of course that would take too long. Content creators just have to slap a licence on their work. Sometimes there are conditions, usually you have to credit them as the creator of the work, sometimes you have to link back to their site and so on. Before you use any images, you need to check that you have the appropriate licence to use it, and that you've met the necessary conditions of use.

Declare your images

Just like variables, you need to declare your images up-front in your code before you use them. It's customary to do this in a function:

function loadImages() {
playerImage = new Image();
playerImage.src = 'player.png';

badGuyImage = new Image();
badGuyImage.src = 'badguy.png';

balls = new Image();
balls.src = 'balls.png';

}
loadImages();
As you can see I just ran the function straight away. This is because our game is very simple, and we only have one level. Otherwise I might only load the images for the level that the player was on, to speed things up a bit.

Does this code look familiar? It should; Image() is a contructor, just like the Player() constructor that we made, only this one is built into JavaScript. Then we give it an src property with the link to the image. So here I've loaded a "playerImage", a "badGuyImage", and "balls", which will replace the coins.

playerImage

First we add a propery to the Player() constructor linking it to the image:
this.image = playerImage;
By the way here's the image we're going to use:

Player graphic
That's a pretty tough looking ship. The curved edges make it more like something a bad guy would fly (good guys are usually triangles), so I like the idea of using this as the player. Thanks to C-TOY for this image (and for the bad guys too).

Next we're going to make some pretty extensive revisions to the playerDraw function.
function playerDraw() {

c.beginPath();

c.strokeStyle="blue";
c.rect(Player1.x, Player1.y, Player1.w, Player1.h);
c.lineWidth=1;
c.stroke();

c.save();
deltaX = mouseX - Player1.x;
deltaY = mouseY - Player1.y;
newAngle = Math.atan(deltaY / deltaX);

c.translate(Player1.x + (Player1.w / 2), Player1.y + (Player1.h / 2) );
if (deltaX < 0) {
c.rotate(newAngle);
c.scale(-1, 1);
} else {
c.rotate(newAngle);
c.scale(1, -1);
}
c.translate(-Player1.x - (Player1.w / 2),-Player1.y - (Player1.h / 2));

c.drawImage(Player1.image, Player1.x - 7, Player1.y - 5,Player1.w * 1.3, Player1.h * 1.3);

c.restore();
}
Let's take this step-by-step:
  1. We begin a new path.
  2. We draw the rectangle as previously. I have only used stroke() here, not fill. All I wanted was an outline of the player's collision boundary, so I can line up the player's image with it. Otherwise, you might have collisions detected when the player's image isn't actually touching a bad guy.
  3. We use c.save(). The save() and restore() canvas methods are similar in principle to the concept of paths, but for different aspects of the canvas. We're going to rotate the player's graphic so that it faces the mouse. To do this, we actually rotate the whole canvas. Imagine that you have a paper canvas on your desk, and your player image is a stamp that you push down onto the paper. What we're doing here is, holding the stamp completely steady, rotating the paper a bit, stamping the player down, then turning the paper back.
  4. We get the angle between the player and the mouse, just like we did when we created the bullets.
  5. We translate to the center of the player (his center being his x plus half his width and his y plus half his height). Translate just means "do all subsequent drawing relative to this point", as if the point you specify is 0, 0 on the canvas. This is essential to make sure the player just spins on the spot instead of doing wider circles around the point you translated to (try translating to somewhere else on the canvas and you'll see what I mean - it's hard to explain in text).
  6. We check whether deltaX is less than 0, which can only be true if the mouse is further left on the canvas than the player. If so, we rotate and scale. Scale is used to change the scale of something, the two parameters being the width and height that you want to scale to, however this is different to a resize - to achieve a scale(), JavaScript changes the coordinate system, making the distance between each coordinate larger or smaller. If you scale to 1, that's like saying 100%, in other words no change. If you scale to -1, you're scaling to -100%, effectively flipping the image on that axis. Since our player graphic faces to the right, we scale its width to -1 whenever the mouse is to the left of the player. Otherwise the rear of the ship would be facing the mouse.
  7. The else statement runs when the mouse is to the right of the player. In this case we just rotate, no need to scale.
  8. We draw (or stamp down) the image onto the rotated canvas using c.drawImage(). Now, we've already translated to the center of the player's collision boundary (i.e., the middle of what used to be the blue box). So that position is now at coordinates 0, 0. So to get the image in the right place, we need to draw at:
    • x = 0 - (Player1.w / 2)
    • y = 0 - (Player1.h / 2)
    You'll see I also deducted a further 7 from the x coordinates and 5 from the 7. The image is bigger than the player's collision boundary, and this puts the collision area fully "within" the graphic. So you won't get a situation where a collision is detected but the images on screen aren't touching. Generally speaking you want to give the benefit of the doubt to the player, especially with images like ours that are not perfect rectangles. For example, if a bad guy touches one of Sonic the Hedgehog's spikes, there is no collision - they have to get a little bit closer in. This avoids frustration for the player. Likewise, when collecting power ups, you might want to use a slightly larger collision area for the power up than it's image on screen, again, to give them the benefit of the doubt in case a little bit of the ship's wing clips one of the coins, the player will still get it. You might not want to use this in all games and situations, but it's a concept worth considering.
  9. Finally, we restore. This changes all of the things we've changed in the canvas - scale and rotation in this case - back to the state they were in during the save(). However, the things we drew between the save and restore are not affected by this (their states were not saved because they didn't exist at that point), so the effect is a rotated ship.

The Bad Guy Images - don't worry this is simpler!

Here's the bad guy image, from C-TOY's same set of images:
Bad Guy graphic
I think you'll agree this is a pretty cool bad guy. Something about these spidery legs which makes them insect-like, you definitely want to keep it away from you!

We defined the image earlier, so let's add it to the bad guy objects in a similar fashion as with the player:
image: badGuyImage
And the updates to badGuysDraw are much simpler:
function badGuysDraw() {
theBadGuys.forEach( function(i, j) {
c.beginPath();
c.strokeStyle="red";
// c.rect(i.x, i.y, i.w, i.h);
c.drawImage(i.image, i.x - 12 , i.y - 12, 50, 50);
c.lineWidth=1;
c.stroke();
});
}
No rotation is needed here as the baddie doesn't have a front. Note that I've kept in the rect() command, but I've commented it out and also removed the fill() and fillStyle(). Again this is just for debugging, if you find some problems with collisions you can uncomment this line, and you'll get a visual representation of each baddies collision area again, making troubleshooting easier.

If you comment this out in your own code, you'll see that the collision boundary is actually just the middle red bit, not the spindly arms. This is due to drawing the image at it's x and y coordinates minus 12. Again, I'm just giving the player the benefit of the doubt here. Given the shape of the bad guy, it would be pretty easy to use two rectangles to make collision detection more sophisticated:
Bad Guy graphic
But I'm happy to keep it simple, you feel free to try that if you want.

The coins

I also got some images for the coins from Open Game Art, this set of orbs by Amon. I chose this specifically to show you something else that you can do with images. You'll notice that Amon has put all of the orbs into one file. Well there's a way to load only part of the image at a time, and that's what we'll do. Observe the changes to the gold coin:
theCoins.push({x: Math.floor(Math.random() * 800),y: Math.floor(Math.random() * 600),w: 20,h: 20, points: 10, color: 'yellow', sourceX: 0, sourceY: 130, sourceW: 130, sourceH: 130});
I've added sourceX, sourceY, sourceW and sourceH. These are the x and y coordinates on the image we want to take from, and the width and height of the snippet we want to take, relative to that point. Each orb is 130 by 130 pixels, and the gold orb is in the lower left, so I'm starting with sourceX of 0, sourceY of 130, and a sourceW and sourceH of 130. I did the same for the other coins too, adjusting these values to fit.

Then we draw the coins (or orbs or planets or whatever they are now). Note that we add our source coordinates BEFORE the destination coordinates. Note also that in this case I have opted not to make the image on screen larger than the collision area - once again, I give the player the benefit of the doubt and avoid the frustration of having images potentially make contact on screen but without a collision being detected.
function drawCoins() {
theCoins.forEach( function(i, j) {
c.beginPath();
c.strokeStyle=i.color;
// c.rect(i.x, i.y, i.w, i.h);
c.drawImage(balls, i.sourceX, i.sourceY, i.sourceW, i.sourceH, i.x, i.y, i.w, i.h);
c.lineWidth=1;
c.stroke();
});
}

Hiding in the background

As a final touch, let's get rid of this green screen. I just found this excellent Vortex Background by darkrose, and set it as the canvas's background in the CSS code:
background: url('background.png');
Simple as that.

I also moved the "Game Over" message up a bit and changed the bullet color to red.

Do you want to give it a try?:

If you can score 200 I'll be impressed.

One more lesson to go and then we're done - let's add some sound!

Go to Lesson 14 - Sound