You are previewing HTML5 Canvas.

HTML5 Canvas

Cover of HTML5 Canvas by Steve Fulton... Published by O'Reilly Media, Inc.
  1. HTML5 Canvas
    1. SPECIAL OFFER: Upgrade this ebook with O’Reilly
    2. A Note Regarding Supplemental Files
    3. Preface
      1. Running the Examples in the Book
      2. What You Need to Know
      3. How This Book Is Organized
      4. Conventions Used in This Book
      5. Using Code Examples
      6. We’d Like to Hear from You
      7. Safari® Books Online
      8. Acknowledgments
    4. 1. Introduction to HTML5 Canvas
      1. The Basic HTML Page
      2. Basic HTML We Will Use in This Book
      3. The Document Object Model (DOM) and Canvas
      4. JavaScript and Canvas
      5. HTML5 Canvas “Hello World!”
      6. Debugging with Console.log
      7. The 2D Context and the Current State
      8. The HTML5 Canvas Object
      9. Another Example: Guess The Letter
      10. What’s Next
    5. 2. Drawing on the Canvas
      1. The Basic File Setup for This Chapter
      2. The Basic Rectangle Shape
      3. The Canvas State
      4. Using Paths to Create Lines
      5. Advanced Path Methods
      6. Compositing on the Canvas
      7. Simple Canvas Transformations
      8. Filling Objects with Colors and Gradients
      9. Filling Shapes with Patterns
      10. Creating Shadows on Canvas Shapes
      11. What’s Next
    6. 3. The HTML5 Canvas Text API
      1. Displaying Basic Text
      2. Setting the Text Font
      3. Text and the Canvas Context
      4. Text with Gradients and Patterns
      5. Width, Height, Scale, and toDataURL() Revisited
      6. Final Version of Text Arranger
      7. What’s Next
    7. 4. Images on the Canvas
      1. The Basic File Setup for This Chapter
      2. Image Basics
      3. Simple Cell-Based Sprite Animation
      4. Advanced Cell-Based Animation
      5. Applying Rotation Transformations to an Image
      6. Creating a Grid of Tiles
      7. Zooming and Panning an Image
      8. Pixel Manipulation
      9. Copying from One Canvas to Another
      10. What’s Next
    8. 5. Math, Physics, and Animation
      1. Moving in a Straight Line
      2. Bouncing Off Walls
      3. Curve and Circular Movement
      4. Simple Gravity, Elasticity, and Friction
      5. Easing
      6. What’s Next?
    9. 6. Mixing HTML5 Video and Canvas
      1. HTML5 Video Support
      2. Converting Video Formats
      3. Basic HTML5 Video Implementation
      4. Preloading Video in JavaScript
      5. Video and the Canvas
      6. Video on the Canvas Examples
      7. Animation Revisited: Moving Videos
      8. What’s Next?
    10. 7. Working with Audio
      1. The Basic <audio> Tag
      2. Audio Formats
      3. Audio Tag Properties, Functions, and Events
      4. Playing a Sound with No Audio Tag
      5. Creating a Canvas Audio Player
      6. Case Study in Audio: Space Raiders Game
      7. What’s Next
    11. 8. Canvas Game Essentials
      1. Why Games in HTML5?
      2. Our Basic Game HTML5 File
      3. Our Game’s Design
      4. Game Graphics: Drawing with Paths
      5. Animating on the Canvas
      6. Applying Transformations to Game Graphics
      7. Game Graphic Transformations
      8. Game Object Physics and Animation
      9. A Basic Game Framework
      10. Putting It All Together
      11. The player Object
      12. Geo Blaster Game Algorithms
      13. The Geo Blaster Basic Full Source
      14. Rock Object Prototype
      15. What’s Next
    12. 9. Combining Bitmaps and Sound
      1. Geo Blaster Extended
      2. Creating a Dynamic Tile Sheet at Runtime
      3. A Simple Tile-Based Game
      4. What’s Next
    13. 10. Mobilizing Games with PhoneGap
      1. Going Mobile!
      2. Creating the iOS Application with PhoneGap
      3. Beyond the Canvas
      4. What’s Next
    14. 11. Further Explorations
      1. 3D with WebGL
      2. Multiplayer Applications with ElectroServer 5
      3. Conclusion
    15. Index
    16. About the Authors
    17. Colophon
    18. SPECIAL OFFER: Upgrade this ebook with O’Reilly
O'Reilly logo

Case Study in Audio: Space Raiders Game

If we were writing a book about standard HTML5, we might be able to stop here and continue on with another topic. However, there is a lot more to playing audio in an application than simply getting a song to play and tracking its progress. In the last part of this chapter, we will look at a case study: Space Raiders. We will iterate through several ideas and attempts to get audio working in an efficient way in conjunction with action on HTML5 Canvas.

Why Sounds in Apps Are Different: Event Sounds

Why make a game as an example for playing sounds in HTML5? Well, a game is a perfect example because it is difficult to predict how many sounds might be playing at any one time.

If you can imagine, games are some of the most demanding applications when it comes to sound. In most games, sounds are played based on user interactions, and those interactions are usually both asynchronous and unpredictable. Because of those factors, we need to create a strategy for playing sounds that is flexible and resource-efficient.

To demonstrate how tricky sounds can be when using JavaScript and HTML5 with a canvas game, we will iterate this game several times until we have a working model.

Here are some assumptions we will make regarding sound in Space Raiders based on what we know about the HTML5 audio object.

  1. After loading a sound, you can make another object with the same source and “load” it without having to wait for it to load. (Flash sort of works this way.)

  2. Playing sounds locally is the same as playing them on a remotely hosted web page.

