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

Lesson 14) Adding sound to a canvas game

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 every time 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 unnecessarily.

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 save all of these audio files in an array (called, say, “laserFiles[]”). We also create a switcher variable, laserSwitcher, which equals 0.

Then, when we detect a mouse click, we play laserFiles[laserSwitcher], which will be the laser.mp3 file saved at position 0 in the array. Just after we play this file, we add 1 to laserSwitcher. So the next time we detect a mouse click, or program will play laserFiles[1]. Every time we increment laserSwitcher, we check if it’s greater than the number of files in laserFiles (i.e., laserFiles.length – 1). If it is, we’ll set it back to 0 and the cycle continues.

Here’s how to do it in code.

Declare the audio files and switcher variables

I’m sure you were expecting this – first we declare the audio files and the switcher variable. All we’re doing here is loading the file multiple times into a different position in an array:

var shotSwitcher = 1;
var laserSwitcher = 0;
var laserFiles = [
  new Audio("laser.mp3"),
  new Audio("laser.mp3"),
  new Audio("laser.mp3"),
  new Audio("laser.mp3"),
  new Audio("laser.mp3"),
  new Audio("laser.mp3"),	
];

Play a different file each time

This is a pretty simple addition to the createBullet function.

laserFiles[laserSwitcher].currentTime = 0.1;
laserFiles[laserSwitcher].play();
laserSwitcher++;
if (laserSwitcher > laserFiles.length - 1) {
  laserSwitcher = 0;
}

That’s actually all there is to it.

The “laserFiles[laserSwitcher].currentTime” line is not necessary, it just allows you to play the audio from any point in the file you like – I actually think this laser noise sounds better from the 0.1 point. It would normally just edit the audio file, which would speed up the download of the files, but I wanted to show you this method.

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 — you can look at the source code if you want to see it.

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("Thanks for playing!", 300, 30);
  c.fillText("https://warrendavies.net", 250, 70);

  c.save();
  c.translate(20,130);
  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.restore();

  c.save();
  c.translate(450,130);
  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.restore();

  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';
}

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. Another good advancement would be to add a progress bar, so that the game doesn’t start unless all the sounds are loaded. See what you can do with the tools you’ve learned here.

Please let me know what you come up with!

Here’s the final game!


Leave a Reply

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