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

Lesson 14) Sound

If a canvas game falls in the woods...

HTML 5 and canvas might be great at dealing with graphics, but they aren't so great with sound. The problem is that the sound is held on file on the server, as probably know. So you'd think you can just call some kind of play() function everytime you want to play the sound, right? Well that's true, but if you then play the sound again, before the first play has finished, the audio just starts again from the beginning - it doesn't overlap.

In order to have the sounds overlapping, you have to tell the browser to download the file again. Now obviously, in a game where we're making a sound effect with every shot fired and every bad guy killed, that's a lot of HTTP requests which could take up a lot of bandwidth and slow the game down unecessarily.

However there's a little workaround we can use. What we do is figure out the maximum number of times that a sound can play at once, and tell the browser to download the sound that many times. So take shooting. So say we have one sound file for our player's weapon sound, called "laser.mp3", and this sound lasts around half a second. If we figure that someone could at best click a mouse 6 times in half a second, we tell the browser to load that file 6 times, and we label these versions laser1, laser2, laser3 etc.

When we detect a mouse click, we play laser1. The next time we detect a mouse click, we play laser2. After the sixth click, we can assume that the laser1 sound has finished, and we can reuse that one. This way, we don't have to load the file every single time. It's kind of like recycling, so feel good - you're doing your bit.

So here's how it's done.

Declare the audio files

I'm sure you were expecting this - first we declare the audio files. All we're doing here is loading the file multiple times and giving each load a unique name:
var shotSwitcher = 1;
var laser1 = new Audio("laser.mp3");
var laser2 = new Audio("laser.mp3");
var laser3 = new Audio("laser.mp3");
var laser4 = new Audio("laser.mp3");
var laser5 = new Audio("laser.mp3");
var laser6 = new Audio("laser.mp3");

Play a different file each time

This is a pretty simple addition to the createBullet function.

You could do this with a switch command if you prefer. We have a variable called shotSwitcher, which we gave a value of 1 earlier. If its value is 1, we use "laser1.play();" to play the audio, if shotSwitcher is 2, we use "laser2.play()", and so on. The "laser1.currentTime" line allows you to play the audio from any point you like - I thought this one sounds better from the 0.1 point so I started it from there. I should probably just trim thre first 0.1 second off of the audio, but I wanted to show you this command as it's pretty useful.

At the end of all the if statements we just add 1 to shotSwitcher so that the next time we create a bullet, it will play the next audio file in the sequence. If we get to 7, we just set it back to 1.
if (shotSwitcher === 1) {
laser1.currentTime = 0.1;

if (shotSwitcher === 2) {
laser2.currentTime = 0.1;

if (shotSwitcher === 3) {
laser3.currentTime = 0.1;

if (shotSwitcher === 4) {
laser4.currentTime = 0.1;

if (shotSwitcher === 5) {
laser5.currentTime = 0.1;

if (shotSwitcher === 6) {
laser6.currentTime = 0.1;


if (shotSwitcher === 7) {
shotSwitcher = 1;
That's actually all there is to it. I did the same thing with sound effects for when the player successfully hits a bad guy, and when a coin is collected. The process is the same so I won't repeat it all here, but you can look at my souce code (Ctrl + u) and duplicate what I've done.

The sounds I used

As with the images, I got the sounds from public domain libraries. I also added a little credits page to endStats, to give these artists their due. All I did was create a variable called "endStatsDisplay" with a value of "score". Then in endStats, I changed this to "credits" if the user presses the C key, and back to score if they press S.

Then I used if statements to display the usual end message and score if endStatsDisplay is set to "score," and the credits page if it's set to "credits". I wanted to hard-code this into the game so that the credit is always there even if the game ends up on another site.

if (keys[67]) {
endStatsDisplay = "credits";

if (keys[83]) {
endStatsDisplay = "score";

if (endStatsDisplay === "credits") {
c.font = '20pt Calibri';
c.fillStyle = 'cyan';
c.fillText("This was a Frozen Lizard Production", 200, 30);
c.fillText("http://frozenlizardproductions.com", 200, 70);

c.font = '16pt Calibri';
c.fillStyle = 'white';
c.fillText("Graphics Thanks:", 0, 0);

c.font = '12pt Calibri';
c.fillText("Vortex background by darkrose:", 0, 30);
c.fillText("http://opengameart.org/users/darkrose", 0, 50);

c.fillText("Player and bad guys by C-TOY:", 0, 90);
c.fillText("http://c-toy.blogspot.co.uk", 0, 110);

c.fillText("Orbs by AMON:", 0, 150);
c.fillText("http://opengameart.org/users/amon", 0, 170);

c.font = '16pt Calibri';
c.fillStyle = 'white';
c.fillText("Sound Thanks:", 0, 0);

c.font = '12pt Calibri';
c.fillText("Laser and orb collection sounds by Kenney Vleugels", 0, 30);
c.fillText("http://www.kenney.nl", 0, 50);

c.fillText("Bad guy explosion by dklon:", 0, 90);
c.fillText("http://opengameart.org/users/dklon", 0, 110);


c.font = '30pt Calibri';
c.fillStyle = 'white';
c.fillText("Press enter to play again!", 190, 455);
c.fillText("Press S for score", 250, 520);

c.fillStyle = 'cyan';
c.fillText("Frozen Lizard Productions", 190, 590);


That's it!

OK my friends, this is the end of the tutorial. And now a challenge: you have enough knowledge here to expand this game in many different ways. You can add different bad guys, weapons, levels, bosses and power ups. See what you can do with the tools you've learned here - it will improve your skills massively to go "into the unknown" rather than following along with a step-by-step guide.

But please let me know what you come up with!