It turns out that both of these assumptions are wrong. As we continue through this case study, we will show you why, as well as how to accommodate them.

Since this is not a chapter about making games, Space Raiders is only going to be a façade. In Hollywood, a façade is a structure built for filming, containing only the parts the camera will see. For example, a building façade might have only the front wall and windows—with nothing behind them. Space Raiders is like this because we are only going to create the parts necessary to include the dynamic sounds we will be using. It will be most of a game, leading you into Chapters 8 and 9, which take a deep dive into making complete games with HTML5 Canvas.

Iterations

In this case study, we will create four iterations of Space Raiders. Each one will attempt to solve a dynamic audio problem in a different way. First, we will show you the basics of the Space Raiders game structure, and then we will discuss how to solve the audio problem.

Space Raiders Game Structure

Space Raiders is an iconic action game where a swarm of alien invaders attack from the top of the screen, and the player’s job is to defend the world. The raiders move in horizontal lines near the top of the screen. When each raider reaches the side of the playfield, it moves down the screen and then switches direction.

The player controls a spaceship by moving the mouse, and fires missiles using the left mouse button. We need to play a “shoot” sound every time the player fires a missile. When the missiles hit the enemy space raiders, we need to remove them from the screen, and then play an “explosion” sound. We are not limiting the number of shots the player can fire, which means that there could be any number of shoot and explode sounds playing simultaneously. Our goal is to manage all these dynamic sounds.

State machine

This game runs using a very simple state machine. A state machine is a construct that allows an application to exist in only one state at a time, which means it is only doing one thing. This kind of construct is great for single-player games because it removes the need to hold a bunch of Booleans describing what is going on at any one moment.

Space Raiders has four states plus a variable named appState that holds the value of the current state. Those states include:

STATE_INIT

A state to set up the loading of assets:

const STATE_INIT = 10;
STATE_LOADING

A wait state that has the application sleep until all assets have been loaded:

const STATE_LOADING = 20;
STATE_RESET

A state to set up the initial game values:

const STATE_RESET = 30;
STATE_PLAYING

A state that handles all game-play logic:

const STATE_PLAYING = 40;

Note

A final game of this type might have a few more states, such as STATE_END_GAME and STATE_NEXT_LEVEL, but our case study does not require them.

The heart of our state machine is the run() function, which is called on an interval every 33 milliseconds. The appState variable determines what function to call at any given time using a switch() statement. appState is updated to a different state any time the program is ready to move on and do something else. The process of calling a function such as run() on an interval and switching states is commonly known as a game loop:

function run() {
      switch(appState) {       case STATE_INIT:
         initApp();
         break;
      case STATE_LOADING:
         //wait for call backs
         break;
      case STATE_RESET:
         resetApp();
         break;
      case STATE_PLAYING:
         drawScreen();
         break;

   }
}

Initializing the game: no global variables

Now that we know a bit about the state machine construct we will use for this game, it’s time to set up the preload for our assets. As we mentioned previously, this game has two sounds, shoot and explode, but it also has three images: a player, an alien, and a missile.

Remember how we kept saying we’d do away with global variables in these applications? Well, here’s where it happens. With the state machine, we now have a mechanism to allow our application to wait for loading assets instead of leveraging only the DOM’s window load event.

In the canvasApp() function, we set up the following variables to use in the game.

The appState variable holds the current state constant:

var appState = STATE_INIT;

We use the loadCount and itemsToLoad variables in exactly the same way we used them in the audio player application—except here we will be loading more items:

var loadCount= 0;
var itemsToLoad = 0;

The variables alienImage, missileImage, and playerImage will hold the loaded images we use in the game:

var alienImage = new Image();
var missileImage = new Image();
var playerImage = new Image();

explodeSound and shootSound will hold the references to the HTMLAudioElement objects we will load:

var explodeSound ;
var shootSound;

The audioType variable will hold the extension of the valid audio file type for the browser displaying the application:

var audioType;

The mouseX and mouseY variables will hold the current x and y location of the mouse:

var mouseX;
var mouseY;

The player variable will hold a dynamic object with the x and y location of the player ship (controlled with the mouse):

var player = {x:250,y:475};

Both the aliens and missiles arrays will hold lists of dynamic objects for displaying aliens and missiles on the canvas:

var aliens = new Array();
var missiles = new Array();

The next five constants set the number of aliens (ALIEN_ROWS, ALIEN_COLS), their starting location (ALIEN_START_X, ALIEN_START_Y), and their spacing on screen (ALIEN_SPACING):

const ALIEN_START_X = 25;
const ALIEN_START_Y = 25;
const ALIEN_ROWS = 5;
const ALIEN_COLS = 8;
const ALIEN_SPACING = 40;

Also in the canvasApp() function, we need to set up event handlers for mouseup and mousemove. To create the game loop, we need to set up our interval to call the run() function:

theCanvas.addEventListener("mouseup",eventMouseUp, false);
theCanvas.addEventListener("mousemove",eventMouseMove, false);

setInterval(run, 33);

At this point, run() will be called and our game loop will start by calling the function associated with the value of appState.

Preloading all assets without global variables

We just showed that the appState variable was initialized to STATE_INIT, which means that when the run() function is called for the first time, the initApp() function will be called. The good news (at least for this discussion) is that initApp() does very little that we have not already seen—it just does it in the context of the Canvas application. The result? Now we don’t need any global variables.

In the code below, notice that we are using the same strategy. We have a single event handler for all loaded assets (itemLoaded()),we set itemsToLoad to 5 (three graphics and two sounds), and we set the appState to STATE_LOADING at the end of the function. The rest of the code is all simple review:

function initApp() {
   loadCount=0;
   itemsToLoad = 5;
   explodeSound = document.createElement("audio");
   document.body.appendChild(explodeSound);
   audioType = supportedAudioFormat(explodeSound);
   explodeSound.setAttribute("src", "explode1." + audioType);
   explodeSound.addEventListener("canplaythrough",itemLoaded,false);

   shootSound = document.createElement("audio");
   document.body.appendChild(shootSound);
   shootSound.setAttribute("src", "shoot1." + audioType);
   shootSound.addEventListener("canplaythrough",itemLoaded,false);

   alienImage = new Image();
   alienImage.onload = itemLoaded;
   alienImage.src = "alien.png";
   playerImage = new Image();
   playerImage.onload = itemLoaded;
   playerImage.src = "player.png";
   missileImage = new Image();
   missileImage.onload = itemLoaded;
   missileImage.src = "missile.png"; appState = STATE_LOADING;
   }

If you recall, STATE_LOADING does nothing in our run() function; it just waits for all events to occur. The action here is handled by the itemLoaded() event handler, which works exactly like the itemLoaded() function we wrote for the audio player, except that it has two additional functions:

  1. It must remove the event listeners from the two sound objects we created. This is because, in some browsers, calling the play() method of an HTMLAudioElement object—or changing the src attribute of an HTMLAudioElement object—initiates a load operation, which will then call the itemLoaded event handler a second time. This will cause unexpected results in your application. Furthermore, it is always a good idea to remove unneeded event handlers from your objects.

  2. We set the appState to STATE_RESET, which will initialize the game the next time the run() function is called on the interval.

Here is the code with the two additional functions:

function itemLoaded(event) {

   loadCount++;
   if (loadCount >= itemsToLoad) {

      shootSound.removeEventListener("canplaythrough",itemLoaded, false);
      explodeSound.removeEventListener("canplaythrough",itemLoaded,false);

      appState = STATE_RESET;

   }

  }

Resetting the game

In the run() function, the STATE_RESET state calls the resetApp() function, which in turn calls startLevel(). It also sets the volume of our two sounds to 50% (.5) before setting the appState to STATE_PLAYING:

function resetApp() {

   startLevel();
   shootSound.volume = .5;
   explodeSound.valume = .5;
   appState = STATE_PLAYING;

  }

The startLevel() function traverses through two nested for:next loops, creating the rows of aliens by column. Each time we create an alien, we push a dynamic object into the aliens array with the following properties:

speed

The number of pixels the aliens will move left or right on each call to drawScreen().

x

The starting x position of the alien on the screen. This value is set by the column (c) multiplied by ALIEN_SPACING, added to ALIEN_START_X.

y

The starting y position of the alien on the screen. This is set by the row (r) multiplied by ALIEN_SPACING, added to ALIEN_START_X.

width

The width of the alien image.

height

The height of the alien image.

Here is the code for the startLevel() function:

function startLevel() {

      for (var r = 0; r < ALIEN_ROWS; r++) {
         for( var c= 0; c < ALIEN_COLS; c++) {
            aliens.push({speed:2,x:ALIEN_START_X+c*ALIEN_SPACING, y:ALIEN_START_Y+r*
               ALIEN_SPACING,width:alienImage.width, height:alienImage.height});
         }
      }
   }

Mouse control

Before we talk about the game play itself, let’s quickly discuss mouse event handlers, which will collect all user input for the game. When the player moves the mouse, the eventMouseMove() handler is called. This function operates just like the same function we created for the audio player, except for the last two lines. Those two lines set the x and y properties of the player object we created back in the variable definition section of canvasApp(). We will use these two properties to position the playerImage on the canvas in the drawScreen() function:

function eventMouseMove(event) {
      if ( event.layerX ||  event.layerX == 0) { // Firefox
            mouseX = event.layerX ;
         mouseY = event.layerY;
        } else if (event.offsetX || event.offsetX == 0) { // Opera
          mouseX = event.offsetX;
         mouseY = event.offsetY;
        }
      player.x = mouseX;
      player.y = mouseY;

   }

The eventMouseUp() handler is called when the player presses and releases the left mouse button. When this event occurs, a missile will fire. The missile object is almost identical to the alien object, as it includes speed, x, y, width, and height properties. Since the player is firing the missile, we set the missile’s x and y positions to the center of the player’s ship on the x-axis (player.x+.5*playerImage.width), and to the y position of the player’s ship, minus the height of the missile (player.y - missileImage.height):

function eventMouseUp(event) {

   missiles.push({speed:5, x: player.x+.5*playerImage.width, 
      y:player.y-missileImage.height,width:missileImage.width, 
      height:missileImage.height});

Next is the first really critical line of code for the subject at hand: audio. For this first iteration of Space Raiders, we simply call the play() function of shootSound. This will play the shoot sound as often as the player presses the left mouse button (in theory):

   shootSound.play();
}

Bounding box collision detection

Before we get to the main part of the game logic, we should discuss bounding box collision detection. We need to detect collisions between the missiles the player fires and the aliens the player is firing upon. To do this, we will create a function that tests to see whether two objects are overlapping. For lack of a better name, we call this function hitTest().

The type of hit test we are going to perform is called a bounding box collision test. This means that we are going to ignore the intricate details of the bitmapped graphics and simply test to see whether an invisible “box” drawn around the bounds of each object overlaps with a similar box drawn around the other objects.

Recall that both the alien and missile dynamic objects were created with similar properties: x, y, width, height. This was so the hitTest() function could test them as generic objects, unspecific to the type of on-screen object that they represent. This means that we can add any other type of object to this game (boss alien, power-ups, enemy missiles, etc.), and if it is created with similar properties, we can use the same function to test collisions against it.

The function works by finding the top, left, bottom, and right values for each object, and then testing to see whether any of those values overlap. Bounding box collision detection will be discussed in detail in Chapter 8, but we just wanted to give you a preview of what it looks like for Space Raiders:

function hitTest(image1,image2)  {
      r1left = image1.x;
      r1top = image1.y;
      r1right = image1.x + image1.width;
      r1bottom = image1.y + image1.height;
      r2left = image2.x;
      r2top = image2.y;
      r2right = image2.x + image2.width;
      r2bottom = image2.y + image2.height;
      retval = false;

      if ( (r1left > r2right) || (r1right < r2left) || (r1bottom < r2top) || 
           (r1top > r2bottom) ) {
         retval = false;
      } else {
         retval = true;
      }


      return retval;
   }

Playing the game

Now the game is ready to play. STATE_PLAYING calls the drawScreen() function, which is the heart of Space Raiders. The first part of this function simply moves the missiles and aliens on the screen. Moving the missiles is quite easy. We loop through the array (backward), updating the y property of each with the speed property. If they move off the top of the screen, we remove them from the array. We move through the array backward so that we can splice() array elements out of the array and not affect loop length. If we did not do this, elements would be skipped after we splice() the array:

for (var i=missiles.length-1; i>= 0;i−−) {
   missiles[i].y −= missiles[i].speed;
   if (missiles[i].y < (0-missiles[i].height)) {
      missiles.splice(i,1);
   }

}

Drawing the aliens is similar to drawing missiles—with a few exceptions. Aliens move left and right, and when they reach the side of the canvas, they move down 20 pixels and then reverse direction. To achieve the reversal in direction, multiply the speed property by -1. If the aliens are moving to the right (speed = 2), this will make the speed = -2, which will subtract from the x position and move the aliens to the left. If the aliens hit the left side of the canvas, the speed property will again be multiplied by -1 (-2 * -1), which will equal 2. The alien will then move to the right because 2 will be added to the x value for the alien each time drawScreen() is called:

//Move Aliens
      for (var i=aliens.length−1; i>= 0;i−−) {
         aliens[i].x += aliens[i].speed;
         if (aliens[i].x > (theCanvas.width-aliens[i].width) || aliens[i].x < 0) {
            aliens[i].speed *= -1;
            aliens[i].y += 20;
         }
         if (aliens[i].y > theCanvas.height) {
            aliens.splice(i,1);
         }

      }

The next step in drawScreen() is to detect collisions between the aliens and the missiles. This part of the code loops through the missiles array backward while nesting a loop through the aliens array. It will test every missile against every alien to determine whether there is a collision. Since we have already covered the hitTest() function, we only need to discuss what happens if a collision is detected. First, we call the play() function of the explodeSound. This is the second critical line of code in this iteration of Space Raiders, as it plays (or attempts to play) the explosion sound every time a collision is detected. After that, it splices the alien and missile objects out of their respective arrays, and then breaks out of the nested for:next loop. If there are no aliens left to shoot, we set the appState to STATE_RESET, which will add more aliens to the canvas so the player can continue shooting:

missile: for (var i=missiles.length−1; i>= 0;i−−) {
         var tempMissile = missiles[i]
         for (var j=aliens.length-1; j>= 0;j−−) {
            var tempAlien =aliens[j];
            if (hitTest(tempMissile,tempAlien)) {
               explodeSound.play();
               missiles.splice(i,1);
               aliens.splice(j,1);
               break missile;
             }
         }

         if (aliens.length <=0) {
            appState = STATE_RESET;
         }
      }

The last few lines of code in drawScreen() loop through the missiles and aliens arrays, and draw them onto the canvas. This is done using the drawImage() method of the context object, and the x and y properties we calculated earlier. Finally, it draws the playerImage on the canvas, and the function is finished:

//Draw Missiles
      for (var i=missiles.length−1; i>= 0;i−−) {
         context.drawImage(missileImage,missiles[i].x,missiles[i].y);

      }
//draw aliens
      for (var i=aliens.length−1; i>= 0;i−−) {
         context.drawImage(alienImage,aliens[i].x,aliens[i].y);

      }

//Draw Player
         context.drawImage(playerImage,player.x,player.y);

Like we stated previously, Space Raiders is not a full game. We have only implemented enough to get the player to shoot missiles so we can play the shoot sound, and to detect collisions so we can play the explode sound.

Iteration #1: Playing Sounds Using a Single Object

We just described the first iteration of the dynamic audio code. It works by attempting to call the play() function of both shootSound and explodeSound as often as necessary. This appears to work at first, but if you listen carefully (and this is apparent on some browsers more than others), the sounds start to play “off,” or not play at all. This is because we are using a single object and attempting to play and replay the same sound over and over. A single HTMLAudioElement was not designed to operate this way. You can test this example in the code distribution by running CH7EX6.html in your HTML5-compliant web browser. Press the fire button as quickly as possible and listen to when and how the sounds play. After a bit, they start to play at the wrong time, don’t finish, or don’t play at all. Figure 7-7 shows what the first iteration of Space Raiders looks like in a web browser.

Iteration #2: Creating Unlimited Dynamic Sound Objects

So, we almost got what we wanted with the first iteration, but we ran into some oddities when calling the play() function on a single HTMLAudioElement multiple times before the sound had finished playing.

For our second iteration, we are going to try something different. Let’s see what happens when you simply create a new HTMLAudioElement object every time you want to play a sound. If this doesn’t sound like an efficient use of memory or resources in the web browser, you are a keen observer. It’s actually a horrible idea. However, let’s proceed just to see what happens.

Space Raiders playing sounds from two objects

Figure 7-7. Space Raiders playing sounds from two objects

In canvasApp(), we will create a couple constants that represent the filenames of the sounds we want to play, but without the associated extension. We will still retrieve the extension with a call to supportedAudioFormat(), just as we did in the first iteration, and store that value in the audioType variable.

We will also create an array named sounds that we will use to hold all the HTMLAudioElement objects we create. This array will tell us how many objects we have created so we can visually see when all hell breaks loose:

const SOUND_EXPLODE = "explode1";
const SOUND_SHOOT  = "shoot1";
var sounds = new Array();

Instead of calling the play() function of each sound directly, we are going to create a function named playSound(). This function accepts two parameters:

sound

One of the constants we created above that contains the name of the sound file

volume

A number between 0 and 1 that represents the volume of the sound to play

The function here creates a new sound object every time it is called by calling the createElement() function of the document DOM object. It then sets the properties (src, loop, volume) and attempts to play the sound. Just for fun, let’s push the object into the sounds array:

function playSound(sound,volume) {
   var tempSound = document.createElement("audio");
   tempSound.setAttribute("src", sound + "." + audioType);
   tempSound.loop = false;
   tempSound.volume = volume;
   tempSound.play();
   sounds.push(tempSound);
}

To play the sounds, we call playSound(), passing the proper parameters.

The call in eventMouseUp() looks like this:

playSound(SOUND_SHOOT,.5);

And in drawScreen() it looks like this:

playSound(SOUND_EXPLODE,.5);

To display on the canvas how many sounds we have created, we add this code to the drawScreen() function:

context.fillStyle  = "#FFFFFF";
context.fillText  ("Active Sounds: " + sounds.length,  200 ,480);

Now, go ahead and try this example (CH7EX7.html in the code distribution). Figure 7-8 shows what Space Raiders iteration #2 looks like. Notice we have added some display text at the bottom of the screen to show how many sounds are in the sounds array. You will discover two issues with this iteration:

  1. The sounds play with almost no pauses when loaded from a local drive. But when the page is loaded from a remote website, there is a defined pause before each sound is loaded and played.

  2. The number of sound objects created is a huge problem. For some browsers, such as Chrome, the number of active sounds caps out at about 50. After that, no sounds play at all.

Space Raiders creating sounds on the fly

Figure 7-8. Space Raiders creating sounds on the fly

Iteration #3: Creating a Sound Pool

So, now we know we don’t want to play an HTMLAudioElement repeatedly, or create unlimited sound objects on the fly. However, what if we cap the number of audio objects we create, and put those objects in a pool so we can use them over and over? This will save us memory, and after the sounds are loaded, we shouldn’t see any loading pause before they are played, right?

We will implement a solution that uses HTMLAudioElement objects as general-purpose sound objects. We will keep a pool of them, and change the src attribute to whatever sound we want to play. This appears to be an elegant solution that reuses as much as possible, in addition to giving us a lot of flexibility as to which sounds we want to play.

In canvasApp(), we will create a new constant named MAX_SOUNDS. This will represent the maximum number of sound objects we can create at any one time. We will also rename our sounds array to soundPool to better describe its purpose:

const MAX_SOUNDS = 8;
var soundPool = new Array();

The big change here is the playSound() function. It uses the same parameters as the one from iteration #2, but the functionality is very different:

function playSound(sound,volume) {

The first half of the function loops through the soundPool array to see whether any of the HTMLAudioElement objects in the pool are available to play a sound. We determine this by checking the ended property. Since only HTMLAudioElement objects that have previously been used to play a sound are put into the pool, the ended property will be set to true once the sound has finished playing. By replaying sounds that have finished, we remove the issue of trying to reuse an HTMLAudioElement object to play a sound while it is already in use:

var soundFound = false;
var soundIndex = 0;
var tempSound;

if (soundPool.length> 0) {
   while (!soundFound && soundIndex < soundPool.length) {

      var tSound = soundPool[soundIndex];

      if (tSound.ended) {
         soundFound = true;
      } else {
        soundIndex++;
      }

   }
}
if (soundFound) {
   tempSound = soundPool[soundIndex];
   tempSound.setAttribute("src", sound + "." + audioType);
   tempSound.loop = false;
   tempSound.volume = volume;
   tempSound.play();

If we don’t find a sound, and the size of the pool is less than MAX_SOUNDS, we go ahead and create a new HTMLAudioElement, call its play() function, and push it into the sound pool. This keeps the pool from getting too large, while making sure there are not too many HTMLAudioElement objects in the browser at any one time:

   } else if (soundPool.length < MAX_SOUNDS){
      tempSound = document.createElement("audio");
      tempSound.setAttribute("src", sound + "." + audioType);
      tempSound.volume = volume;
      tempSound.play();
      soundPool.push(tempSound);
   }

}

You can go ahead and try this iteration by loading CH7EX8.html in your HTML5-compliant web browser. In this case, it works! You hear every sound, and the browser doesn’t die like it would with iteration #2.

Unfortunately, there are some issues. On some browsers, there is still a pause before a sound plays, just like with iteration #2. Again, this happens more often when the page is loaded from an external website than when it is loaded locally in a web browser.

The worst manifestation of this comes in Google Chrome, where the sounds pause every time they are played. Also, in Firefox, the src doesn’t change for all the objects, making the shoot sound play when the explode sound should play, and vice versa.

Uh-oh, it looks like we need another iteration. Figure 7-9 shows Space Raiders playing with a pool size governed by MAX_SOUNDS.

Space Raiders with a sound pool

Figure 7-9. Space Raiders with a sound pool

Iteration #4: Reusing Preloaded Sounds

Even though the code in iteration #3 was pretty clean, it simply did not work for us. Instead, we need to compromise and implement a solution that is less elegant, but that works to play sounds nearly every time they are needed. This solution must also work both locally and when loaded from a website.

For this final iteration, we are going to use a sound pool just like in iteration #3, but it will operate in a different way. We will not reuse sound objects for different sound files. Instead, we will load all our sounds up front, and simply play a sound object that is currently not being used. In effect, we will “prime the pump,” creating three sound objects for each of our two sounds for a total of six sound objects when we start the application. While this may not seem like the perfect solution, it appears to work fairly well in all browsers and plays sounds in the most effective way.

In canvasApp(), we set our MAX_SOUNDS constant to 6. We could make it higher, but for this example we will limit it to the number of sounds we will create and preload:

const MAX_SOUNDS = 6;

We then create six variables to hold our HTMLAudioElement objects: three for the explode sound…

var explodeSound ;
var explodeSound2 ;
var explodeSound3 ;

…and three for the shoot sound:

var shootSound;
var shootSound2;
var shootSound3;

In the initApp() function, we preload all of these sound objects. Yes, we load the same object multiple times:

explodeSound = document.createElement("audio");
document.body.appendChild(explodeSound);
audioType = supportedAudioFormat(explodeSound);
explodeSound.setAttribute("src", "explode1." + audioType);
explodeSound.addEventListener("canplaythrough",itemLoaded,false);

explodeSound2 = document.createElement("audio");
document.body.appendChild(explodeSound2);
explodeSound2.setAttribute("src", "explode1." + audioType);
explodeSound2.addEventListener("canplaythrough",itemLoaded,false);

explodeSound3 = document.createElement("audio");
document.body.appendChild(explodeSound3);
explodeSound3.setAttribute("src", "explode1." + audioType);
explodeSound3.addEventListener("canplaythrough",itemLoaded,false);

shootSound = document.createElement("audio");
document.body.appendChild(shootSound);
shootSound.setAttribute("src", "shoot1." + audioType);
shootSound.addEventListener("canplaythrough",itemLoaded,false);

shootSound2 = document.createElement("audio");
document.body.appendChild(shootSound2);
shootSound2.setAttribute("src", "shoot1." + audioType);
shootSound2.addEventListener("canplaythrough",itemLoaded,false);

shootSound3 = document.createElement("audio");
document.body.appendChild(shootSound3);
shootSound3.setAttribute("src", "shoot1." + audioType);
shootSound3.addEventListener("canplaythrough",itemLoaded,false);

In the itemLoaded() function, we remove the event listeners for all six loaded sounds:

shootSound.removeEventListener("canplaythrough",itemLoaded, false);
shootSound2.removeEventListener("canplaythrough",itemLoaded, false);
shootSound3.removeEventListener("canplaythrough",itemLoaded, false);
explodeSound.removeEventListener("canplaythrough",itemLoaded,false);
explodeSound2.removeEventListener("canplaythrough",itemLoaded,false);
explodeSound3.removeEventListener("canplaythrough",itemLoaded,false);

Then, we push each sound into our soundPool array. However, this time, we push them as dynamic objects so we can set the following properties, which don’t exist in the HTMLAudioElement object:

name

The name of the sound file to play (again, without the extension).

element

The reference to the HTMLAudioElement object.

played

A Boolean that tells us whether this sound has played once or not. We need this property because we are putting all of these sound objects into our array, but they have not been played yet. That means their ended property has not yet been set to true. The played property tells us whether the sound is ready to play—that is, it has not been played yet. We will set this to true after we play the sound once:

soundPool.push({name:"explode1", element:explodeSound, played:false});
soundPool.push({name:"explode1", element:explodeSound2, played:false});
soundPool.push({name:"explode1", element:explodeSound3, played:false});
soundPool.push({name:"shoot1", element:shootSound, played:false});
soundPool.push({name:"shoot1", element:shootSound2, played:false});
soundPool.push({name:"shoot1", element:shootSound3, played:false});

Now we need to make a change in our resetApp() function. This change is to support sounds playing in Chrome, which appears to be the only browser that has a slight issue with loading sounds in this manner. The first time you play a sound in Chrome, there is a pause before it starts. To alleviate this, we play each sound type once but set the volume to 0. This will make sure a sound is loaded and ready to play the first time we call playSound() in Chrome:

function resetApp() {

   playSound(SOUND_EXPLODE,0);
   playSound(SOUND_SHOOT,0);
   startLevel();
   appState = STATE_PLAYING;


  }

The playSound() function operates in a similar way to iteration #3. It loops through the soundPool array looking for a sound that it can play. However, in this version, we check to see whether the HTMLAudioElement has ended (tSound.element.ended) or if it has not been played (!tSound.played) yet. We also check whether the value in the sound parameter matches the name property of the sound object in soundPool (tSound.name == sound):

function playSound(sound,volume) {

      var soundFound = false;
      var soundIndex = 0;
      var tempSound;

      if (soundPool.length > 0) {
         while (!soundFound && soundIndex < soundPool.length) {

            var tSound = soundPool[soundIndex];
            if ((tSound.element.ended || !tSound.played) && tSound.name == sound) {
               soundFound = true;
               tSound.played = true;
            } else {
               soundIndex++;
            }

         }
      }

Using this method, we play a sound only if it has not been played, it has ended, and it already has the sound file loaded that we need to play. There is no pause to load (most of the time), and sounds play at pretty much the time we need them to play. If we need more sounds, we can load more up front, or set MAX_SOUNDS to a number greater than the number of preloaded sounds. If we do that, we will create new sound objects on the fly (although this might still give you a pause when loading from a web server):

if (soundFound) {
         tempSound = soundPool[soundIndex].element;
         tempSound.volume = volume;
         tempSound.play();

      } else if (soundPool.length < MAX_SOUNDS){
         tempSound = document.createElement("audio");
         tempSound.setAttribute("src", sound + "." + audioType);
         tempSound.volume = volume;
         tempSound.play();
         soundPool.push({name:sound, element:tempSound, type:audioType, played:true});
      }

Go ahead and try this code. It is CH7EX9.html in the code distribution, or you can type in the program listing.

Other stuff you could do to improve the game

Since the next couple chapters introduce game concepts, we really shouldn’t go much further with Space Raiders. Still, if you were going to finish this game, these are the things you might consider doing:

  1. Add a score.

  2. Increase the aliens’ speed on each new level.

  3. Collision-detect the aliens and the player.

  4. Make an object pool for missiles and aliens.

  5. Slow down firing with a wait() state or frame counter.

  6. Add explosions.

  7. Include a title sequence, level sequence, and end game sequence.

  8. Add a looping soundtrack.

The final code for Space Raiders

Example 7-6 shows the final code for the Space Raiders game (CH7EX9.html).

Example 7-6. Space Raiders with optimized dynamic network sound and state loader

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH7EX9: Space Raiders With Optimized Dynamic Network Sound And State Loader</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);

function eventWindowLoaded() {

   canvasApp();

}

function supportedAudioFormat(audio) {
   var returnExtension = "";
   if (audio.canPlayType("audio/ogg") =="probably" || 
       audio.canPlayType("audio/ogg") == "maybe") {
         returnExtension = "ogg";   } else if(audio.canPlayType("audio/wav") =="probably" || 
       audio.canPlayType("audio/wav") == "maybe") {
         returnExtension = "wav";
   } else if(audio.canPlayType("audio/mp3") == "probably" || 
       audio.canPlayType("audio/mp3") == "maybe") {
         returnExtension = "mp3";
   }

   return returnExtension;

}

function canvasSupport () {
     return Modernizr.canvas;
}

function canvasApp() {

   const STATE_INIT  = 10;
   const STATE_LOADING = 20;
   const STATE_RESET  = 30;
   const STATE_PLAYING = 40;
   var appState = STATE_INIT;
   var loadCount= 0;
   var itemsToLoad = 0;
   var alienImage = new Image();
   var missileImage = new Image();
   var playerImage = new Image();

   const SOUND_EXPLODE = "explode1";
   const SOUND_SHOOT  = "shoot1";
   const MAX_SOUNDS = 6;
   var soundPool = new Array();
   var explodeSound ;
   var explodeSound2 ;
   var explodeSound3 ;
   var shootSound;
   var shootSound2;
   var shootSound3;
   var audioType;

   var mouseX;
   var mouseY;
   var player = {x:250,y:475};
   var aliens = new Array();
   var missiles = new Array();

   const ALIEN_START_X = 25;
   const ALIEN_START_Y = 25;
   const ALIEN_ROWS = 5;
   const ALIEN_COLS = 8;
   const ALIEN_SPACING = 40;

   if (!canvasSupport()) {
          return;
        }

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");

  function itemLoaded(event) {

   loadCount++;
   if (loadCount >= itemsToLoad) {

      shootSound.removeEventListener("canplaythrough",itemLoaded, false);
      shootSound2.removeEventListener("canplaythrough",itemLoaded, false);
      shootSound3.removeEventListener("canplaythrough",itemLoaded, false);
      explodeSound.removeEventListener("canplaythrough",itemLoaded,false);
      explodeSound2.removeEventListener("canplaythrough",itemLoaded,false);
      explodeSound3.removeEventListener("canplaythrough",itemLoaded,false);
      soundPool.push({name:"explode1", element:explodeSound, played:false});
      soundPool.push({name:"explode1", element:explodeSound2, played:false});
      soundPool.push({name:"explode1", element:explodeSound3, played:false});
      soundPool.push({name:"shoot1", element:shootSound, played:false});
      soundPool.push({name:"shoot1", element:shootSound2, played:false});
      soundPool.push({name:"shoot1", element:shootSound3, played:false});

      appState = STATE_RESET;

   }

  }

  function initApp() {
   loadCount=0;
   itemsToLoad = 9;
   explodeSound = document.createElement("audio");
   document.body.appendChild(explodeSound);
   audioType = supportedAudioFormat(explodeSound);
   explodeSound.setAttribute("src", "explode1." + audioType);
   explodeSound.addEventListener("canplaythrough",itemLoaded,false);

   explodeSound2 = document.createElement("audio");
   document.body.appendChild(explodeSound2);
   explodeSound2.setAttribute("src", "explode1." + audioType);
   explodeSound2.addEventListener("canplaythrough",itemLoaded,false);

   explodeSound3 = document.createElement("audio");
   document.body.appendChild(explodeSound3);
   explodeSound3.setAttribute("src", "explode1." + audioType);
   explodeSound3.addEventListener("canplaythrough",itemLoaded,false);

   shootSound = document.createElement("audio");
   document.body.appendChild(shootSound);
   shootSound.setAttribute("src", "shoot1." + audioType);
   shootSound.addEventListener("canplaythrough",itemLoaded,false);

   shootSound2 = document.createElement("audio");
   document.body.appendChild(shootSound2);
   shootSound2.setAttribute("src", "shoot1." + audioType);
   shootSound2.addEventListener("canplaythrough",itemLoaded,false);

   shootSound3 = document.createElement("audio");
   document.body.appendChild(shootSound3);
   shootSound3.setAttribute("src", "shoot1." + audioType);
   shootSound3.addEventListener("canplaythrough",itemLoaded,false);

   alienImage = new Image();
   alienImage.onload = itemLoaded;
   alienImage.src = "alien.png";
   playerImage = new Image();
   playerImage.onload = itemLoaded;
   playerImage.src = "player.png";
   missileImage = new Image();
   missileImage.onload = itemLoaded;
   missileImage.src = "missile.png";
   appState = STATE_LOADING;
   }

  function startLevel() {

      for (var r = 0; r < ALIEN_ROWS; r++) {
         for( var c= 0; c < ALIEN_COLS; c++) {
            aliens.push({speed:2,x:ALIEN_START_X+c*ALIEN_SPACING, y:ALIEN_START_Y+r*
                ALIEN_SPACING,width:alienImage.width, height:alienImage.height});
         }
      }
   }

  function resetApp() {

   playSound(SOUND_EXPLODE,0);
   playSound(SOUND_SHOOT,0);
   startLevel();
   appState = STATE_PLAYING;

  }

  function  drawScreen () {

      //Move missiles
      for (var i=missiles.length−1; i>= 0;i−−) {
         missiles[i].y −= missiles[i].speed;
         if (missiles[i].y < (0-missiles[i].height)) {
            missiles.splice(i,1);
         }

      }

      //Move Aliens
      for (var i=aliens.length−1; i>= 0;i−−) {
         aliens[i].x += aliens[i].speed;
         if (aliens[i].x > (theCanvas.width-aliens[i].width) || aliens[i].x < 0) {
            aliens[i].speed *= -1;
            aliens[i].y += 20;
         }
         if (aliens[i].y > theCanvas.height) {
            aliens.splice(i,1);
         }

      }

      //Detect Collisions
      missile: for (var i=missiles.length−1; i>= 0;i−−) {
         var tempMissile = missiles[i]
         for (var j=aliens.length−1; j>= 0;j−−) {
            var tempAlien =aliens[j];
            if (hitTest(tempMissile,tempAlien)) {
               playSound(SOUND_EXPLODE,.5);
               missiles.splice(i,1);
               aliens.splice(j,1);
               break missile;
             }
         }

         if (aliens.length <=0) {
            appState = STATE_RESET;
         }
      }

      //Background
      context.fillStyle = "#000000";
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = "#EEEEEE";
      context.strokeRect(5,  5, theCanvas.width−10, theCanvas.height−10);

      //Draw Player
      context.drawImage(playerImage,player.x,player.y);

      //Draw Missiles
      for (var i=missiles.length−1; i>= 0;i−−) {
         context.drawImage(missileImage,missiles[i].x,missiles[i].y);

      }

      //draw aliens
      for (var i=aliens.length−1; i>= 0;i−−) {
         context.drawImage(alienImage,aliens[i].x,aliens[i].y);

      }

      //Draw Text
      context.fillStyle = "#FFFFFF";
      context.fillText  ("Active Sounds: " + soundPool.length,  200 ,480);

   }

   function hitTest(image1,image2)  {
      r1left = image1.x;
      r1top = image1.y;
      r1right = image1.x + image1.width;
      r1bottom = image1.y + image1.height;
      r2left = image2.x;
      r2top = image2.y;
      r2right = image2.x + image2.width;
      r2bottom = image2.y + image2.height;
      retval = false;

      if ( (r1left > r2right) || (r1right < r2left) || (r1bottom < r2top) || 
           (r1top > r2bottom) ) {
         retval = false;
      } else {
         retval = true;
      }

      return retval;
   }

   function eventMouseMove(event) {
      if ( event.layerX ||  event.layerX == 0) { // Firefox
            mouseX = event.layerX ;
         mouseY = event.layerY;
        } else if (event.offsetX || event.offsetX == 0) { // Opera
          mouseX = event.offsetX;
         mouseY = event.offsetY;
        }

      player.x = mouseX;
      player.y = mouseY;

   }

   function eventMouseUp(event) {

      missiles.push({speed:5, x: player.x+.5*playerImage.width, 
         y:player.y-missileImage.height,width:missileImage.width, 
         height:missileImage.height});

      playSound(SOUND_SHOOT,.5);
   }

   function playSound(sound,volume) {

      var soundFound = false;
      var soundIndex = 0;
      var tempSound;

      if (soundPool.length> 0) {
         while (!soundFound && soundIndex < soundPool.length) {

            var tSound = soundPool[soundIndex];
            if ((tSound.element.ended || !tSound.played) && tSound.name == sound) {
               soundFound = true;
               tSound.played = true;
            } else {
               soundIndex++;
            }

         }
      }
      if (soundFound) {
         tempSound = soundPool[soundIndex].element;
         tempSound.volume = volume;
         tempSound.play();

      } else if (soundPool.length < MAX_SOUNDS){
         tempSound = document.createElement("audio");
         tempSound.setAttribute("src", sound + "." + audioType);
         tempSound.volume = volume;
         tempSound.play();
         soundPool.push({name:sound, element:tempSound, type:audioType, played:true});
      }

   }

   function run() {
      switch(appState) {
      case STATE_INIT:
         initApp();
         break;
      case STATE_LOADING:
         //wait for call backs
         break;
      case STATE_RESET:
         resetApp();
         break;
      case STATE_PLAYING:
         drawScreen();
         break;

   }

  }

   theCanvas.addEventListener("mouseup",eventMouseUp, false);
   theCanvas.addEventListener("mousemove",eventMouseMove, false);

   setInterval(run, 33);

}


</script>
</head>

<body>
<div style="position: absolute; top: 50px; left: 50px;">

<canvas id="canvasOne" width="500" height="500">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

The best content for your career. Discover unlimited learning on demand for around $1/day.