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

Chapter 9. Combining Bitmaps and Sound

Geo Blaster Basic was constructed using pure paths for drawing. In its creation, we began to cover some game-application-related topics, such as basic collision detection and state machines. In this chapter, we will focus on using bitmaps and tile sheets for our game graphics, and we will also add sound using techniques introduced in Chapter 7.

Along the way, we will update the FrameRateCounter from Chapter 8 by adding in a “step timer.” We will also examine how we can eliminate the use of a tile sheet for rotations by precreating an array of imageData instances using the getImageData() and putImageData() Canvas functions.

In the second half of this chapter, we will create another small turn-based strategy game using bitmaps. This game will be roughly based on the classic computer game Daleks.

Geo Blaster Extended

We will create a new game, Geo Blaster Extended, by adding bitmaps and sound to the Geo Blaster Basic game from Chapter 8. Much of the game logic will be the same, but adding bitmaps to replace paths will enable us to optimize the game for rendering. Optimized rendering is very important when you are targeting limited-processor devices, such as mobile phones. We will also add sound to Geo Blaster Extended, and apply an object pool to the particles used for game explosions. Figure 9-1 shows an example screen of the finished game.

Geo Blaster Extended

Figure 9-1. Geo Blaster Extended

First, let’s look at the tile sheets we will use for our new game.

Geo Blaster Tile Sheet

In Chapter 4, we examined applying bitmap graphics to the canvas, and we explored using tile sheet methods to render images. In Chapter 8, we drew all our game graphics as paths and transformed them on the fly. In this chapter, we will apply the concepts from Chapter 4 to optimizing the rendering of the Geo Blaster Basic game. We will do this by prerendering all of our game graphics and transformations as bitmaps. We will then use these bitmaps instead of paths and the immediate-mode transformations that were necessary in Chapter 8 to create Geo Blaster Extended.

Figure 9-2 shows one of the tile sheets we will use for this game (ship_tiles.png).

These tiles are the 36 rotations for our player ship. We are “canning” the rotations in a tile sheet to avoid spending processor cycles transforming them on each frame tick as we draw them to the canvas.

The ship_tiles.png tile sheet

Figure 9-2. The ship_tiles.png tile sheet

Figure 9-3 shows a second set of tiles for the ship with the “thruster” firing (ship_tiles2.png). We will use this set to depict the ship when the user is pressing the up arrow key.

The ship_tiles2.png tile sheet

Figure 9-3. The ship_tiles2.png tile sheet

The next three sets of tiles are for the rocks that the player will destroy. We have three sheets for these: largerocks.png (Figure 9-4), mediumrocks.png (Figure 9-5), and smallrocks.png (Figure 9-6).

The largerocks.png tile sheet

Figure 9-4. The largerocks.png tile sheet

The mediumrocks.png tile sheet

Figure 9-5. The mediumrocks.png tile sheet

The smallrocks.png tile sheet

Figure 9-6. The smallrocks.png tile sheet

These three tile sheets only need to be five tiles each. Since the rock is a square, we can simply repeat the five frames to simulate rotation in either the clockwise or counterclockwise direction.

The saucer that attempts to shoot the player is a single tile, saucer.png, shown in Figure 9-7.

The saucer.png tile

Figure 9-7. The saucer.png tile

Finally, parts.png (Figure 9-8), is a tiny 8×2 tile sheet that contains four 2×2 “particle” tiles. These will be used for the explosions and missiles fired by the player and the saucer.

The parts.png tile sheet

Figure 9-8. The parts.png tile sheet

You cannot see the colors in a black-and-white printed book, but you can view them by downloading the files from this book’s website. The first tile is green, and it will be used for the small rock and saucer explosions. The second tile is light blue, and it will depict the player’s missiles and the player explosion. The third tile is reddish pink (salmon, if you will), and it will illustrate the large rock explosions. The final, purple tile will be used for the medium rock explosions.

Now that we have our tiles in place, let’s look at the methods we will use to transform Geo Blaster Basic’s immediate-mode path, rendering it to Geo Blaster Extended’s tile-based bitmap.

Refresher: Calculating the tile source location

In Chapter 4, we examined the method to calculate a tile’s location on a tile sheet if we know the single-dimension id of that tile. Let’s briefly look back at this, as it will be reused to render all the tiles for the games in this chapter.

Given that we have a tile sheet such as ship_tiles.png, we can locate the tile we want to display with a simple math trick.

ship_tiles.png is a 36-tile animation with the player ship starting in the 0-degree angle, or “pointing right” direction. Each of the remaining 35 tiles displays the ship rotating in 10-degree increments.

If we would like to display tile 19 (the ship pointing to the left, or in the 190-degree angle), we first need to find the x and y coordinates for the top-left corner of the tile, by calculating sourceX and sourceY.

Here is pseudocode for the sourceX calculation:

sourceX = integer(current_frame_index modulo
the_number_columns_in_the_tilesheet) * tile_width

The modulo (%) operator will return the remainder of the division calculation. Below is the actual code (with variables replaced with literals) we will use for this calculation:

var sourceX = Math.floor(19 % 10) *32;

The result is x = 9*32 = 288;.

The calculation for the sourceY value is similar except we divide rather than use the modulo operator:

sourceY = integer(current_frame_index divided by
the_number_columns_in_the_tilesheet) *tile_height

Here’s the actual code we will use for this calculation:

var sourceY = Math.floor(19 / 10) *32;

This works out to y = 1*32 = 32;. So, the top-left location on the ship_tiles.png from which to start copying pixels is 288,32.

To actually copy this to the canvas, we will use this statement:

context.drawImage(shipTiles, sourceX, sourceY,32,32,player.x,player.y,32,32);

In Chapter 8, we needed quite a lot of code to draw and translate the player ship at the current rotation. When we use a tile sheet, this code is reduced considerably.

Here is the code we will use to render the player ship. It will replace the renderPlayer() function in Example 8-12 in Chapter 8:

function renderPlayerShip(x,y,rotation, scale) {
    //transformation
    context.save(); //save current state in stack
    context.globalAlpha = parseFloat(player.alpha);
    var angleInRadians = rotation * Math.PI / 180;
    var sourceX = Math.floor((player.rotation/10) % 10) * 32;
    var sourceY = Math.floor((player.rotation/10) /10) *32;
    if (player.thrust){
       context.drawImage(shipTiles2, sourceX, sourceY, 32, 32,
          player.x,player.y,32,32);
    }else{
       context.drawImage(shipTiles, sourceX, sourceY, 32, 32,
       player.x,player.y,32,32);
    }

    //restore context
    context.restore(); //pop old state on to screen

    context.globalAlpha = 1;

 }

Note

You will find the entire source code for Geo Blaster Extended (Example 9-1) later, in Geo Blaster Extended Full Source.

The renderPlayer() function divides the player.rotation by 10 to determine which of the 36 tiles in the shipTiles image instance to display on the canvas. If the player is in “thrust” mode, the shipTiles2 image is used instead of shipTiles.

This works because we have set the ship to rotate by 10 degrees with each press of the left or right arrow key. In Chapter 8’s version of the game, we set this to 5 degrees. If we had created a 72-frame tile sheet, with the player ship rotated in 5-degree increments, we could have kept the player.rotationalVelocity at 5. For Geo Blaster Extended, we only drew 36 tiles for the player ship, so we are using the value 10 for the rotational velocity. There certainly is no reason why we could not use 72 or even 360 frames for the player ship rotation tiles. This is only limited by creative imagination (and patience with a drawing tool).

Let’s look at the rotationalVelocity value assigned earlier in the gameStateNewGame() function:

function gameStateNewGame(){
   ConsoleLog.log("gameStateNewGame")
   //setup new game
   level = 0;
   score = 0;
   playerShips = 3;
   player.maxVelocity = 5;
   player.width = 32;
   player.height = 32;
   player.halfWidth = 16;
   player.halfHeight = 16;
   player.hitWidth = 24;
   player.hitHeight = 24;
   player.rotationalVelocity = 10; //how many degrees to turn the ship
   player.thrustAcceleration = .05;
   player.missileFrameDelay = 5;
   player.thrust = false;
   player.alpha = 1;
   player.rotation = 0;
   player.x = 0;
   player.y = 0;

   fillBackground();
   renderScoreBoard();
   switchGameState(GAME_STATE_NEW_LEVEL)

}

Other new player attributes

Along with the change in the rotational velocity, we have also modified the player’s width and height attributes. These are both now 32, which is the same as the tile width and height. If you look at the first frame of the ship_tiles.png tile sheet, you will see that the player ship does not fill the entire 32×32 tile. It is centered in the middle, taking up roughly 24×24 of the tile, which leaves enough space around the edges of the tile to eliminate clipping when the ship is rotated. We also used this concept when we created the rock rotations.

The extra pixels of padding added to eliminate clipping during frame rotation poses a small problem for collision detection. In the Chapter 8 version of the game, we used the width and height values for bounding box collision detection. We will not use those values in this new version because we have created two new variables to use for collision detection: hitWidth and hitHeight. Instead of setting these values to 32, they are 24. This new smaller value makes our collision detection more accurate than if we used the entire tile width and height.

The new boundingBoxCollide() algorithm

All the other game objects will also have new hitWidth and hitHeight attributes. We will modify the boundingBoxCollide() function from Geo Blaster Basic to use these new values for all collision testing:

function boundingBoxCollide(object1, object2) {

   var left1 = object1.x;
   var left2 = object2.x;
   var right1 = object1.x + object1.hitWidth;
   var right2 = object2.x + object2.hitWidth;
   var top1 = object1.y;
   var top2 = object2.y;
   var bottom1 = object1.y + object1.hitHeight;
   var bottom2 = object2.y + object2.hitHeight;

   if (bottom1 < top2) return(false);
   if (top1 > bottom2) return(false);

   if (right1 < left2) return(false);
   if (left1 > right2) return(false);

   return(true);

   }

Next, we will take a quick look at how we will use these same ideas to render the rest of the game objects with the new tile sheets.

Rendering the Other Game Objects

The rocks, saucers, missiles, and particles will all be rendered in a manner similar to the method implemented for the player ship. Let’s first look at the code for the saucer’s render function.

Rendering the saucers

The saucers do not have a multiple-cell tile sheet, but to be consistent, we will render them as though they do. This will allow us to add more animation tiles for the saucers later:

function renderSaucers() {
   var tempSaucer = {};
   var saucerLength = saucers.length-1;
   for (var saucerCtr=saucerLength;saucerCtr>=0;saucerCtr--){
      //ConsoleLog.log("saucer: " + saucerCtr);
      tempSaucer = saucers[saucerCtr];

      context.save(); //save current state in stack
      var sourceX = 0;
      var sourceY = 0;
      context.drawImage(saucerTiles, sourceX, sourceY, 30, 15,
      tempSaucer.x,tempSaucer.y,30,15);
      context.restore(); //pop old state on to screen
   }
}

There is no need to actually calculate the sourceX and sourceY values for the saucer because the saucer is only a single tile. In this instance, we can just set them to 0. We have hardcoded the saucer.width (30) and saucer.height (15) as an example, but with all the rest of the game objects, we will use the object width and height attributes rather than literals.

Next, let’s look at the rock rendering, which varies slightly from both the player ship and the saucers.

Rendering the rocks

The rock tiles are contained inside three separate tile sheets based on their size (large, medium, and small), and we have used only five tiles for each rock. The rocks are square with a symmetrical pattern, so we only need to precreate a single quarter-turn rotation for each of the three sizes.

Here is the renderRocks() function. Notice that we must “switch” based on the scale of the rock (1=large, 2=medium, 3=small) to choose the right tile sheet to render:

function renderRocks() {
    var tempRock = {};
    var rocksLength = rocks.length-1;
    for (var rockCtr=rocksLength;rockCtr>=0;rockCtr--){
    context.save(); //save current state in stack
    tempRock = rocks[rockCtr];
    var sourceX = Math.floor((tempRock.rotation) % 5) * tempRock.width;
    var sourceY = Math.floor((tempRock.rotation) /5) *tempRock.height;

    switch(tempRock.scale){
       case 1:
       context.drawImage(largeRockTiles, sourceX, sourceY, 
        tempRock.width,tempRock.height,tempRock.x,tempRock.y,
        tempRock.width,tempRock.height);
       break;
       case 2:
       context.drawImage(mediumRockTiles, sourceX, 
        sourceY,tempRock.width,tempRock.height,tempRock.x,tempRock.y,
        tempRock.width,tempRock.height);
       break;
       case 3:
       context.drawImage(smallRockTiles, sourceX, 
        sourceY,tempRock.width,tempRock.height,tempRock.x,tempRock.y,
        tempRock.width,tempRock.height);
       break;

    }

    context.restore(); //pop old state on to screen

    }
 }

In the renderRocks() function, we are no longer using the rock.rotation attribute as the angle of rotation as we did in Geo Blaster Basic. Instead, we have repurposed the rotation attribute to represent the tile id (0–4) of the current tile on the tile sheet to render.

In the Chapter 8 version, we were able to simulate faster or slower speeds for the rock rotations by simply giving each rock a random rotationInc value. This value, either negative for counterclockwise or positive for clockwise, was added to the rotation attribute on each frame. In this new tilesheet-based version, we only have five frames of animation, so we don’t want to skip frames because it will look choppy. Instead, we are going to add two new attributes to each rock: animationCount and animationDelay.

The animationDelay will represent the number of frames between each tile change for a given rock. The animationCount variable will restart at 0 after each tile frame change and will increase by 1 on each subsequent frame tick. When animationCount is greater than animationDelay, the rock.rotation value will be increased (clockwise) or decreased (counterclockwise). Here is the new code that we will have in our updateRocks() function:

tempRock.animationCount++;
   if (tempRock.animationCount > tempRock.animationDelay){
      tempRock.animationCount = 0;
      tempRock.rotation += tempRock.rotationInc;

      if (tempRock.rotation > 4){
         tempRock.rotation = 0;
      }else if (tempRock.rotation <0){
         tempRock.rotation = 4;
      }
   }

You will notice that we have hardcoded the values 4 and 0 into the tile id maximum and minimum checks. We could have just as easily used a constant or two variables for this purpose.

Rendering the missiles

Both the player missiles and saucer missiles are rendered in the same manner. For each, we simply need to know the tile id on the four-tile particleTiles image representing the tile we want to display. For the player missiles, this tile id is 1; for the saucer missile, the tile id is 0.

Let’s take a quick look at both of these functions:

 function renderPlayerMissiles() {
    var tempPlayerMissile = {};
    var playerMissileLength = playerMissiles.length-1;
    //ConsoleLog.log("render playerMissileLength=" + playerMissileLength);
    for (var playerMissileCtr=playerMissileLength; playerMissileCtr>=0;
     playerMissileCtr--){

    //ConsoleLog.log("draw player missile " + playerMissileCtr)
    tempPlayerMissile = playerMissiles[playerMissileCtr];
    context.save(); //save current state in stack
    var sourceX = Math.floor(1 % 4) * tempPlayerMissile.width;
    var sourceY = Math.floor(1 / 4) * tempPlayerMissile.height;

    context.drawImage(particleTiles, sourceX, sourceY,
     tempPlayerMissile.width,tempPlayerMissile.height,
     tempPlayerMissile.x,tempPlayerMissile.y,tempPlayerMissile.width,
     tempPlayerMissile.height);

    context.restore(); //pop old state on to screen
    }
    }

function renderSaucerMissiles() {
    var tempSaucerMissile = {};
    var saucerMissileLength = saucerMissiles.length-1;
    //ConsoleLog.log("saucerMissiles= " + saucerMissiles.length)
    for (var saucerMissileCtr=saucerMissileLength;
    saucerMissileCtr >= 0;saucerMissileCtr--){
    //ConsoleLog.log("draw player missile " + playerMissileCtr)
    tempSaucerMissile = saucerMissiles[saucerMissileCtr];
    context.save(); //save current state in stack
    var sourceX = Math.floor(0 % 4) * tempSaucerMissile.width;
    var sourceY = Math.floor(0 / 4) * tempSaucerMissile.height;

    context.drawImage(particleTiles, sourceX, sourceY,
     tempSaucerMissile.width,tempSaucerMissile.height,
     tempSaucerMissile.x,tempSaucerMissile.y,tempSaucerMissile.width,
     tempSaucerMissile.height);

    context.restore(); //pop old state on to screen

    }
    }

The particle explosion will also be rendered using a bitmap tile sheet, and its code will be very similar to the code for the projectiles. Let’s examine the particles next.

Rendering the particles

The particles will use the same four-tile parts.png file (as shown in Figure 9-8) that rendered the projectiles. The Geo Blaster Basic game from Chapter 8 used only a single white particle for all explosions. We replace the createExplode() function from this previous game with a new one that will be able to use a different-colored particle for each type of explosion. This way the rocks, saucers, and player ship can all have unique colored explosions.

The new createExplode() function will handle this by adding a final type parameter to its parameter list. Let’s look at the code:

function createExplode(x,y,num,type) {

   playSound(SOUND_EXPLODE,.5);
   for (var partCtr=0;partCtr<num;partCtr++){
      if (particlePool.length > 0){
        newParticle = particlePool.pop();
      newParticle.dx = Math.random()*3;
      if (Math.random()<.5){
         newParticle.dx *= -1;
      }
      newParticle.dy = Math.random()*3;
      if (Math.random()<.5){
      newParticle.dy *= -1;
      }

      newParticle.life = Math.floor(Math.random()*30+30);
      newParticle.lifeCtr = 0;
      newParticle.x = x;
      newParticle.width = 2;
      newParticle.height = 2;
      newParticle.y = y;
      newParticle.type = type;
      //ConsoleLog.log("newParticle.life=" + newParticle.life);
      particles.push(newParticle);
      }

   }

}

As the particle objects are created in createExplode(), we added a new type attribute to them. When an explosion is triggered in the checkCollisions() function, the call to createExplode() will now include this type value based on the object that was destroyed. Each rock already has a scale parameter that varies from 1 to 3 based on its size. We will use those as our base type value to pass in for the rocks. Now we only need type values for the player and the saucer. For the saucer we will use 0, and for the player we will use 4. We pulled these id values out of the air. We very well could have used 99 for the saucer and 200 for the player. We just could not use 1, 2, or 3 because those values are used for the rocks. The type breakdown looks like this:

  • Saucer: type=0

  • Large rock: type=1

  • Medium rock: type=2

  • Small rock: type=3

  • Player: type=4

This type value will need to be used in a switch statement inside the renderParticles() function to determine which of the four tiles to render for a given particle. Let’s examine this function now:

function renderParticles() {

   var tempParticle = {};
   var particleLength = particles.length-1;
   for (var particleCtr=particleLength;particleCtr>=0;particleCtr--){
      tempParticle = particles[particleCtr];
      context.save(); //save current state in stack

      var tile;

      console.log("part type=" + tempParticle.type)
      switch(tempParticle.type){
          case 0: // saucer
            tile = 0;
            break;
          case 1: //large rock
            tile = 2
            break;
          case 2: //medium rock
            tile = 3;
            break;
          case 3: //small rock
            tile = 0;
            break;
          case 4: //player
            tile = 1;
            break;

      }

      var sourceX = Math.floor(tile % 4) * tempParticle.width;
      var sourceY = Math.floor(tile / 4) * tempParticle.height;

      context.drawImage(particleTiles, sourceX, sourceY,
      tempParticle.width, tempParticle.height, tempParticle.x,
      tempParticle.y,tempParticle.width,tempParticle.height);

      context.restore(); //pop old state on to screen


 }

In checkCollisions(), we will need to pass the type parameter to the createExplode() function so the type can be assigned to the particles in the explosion. Here is an example of a createExplode() function call used for a rock instance:

createExplode(tempRock.x+tempRock.halfWidth,tempRock.y+tempRock.halfHeight,
 10,tempRock.scale);

We pass the tempRock.scale as the final parameter because we are using the rock’s scale as the type.

For a saucer:

createExplode(tempSaucer.x+tempSaucer.halfWidth,
 tempSaucer.y+tempSaucer.halfHeight,10,0);

For the saucers and the player, we will pass a number literal into the createExplode() function. In the saucer’s case, we pass in a 1. For the player ship, we pass in a 4:

createExplode(player.x+player.halfWidth, player.y+player.halfWidth,50,4);

Note that the createExplode() function call for the player is in the playerDie() function, which is called from checkCollisions().

Note

After we discuss adding sound and a particle pool to this game, we will present the entire set of code (Example 9-1), replacing the Geo Blaster Basic code. There will be no need to make the changes to the individual functions.

Adding Sound

In Chapter 7, we covered everything we need to know to add robust sound management to our canvas applications. If you are unfamiliar with the concepts presented in Chapter 7, please review that chapter first. In this chapter, we will cover only the code necessary to include sound in our game.

Arcade games need to play many sounds simultaneously, and sometimes those sounds play very rapidly in succession. In Chapter 7, we used the HTML5 <audio> tag to create a pool of sounds, solving the problems associated with playing the same sound instance multiple times.

Note

As of this writing, the Opera browser in Windows offers the best support for playing sounds. If you are having trouble with the sound in this game, any other sound example in the book, or in your own games, please test them out in the Opera browser.

The sounds for our game

We will be adding three sounds to our game:

  • A sound for when the player shoots a projectile (shoot1.mp3, .ogg, .wav)

  • A sound for explosions (explode1.mp3, .ogg, .wav)

  • A sound for when the saucer shoots a projectile (saucershoot.mp3, .ogg, .wav)

In the file download for this chapter, we have provided each of the three sounds in three different formats: .wav, .ogg, and .mp3.

Adding sound instances and management variables to the game

In the variable definition section of our game code, we will create variables to work with the sound manager code from Chapter 7. We will create three instances of each sound that goes into our pool:

var explodeSound;
var explodeSound2;
var explodeSound3;
var shootSound;
var shootSound2;
var shootSound3;
var saucershootSound;
var saucershootSound2;
var saucershootSound3;

We also need to create an array to hold our pool of sounds:

var soundPool = new Array();

To control which sound we want to play, we will assign a constant string to each, and to play the sound, we only ever need to use the constant. This way, we can change the sound names easily, which will help in refactoring code if we want to modify the sounds at a later time:

 const SOUND_EXPLODE = "explode1";
     const SOUND_SHOOT = "shoot1";
     const SOUND_SAUCER_SHOOT = "saucershoot"

Finally, we need a variable called audioType, which we will use to reference the current file type (.ogg, .mp3, or .wav) by the sound manager code.

Loading in sounds and tile sheet assets

In Chapter 7, we used a function to load all of the game assets while our state machine waited in an idle state. We will add this code to our game in a function called gameStateInit():

function gameStateInit() {
   loadCount = 0;
   itemsToLoad  = 16;

   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");
   audioType = supportedAudioFormat(shootSound);
   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);

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

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

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

   shipTiles = new Image();
   shipTiles.src = "ship_tiles.png";
   shipTiles.onload = itemLoaded;

   shipTiles2 = new Image();
   shipTiles2.src = "ship_tiles2.png";
   shipTiles2.onload = itemLoaded;

   saucerTiles= new Image();
   saucerTiles.src = "saucer.png";
   saucerTiles.onload = itemLoaded;

   largeRockTiles = new Image();
   largeRockTiles.src = "largerocks.png";
   largeRockTiles.onload = itemLoaded;

   mediumRockTiles = new Image();
   mediumRockTiles.src = "mediumrocks.png";
   mediumRockTiles.onload = itemLoaded;

   smallRockTiles = new Image();
   smallRockTiles.src = "smallrocks.png";
   smallRockTiles.onload = itemLoaded;

   particleTiles = new Image();
   particleTiles.src = "parts.png";
   particleTiles.onload = itemLoaded;

   switchGameState(GAME_STATE_WAIT_FOR_LOAD);

}

Notice that we must create and preload three separate instances of each sound, even though they share the same sound file (or files). In this function, we also load in our tile sheets. The application scope itemsToLoad variable will be used to check against the application scope loadCount variable in the load event callback itemLoaded() function, which is shared by all assets to be loaded. This will make it easy for the application to change state so that it can start playing the game when all assets have loaded. Let’s briefly look at the itemLoaded() function now:

function itemLoaded(event) {

   loadCount++;
   //console.log("loading:" + 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);
       saucershootSound.removeEventListener("canplaythrough",itemLoaded,false);
       saucershootSound2.removeEventListener("canplaythrough",itemLoaded,false);
       saucershootSound3.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});
       soundPool.push({name:"saucershoot", element:saucershootSound, 
        played:false});
       soundPool.push({name:"saucershoot", element:saucershootSound2,
        played:false});
       soundPool.push({name:"saucershoot", element:saucershootSound3,
        played:false});

       switchGameState(GAME_STATE_TITLE)
    }

}

In this function, we first remove the event listener from each loaded item, then add the sounds to our sound pool. Finally, we call the switchGameState() to send the game to the title screen.

Playing sounds

Sounds will be played using the playSound() function from Chapter 7. We will not reprint that function here, but it will be in Example 9-1 where we give the entire set of code for the game. We will call the playSound() function at various instances in our code to play the needed sounds. For example, the createExplode() function presented earlier in this chapter included this line:

playSound(SOUND_EXPLODE,.5);

When we want to play a sound instance from the pool, we call the playSound() function and pass in the constants representing the sound and the volume for the sound. If an instance of the sound is available in the pool, it will be used and the sound will play.

Now, let’s move on to another type of application pool—the object pool.

Pooling Object Instances

We have looked at object pools as they relate to sounds, but we have not applied this concept to our game objects. Object pooling is a technique designed to save processing time, so it is very applicable to an arcade game application such as the one we are building. By pooling object instances, we avoid the sometimes processor-intensive task of creating object instances on the fly during game execution. This is especially applicable to our particle explosions, as we create multiple objects on the same frame tick. On a lower-powered platform, such as a handheld device, object pooling can help increase frame rate.

Object pooling in Geo Blaster Extended

In our game, we will apply the pooling concept to the explosion particles. Of course, we can extend this concept to rocks, projectiles, saucers, and any other type of object that requires multiple instances. For this example, though, let’s focus on the particles. As we will see, adding pooling in JavaScript is a relatively simple but powerful technique.

Adding pooling variables to our game

We will need to add four application scope variables to our game to make use of pooling for our game particle:

 var particlePool = [];
     var maxParticles = 200;
     var newParticle;
     var tempParticle;

The particlePool array will hold the list of particle object instances that are waiting to be used. When createExplode() needs to use a particle, it will first look to see whether any are available in this array. If one is available, it will be “popped” off the top of the particlePool stack and placed in the application scope newParticle variable—which is a reference to the pooled particle. The createExplode() function will set the properties of the newParticle, and then “push” it to the end of the existing particles array.

Once a particle’s life has been exhausted, the updateParticles() function will splice the particle from the particles array and push it back into the particlePool array. We have created the tempParticle reference to alleviate the updateParticles() function’s need to create this instance on each frame tick.

The maxParticles value will be used in a new function called createObjectPools(). We will call this function in the gameStateInit() state function before we create the sound and tile sheet loading events.

Let’s take a look at the createObjectPools() function now:

function createObjectPools(){
   for (var ctr=0;ctr<maxParticles;ctr++){
      var newParticle = {};
      particlePool.push(newParticle)
   }
   console.log("particlePool=" + particlePool.length)
}

As you can see, we simply iterate from 0 to 1 less than the maxParticles value, and place a generic object instance at each element in the pool. When a particle is needed, the createExplode() function will look to see whether particlePool.length is greater than 0. If a particle is available, it will be added to the particles array after its attributes are set. If no particle is available, none will be used.

Note

This functionality can be extended to add a particle as needed to the pool when none is available. We have not added that functionality to our example, but it is common in some pooling algorithms.

Here is the newly modified createExplode() function in its entirety:

function createExplode(x,y,num,type) {

   playSound(SOUND_EXPLODE,.5);
   for (var partCtr=0;partCtr<num;partCtr++){
      if (particlePool.length > 0){

      newParticle = particlePool.pop();
      newParticle.dx = Math.random()*3;
      if (Math.random()<.5){
         newParticle.dx* = -1;
      }
      newParticle.dy = Math.random()*3;
      if (Math.random()<.5){
      newParticle.dy* = -1;
      }

      newParticle.life = Math.floor(Math.random()*30+30);
      newParticle.lifeCtr = 0;
      newParticle.x = x;
      newParticle.width = 2;
      newParticle.height = 2;
      newParticle.y = y;
      newParticle.type = type;
      //ConsoleLog.log("newParticle.life=" + newParticle.life);
      particles.push(newParticle);
      }

   }

}

The updateParticles() function will loop through the particles instances, update the attributes of each, and then check to see whether the particle’s life has been exhausted. If it has, the function will place the particle back in the pool. Here is the code we will add to updateParticles() to replenish the pool:

if (remove) {
   particlePool.push(tempParticle)
   particles.splice(particleCtr,1)

}

Adding in a Step Timer

In Chapter 8, we created a simple FrameRateCounter object prototype that was used to display the current frame rate as the game was running. We are going to extend the functionality of this counter to add in a “step timer.” The step timer will use the time difference calculated between frames to create a “step factor.” This step factor will be used when updating the positions of the objects on the canvas. The result will be smoother rendering of the game objects when there are drops in frame rate, as well as keeping relatively consistent game play on browsers and systems that cannot maintain the frame rate needed to play the game effectively.

How the step timer works

We will update the constructor function for FrameRateCounter to accept in a new single parameter called fps. This value will represent the frames per second that we want our game to run:

function FrameRateCounter(fps) {
   if (fps == undefined){
      this.fps = 40
   }else{
      this.fps = fps
   }

If no fps value is passed in, the value 40 will be used.

We will also add in two new object-level scope variables to calculate the step in our step timer:

this.lastTime = dateTemp.getTime();
this.step = 1;

The lastTime variable will contain the time in which the previous frame completed its work.

We calculate the step by comparing the current time value with the lastTime value on each frame tick. This calculation will occur in the FrameRateCounter countFrames() function:

FrameRateCounter.prototype.countFrames=function() {

   var dateTemp = new Date();

   var timeDifference = dateTemp.getTime()-this.lastTime;
   this.step = (timeDifference/1000)*this.fps;
   this.lastTime = dateTemp.getTime();

The local timeDifference value is calculated by subtracting the lastTime value from the current time (represented by the dateTemp.getTime() return value).

To calculate the step value, divide the timeDifference by 1000 (the number of milliseconds in a single second), and multiply the result by the desired frame rate. If the game is running with no surplus or no deficit in time between frame ticks, the step value will be 1. If the current frame tick took longer than a single frame to finish, the value will be greater than 1 (a deficit). If the current frame took less time than a single frame, the step value will be less than 1 (a surplus).

For example, if the last frame took too long to process, the current frame will compensate by moving each object a little bit more than the step value of 1. Let’s illustrate this with a simple example.

Let’s say we want the saucer to move five pixels to the right on each frame tick. This would be a dx value of 5.

For this example, we will also say that our desired frame rate is 40 FPS. This means that we want each frame tick to use up 25 milliseconds (1000/40 = 25).

Let’s also suppose that the timeDifference between the current frame and the last frame is 26 milliseconds. Our game is running at a deficit of 1 millisecond per frame—this means that the game processing is taking more time than we want it to.

To calculate the step value, divide the timeDifference by 1000: 26/1000 = .026.

We multiply this value by the desired fps for our game: .026 * 40 = 1.04

Our step value is 1.04 for the current frame. Because of the deficit in processing time, we want to move each game object slightly more than a frame so there is no surplus or deficit. In the case of no surplus or deficit, the step value would be 1. If there is a surplus, the step value would be less than 1.

This step value will be multiplied to the changes in movement vectors for each object in the update functions. This allows the game to keep a relatively smooth look even when there are fluctuations in the frame rate. In addition, the game will update the screen in a relatively consistent manner across the various browsers and systems, resulting in game play that is relatively consistent for each user.

Here are the new movement vector calculations for each object:

player

player.x += player.movingX*frameRateCounter.step;
player.y += player.movingY*frameRateCounter.step;
playerMissiles

tempPlayerMissile.x += tempPlayerMissile.dx*frameRateCounter.step;
tempPlayerMissile.y += tempPlayerMissile.dy*frameRateCounter.step;
rocks

tempRock.x += tempRock.dx*frameRateCounter.step;
tempRock.y += tempRock.dy*frameRateCounter.step;
saucers

tempSaucer.x += tempSaucer.dx*frameRateCounter.step;
tempSaucer.y += tempSaucer.dy*frameRateCounter.step;
saucerMissiles

tempSaucerMissile.x += tempSaucerMissile.dx*frameRateCounter.step;
tempSaucerMissile.y += tempSaucerMissile.dy*frameRateCounter.step;
particles

tempParticle.x += tempParticle.dx*frameRateCounter.step;
tempParticle.y += tempParticle.dy*frameRateCounter.step;

We have now covered all of the major changes to turn Geo Blaster Basic into Geo Blaster Extended. Let’s look at Example 9-1, which has the entire code for the final game.

Geo Blaster Extended Full Source

Example 9-1. Geo Blaster Extended full source code listing

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH9EX1: Geo Blaster Extended</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

    canvasApp();

}

function canvasSupport () {
   return Modernizr.canvas;
}

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/wav") == "probably" || 
       audio.canPlayType("audio/wav") == "maybe") {
         returnExtension = "mp3";
   }

   return returnExtension;

}

function canvasApp(){

   if (!canvasSupport()) {
          return;
     }else{
       theCanvas = document.getElementById("canvas");
       context = theCanvas.getContext("2d");
   }

   //sounds
   const SOUND_EXPLODE = "explode1";
   const SOUND_SHOOT = "shoot1";
   const SOUND_SAUCER_SHOOT = "saucershoot"
   const MAX_SOUNDS = 9;
   var soundPool = new Array();
   var explodeSound;
   var explodeSound2;
   var explodeSound3;
   var shootSound;
   var shootSound2;
   var shootSound3;
   var saucershootSound;
   var saucershootSound2;
   var saucershootSound3;
   var audioType;

   //application states
   const GAME_STATE_INIT = 0;
   const GAME_STATE_WAIT_FOR_LOAD = 5;
   const GAME_STATE_TITLE = 10;
   const GAME_STATE_NEW_GAME = 20;
   const GAME_STATE_NEW_LEVEL = 30;
   const GAME_STATE_PLAYER_START = 40;
   const GAME_STATE_PLAY_LEVEL = 50;
   const GAME_STATE_PLAYER_DIE = 60;
   const GAME_STATE_GAME_OVER = 70;
   var currentGameState = 0;
   var currentGameStateFunction = null;

   //title screen
   var titleStarted = false;

   //game over screen
   var gameOverStarted = false;

   //objects for game play

   //game environment
   var score = 0;
   var level = 0;
   var extraShipAtEach = 10000;
   var extraShipsEarned = 0;
   var playerShips = 3;

   //playfield
   var xMin = 0;
   var xMax = 400;
   var yMin = 0;
   var yMax = 400;

   //score values
   var bigRockScore = 50;
   var medRockScore = 75;
   var smlRockScore = 100;
   var saucerScore = 300;

   //rock scale constants
   const ROCK_SCALE_LARGE = 1;
   const ROCK_SCALE_MEDIUM = 2;
   const ROCK_SCALE_SMALL = 3;

   //create game objects and arrays
   var player = {};
   var rocks = [];
   var saucers = [];
   var playerMissiles = [];
   var particles = [];
   var saucerMissiles = [];
   var particlePool = [];
   var maxParticles = 200;
   var newParticle;
   var tempParticle;

   //level specific
   var levelRockMaxSpeedAdjust = 1;
   var levelSaucerMax = 1;
   var levelSaucerOccurrenceRate = 25;
   var levelSaucerSpeed = 1;
   var levelSaucerFireDelay = 300;
   var levelSaucerFireRate = 30;
   var levelSaucerMissileSpeed = 1;

   //keyPresses
   var keyPressList=[];

   //tile sheets
   var shipTiles;
   var shipTiles2;
   var saucerTiles;
   var largeRockTiles;
   var mediumRockTiles;
   var smallRockTiles;
   var particleTiles;

    function itemLoaded(event) {

      loadCount++;
      //console.log("loading:" + 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);
         saucershootSound.removeEventListener("canplaythrough",itemLoaded,false);
         saucershootSound2.removeEventListener("canplaythrough",itemLoaded, 
          false);
         saucershootSound3.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});
         soundPool.push({name:"saucershoot", element:saucershootSound, 
          played:false});
         soundPool.push({name:"saucershoot", element:saucershootSound2,
          played:false});
         soundPool.push({name:"saucershoot", element:saucershootSound3,
          played:false});

         switchGameState(GAME_STATE_TITLE)

      }

   }
   function playSound(sound,volume) {
      ConsoleLog.log("play sound" + sound);
      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) {
         ConsoleLog.log("sound found");
         tempSound = soundPool[soundIndex].element;
         //tempSound.setAttribute("src", sound + "." + audioType);
         //tempSound.loop = false;
         //tempSound.volume = volume;
         tempSound.play();

      } else if (soundPool.length < MAX_SOUNDS){
         ConsoleLog.log("sound not found");
         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 runGame(){
      currentGameStateFunction();
   }

   function switchGameState(newState) {
      currentGameState = newState;
      switch (currentGameState) {

         case GAME_STATE_INIT:
            currentGameStateFunction = gameStateInit;
            break;
         case GAME_STATE_WAIT_FOR_LOAD:
            currentGameStateFunction = gameStateWaitForLoad;
            break;
         case GAME_STATE_TITLE:
             currentGameStateFunction = gameStateTitle;
             break;
         case GAME_STATE_NEW_GAME:
             currentGameStateFunction = gameStateNewGame;
             break;
         case GAME_STATE_NEW_LEVEL:
             currentGameStateFunction = gameStateNewLevel;
             break;
         case GAME_STATE_PLAYER_START:
             currentGameStateFunction = gameStatePlayerStart;
             break;
         case GAME_STATE_PLAY_LEVEL:
             currentGameStateFunction = gameStatePlayLevel;
             break;
         case GAME_STATE_PLAYER_DIE:
             currentGameStateFunction = gameStatePlayerDie;
             break;
         case GAME_STATE_GAME_OVER:
             currentGameStateFunction = gameStateGameOver;
             break;

      }

   }

   function gameStateWaitForLoad(){
      //do nothing while loading events occur
      console.log("doing nothing...")
   }

   function createObjectPools(){
      for (var ctr=0;ctr<maxParticles;ctr++){
         var newParticle = {};
         particlePool.push(newParticle)
      }
      console.log("particlePool=" + particlePool.length)
   }

   function gameStateInit() {
      createObjectPools();

      loadCount = 0;
      itemsToLoad = 16;

      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");
      audioType = supportedAudioFormat(shootSound);
      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);

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

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

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

      shipTiles = new Image();
      shipTiles.src = "ship_tiles.png";
      shipTiles.onload = itemLoaded;

      shipTiles2 = new Image();
      shipTiles2.src = "ship_tiles2.png";
      shipTiles2.onload = itemLoaded;

      saucerTiles= new Image();
      saucerTiles.src = "saucer.png";
      saucerTiles.onload = itemLoaded;

      largeRockTiles = new Image();
      largeRockTiles.src = "largerocks.png";
      largeRockTiles.onload = itemLoaded;

      mediumRockTiles = new Image();
      mediumRockTiles.src = "mediumrocks.png";
      mediumRockTiles.onload = itemLoaded;

      smallRockTiles = new Image();
      smallRockTiles.src = "smallrocks.png";
      smallRockTiles.onload = itemLoaded;

      particleTiles = new Image();
      particleTiles.src = "parts.png";
      particleTiles.onload = itemLoaded;

      switchGameState(GAME_STATE_WAIT_FOR_LOAD);
   }

   function gameStateTitle() {
      if (titleStarted !=true){
         fillBackground();
         setTextStyleTitle();
         context.fillText  ("Geo Blaster X-ten-d", 120, 70);

         setTextStyle();
         context.fillText  ("Press Space To Play", 130, 140);

         setTextStyleCredits();
         context.fillText  ("An HTML5 Example Game", 125, 200);
         context.fillText  ("From our upcoming HTML5 Canvas", 100, 215);
         context.fillText  ("book on O'Reilly Press", 130, 230);

         context.fillText  ("Game Code - Jeff Fulton", 130, 260);
         context.fillText  ("Sound Manager - Steve Fulton", 120, 275);


         titleStarted = true;
      }else{
         //wait for space key click
         if (keyPressList[32]==true){
            ConsoleLog.log("space pressed");
            switchGameState(GAME_STATE_NEW_GAME);
            titleStarted = false;

         }

      }

   }

   function gameStateNewGame(){
      ConsoleLog.log("gameStateNewGame")
      //set up new game
      level = 0;
      score = 0;
      playerShips = 3;
      player.maxVelocity = 5;
      player.width = 32;
      player.height = 32;
      player.halfWidth = 16;
      player.halfHeight = 16;
      player.hitWidth = 24;
      player.hitHeight = 24;
      player.rotationalVelocity = 10; //how many degrees to turn the ship
      player.thrustAcceleration = .05;
      player.missileFrameDelay = 5;
      player.thrust = false;
      player.alpha = 1;
      player.rotation = 0;
      player.x = 0;
      player.y = 0;

      fillBackground();
      renderScoreBoard();
      switchGameState(GAME_STATE_NEW_LEVEL)

   }

   function gameStateNewLevel(){
      rocks = [];
      saucers = [];
      playerMissiles = [];
      particles = [];
      saucerMissiles = [];
      level++;
      levelRockMaxSpeedAdjust = level*.25;
      if (levelRockMaxSpeedAdjust > 3){
         levelRockMaxSpeed = 3;
      }

      levelSaucerMax = 1+Math.floor(level/10);
      if (levelSaucerMax > 5){
         levelSaucerMax = 5;
      }
      levelSaucerOccurrenceRate = 10+3*level;
      if (levelSaucerOccurrenceRate > 35){
         levelSaucerOccurrenceRate = 35;
      }
      levelSaucerSpeed = 1+.5*level;
      if (levelSaucerSpeed>5){
         levelSaucerSpeed = 5;
      }
      levelSaucerFireDelay = 120-10*level;
      if (levelSaucerFireDelay<20) {
         levelSaucerFireDelay = 20;
      }

      levelSaucerFireRate = 20 + 3*level;
      if (levelSaucerFireRate<50) {
         levelSaucerFireRate = 50;
      }

      levelSaucerMissileSpeed = 1+.2*level;
      if (levelSaucerMissileSpeed > 4){
         levelSaucerMissileSpeed = 4;
      }
      //create level rocks
      for (var newRockctr=0;newRockctr<level+3;newRockctr++){
            var newRock = {};

            newRock.scale = 1;
            //scale
            //1 = large
            //2 = medium
            //3 = small
            //these will be used as the divisor for the new size
            //50/1 = 50
            //50/2 = 25
            //50/3 = 16
            newRock.width = 64;
            newRock.height = 64;
            newRock.halfWidth = 32;
            newRock.halfHeight = 32;

            newRock.hitWidth = 48;
            newRock.hitHeight = 48;

            //start all new rocks in upper left for ship safety
            newRock.x = Math.floor(Math.random()*50);
            //ConsoleLog.log("newRock.x=" + newRock.x);
            newRock.y = Math.floor(Math.random()*50);
            //ConsoleLog.log("newRock.y=" + newRock.y);
            newRock.dx = (Math.random()*2)+levelRockMaxSpeedAdjust;
            if (Math.random()<.5){
               newRock.dx *= -1;
            }
            newRock.dy=(Math.random()*2)+levelRockMaxSpeedAdjust;
            if (Math.random()<.5){
               newRock.dy *= -1;
            }
            //rotation speed and direction

            if (Math.random()<.5){
               newRock.rotationInc = -1;
            }else{
               newRock.rotationInc = 1;
            }

            newRock.animationDelay = Math.floor(Math.random()*3+1);
            newRock.animationCount = 0;

            newRock.scoreValue = bigRockScore;
            newRock.rotation = 0;

            rocks.push(newRock);
            //ConsoleLog.log("rock created rotationInc=" + newRock.rotationInc);
         }
      resetPlayer();
      switchGameState(GAME_STATE_PLAYER_START);

   }

   function gameStatePlayerStart(){

      fillBackground();
      renderScoreBoard();
      if (player.alpha < 1){
         player.alpha += .01;

         ConsoleLog.log("player.alpha=" + context.globalAlpha)
      }else{
         switchGameState(GAME_STATE_PLAY_LEVEL);
         player.safe = false; // added chapter 9

      }

      //renderPlayerShip(player.x, player.y,270,1);
      context.globalAlpha = 1;
      //new in chapter 9
      checkKeys();
      update();
      render(); //added chapter 9
      checkCollisions();
      checkForExtraShip();
      checkForEndOfLevel();
      frameRateCounter.countFrames();

   }

   function gameStatePlayLevel(){
      checkKeys();
      update();
      render();
      checkCollisions();
      checkForExtraShip();
      checkForEndOfLevel();
      frameRateCounter.countFrames();

   }

   function resetPlayer() {
      player.rotation = 270;
      player.x = .5*xMax;
      player.y = .5*yMax;
      player.facingX = 0;
      player.facingY = 0;
      player.movingX = 0;
      player.movingY = 0;
      player.alpha = 0;
      player.missileFrameCount = 0;
      //added chapter 9
      player.safe = true;
   }

   function checkForExtraShip() {
      if (Math.floor(score/extraShipAtEach) > extraShipsEarned) {
         playerShips++
         extraShipsEarned++;
      }
   }

   function checkForEndOfLevel(){
      if (rocks.length==0) {
         switchGameState(GAME_STATE_NEW_LEVEL);
      }
   }

   function gameStatePlayerDie(){
      if (particles.length >0 || playerMissiles.length>0) {
         fillBackground();
         renderScoreBoard();
         updateRocks();
         updateSaucers();
         updateParticles();
         updateSaucerMissiles();
         updatePlayerMissiles();
         renderRocks();
         renderSaucers();
         renderParticles();
         renderSaucerMissiles();
         renderPlayerMissiles();
         frameRateCounter.countFrames();

      }else{
         playerShips--;
         if (playerShips<1) {
            switchGameState(GAME_STATE_GAME_OVER);
         }else{
            //resetPlayer();
            switchGameState(GAME_STATE_PLAYER_START);
         }
      }
   }

   function gameStateGameOver() {
      //ConsoleLog.log("Game Over State");
      if (gameOverStarted !=true){
         fillBackground();
         renderScoreBoard();
         setTextStyle();
         context.fillText  ("Game Over!", 160, 70);
         context.fillText  ("Press Space To Play", 130, 140);

         gameOverStarted = true;
      }else{
         //wait for space key click
         if (keyPressList[32]==true){
            ConsoleLog.log("space pressed");
            switchGameState(GAME_STATE_TITLE);
            gameOverStarted = false;

         }

      }
   }

   function fillBackground() {
      // draw background and text
      context.fillStyle = '#000000';
      context.fillRect(xMin, yMin, xMax, yMax);

   }

   function setTextStyle() {
      context.fillStyle = '#ffffff';
      context.font = '15px _sans';
      context.textBaseline = 'top';
   }

   function setTextStyleTitle() {
      context.fillStyle  = '#54ebeb';
      context.font = '20px _sans';
      context.textBaseline = 'top';
   }

   function setTextStyleCredits() {
      context.fillStyle = '#ffffff';
      context.font = '12px _sans';
      context.textBaseline = 'top';
   }

   function renderScoreBoard() {

      context.fillStyle = "#ffffff";
      context.fillText('Score ' + score, 10, 20);
      renderPlayerShip(200,16,270,.75)
      context.fillText('X ' + playerShips, 220, 20);

      context.fillText('FPS: ' + frameRateCounter.lastFrameCount, 300,20)

   }

   function checkKeys() {
      //check keys

      if (keyPressList[38]==true){
      //thrust
         var angleInRadians = player.rotation * Math.PI / 180;
         player.facingX = Math.cos(angleInRadians);
         player.facingY = Math.sin(angleInRadians);

         var movingXNew = player.movingX+player.thrustAcceleration*player.facingX;
         var movingYNew = player.movingY+player.thrustAcceleration*player.facingY;

         var currentVelocity = Math.sqrt ((movingXNew*movingXNew) +
         (movingXNew*movingXNew));

         if (currentVelocity < player.maxVelocity) {
            player.movingX = movingXNew;
            player.movingY = movingYNew;
         }
         player.thrust = true;

      }else{
         player.thrust = false;
      }

      if (keyPressList[37]==true) {
         //rotate counterclockwise
         player.rotation -= player.rotationalVelocity;
         if (player.rotation <0) {
            player.rotation = 350
         }

      }

      if (keyPressList[39]==true) {
         //rotate clockwise
         player.rotation += player.rotationalVelocity;
         if (player.rotation >350) {
            player.rotation = 10
         }
      }

      if (keyPressList[32]==true) {
            if (player.missileFrameCount>player.missileFrameDelay){
            playSound(SOUND_SHOOT,.5);
            firePlayerMissile();
            player.missileFrameCount = 0;

         }
      }
   }

   function update() {
      updatePlayer();
      updatePlayerMissiles();
      updateRocks();
      updateSaucers();
      updateSaucerMissiles();
      updateParticles();
   }

   function render() {
      fillBackground();
      renderScoreBoard();
      renderPlayerShip(player.x,player.y,player.rotation,1);
      renderPlayerMissiles();
      renderRocks();
      renderSaucers();
      renderSaucerMissiles();
      renderParticles();
   }

   function updatePlayer() {
      player.missileFrameCount++;

      player.x += player.movingX*frameRateCounter.step;
      player.y += player.movingY*frameRateCounter.step;

      if (player.x > xMax) {
         player.x =- player.width;
      }else if (player.x<-player.width){
         player.x = xMax;
      }

      if (player.y > yMax) {
         player.y =- player.height;
      }else if (player.y<-player.height){
         player.y = yMax;
      }
   }

   function updatePlayerMissiles() {
      var tempPlayerMissile = {};
      var playerMissileLength=playerMissiles.length-1;
      //ConsoleLog.log("update playerMissileLength=" + playerMissileLength);
      for (var playerMissileCtr=playerMissileLength;playerMissileCtr>=0; 
       playerMissileCtr--){
         //ConsoleLog.log("update player missile" + playerMissileCtr)
         tempPlayerMissile = playerMissiles[playerMissileCtr];
         tempPlayerMissile.x += tempPlayerMissile.dx*frameRateCounter.step;;
         tempPlayerMissile.y += tempPlayerMissile.dy*frameRateCounter.step;;
         if (tempPlayerMissile.x > xMax) {
            tempPlayerMissile.x =- tempPlayerMissile.width;
         }else if (tempPlayerMissile.x<-tempPlayerMissile.width){
            tempPlayerMissile.x = xMax;
         }

         if (tempPlayerMissile.y > yMax) {
            tempPlayerMissile.y =- tempPlayerMissile.height;
         }else if (tempPlayerMissile.y<-tempPlayerMissile.height){
            tempPlayerMissile.y = yMax;
         }

         tempPlayerMissile.lifeCtr++;
         if (tempPlayerMissile.lifeCtr > tempPlayerMissile.life){
            //ConsoleLog.log("removing player missile");
            playerMissiles.splice(playerMissileCtr,1)
            tempPlayerMissile = null;
         }
      }
   }

   function updateRocks(){

      var tempRock = {};
      var rocksLength = rocks.length-1;
      //ConsoleLog.log("update rocks length=" + rocksLength);
      for (var rockCtr=rocksLength;rockCtr>=0;rockCtr--){
         tempRock = rocks[rockCtr]
         tempRock.x += tempRock.dx*frameRateCounter.step;
         tempRock.y += tempRock.dy*frameRateCounter.step;

         tempRock.animationCount++;
         if (tempRock.animationCount > tempRock.animationDelay){
            tempRock.animationCount = 0;
            tempRock.rotation += tempRock.rotationInc;

            if (tempRock.rotation > 4){
               tempRock.rotation = 0;
            }else if (tempRock.rotation <0){
               tempRock.rotation = 4;
            }
         }

         if (tempRock.x > xMax) {
            tempRock.x = xMin-tempRock.width;
         }else if (tempRock.x<xMin-tempRock.width){
            tempRock.x = xMax;
         }

         if (tempRock.y > yMax) {
            tempRock.y = yMin-tempRock.width;
         }else if (tempRock.y<yMin-tempRock.width){
            tempRock.y = yMax;
         }

         //ConsoleLog.log("update rock "+ rockCtr)
      }
   }

   function updateSaucers() {
      //first check to see if we want to add a saucer

      if (saucers.length< levelSaucerMax){
         if (Math.floor(Math.random()*100)<=levelSaucerOccurrenceRate){
            //ConsoleLog.log("create saucer")
            var newSaucer = {};

            newSaucer.width = 30;
            newSaucer.height = 13;
            newSaucer.halfHeight = 6.5;
            newSaucer.halfWidth = 15;
            newSaucer.hitWidth = 30;
            newSaucer.hitHeight = 13;
            newSaucer.scoreValue = saucerScore;
            newSaucer.fireRate = levelSaucerFireRate;
            newSaucer.fireDelay = levelSaucerFireDelay;
            newSaucer.fireDelayCount = 0;
            newSaucer.missileSpeed = levelSaucerMissileSpeed;
            newSaucer.dy = (Math.random()*2);
            if (Math.floor(Math.random)*2==1){
               newSaucer.dy *= -1;
            }

            //choose betweeen left or right edge to start
            if (Math.floor(Math.random()*2)==1){
               //start on right and go left
               newSaucer.x = 450;
               newSaucer.dx = -1*levelSaucerSpeed;

            }else{
               //left to right
               newSaucer.x = -50;
               newSaucer.dx = levelSaucerSpeed;
            }

            newSaucer.missileSpeed = levelSaucerMissileSpeed;
            newSaucer.fireDelay = levelSaucerFireDelay;
            newSaucer.fireRate = levelSaucerFireRate;
            newSaucer.y = Math.floor(Math.random()*400);

            saucers.push(newSaucer);
         }

      }

      var tempSaucer = {};
      var saucerLength = saucers.length-1;
      //ConsoleLog.log("update rocks length=" + rocksLength);
      for (var saucerCtr=saucerLength;saucerCtr>=0;saucerCtr--){
         tempSaucer = saucers[saucerCtr];

         //should saucer fire
         tempSaucer.fireDelayCount++;
         if (Math.floor(Math.random()*100) <=tempSaucer.fireRate && 
          tempSaucer.fireDelayCount>tempSaucer.fireDelay ){
            playSound(SOUND_SAUCER_SHOOT,.5);
            fireSaucerMissile(tempSaucer)
            tempSaucer.fireDelayCount=0;
         }

         var remove = false;
         tempSaucer.x += tempSaucer.dx*frameRateCounter.step;
         tempSaucer.y += tempSaucer.dy*frameRateCounter.step;

         //remove saucers on left and right edges
         if (tempSaucer.dx > 0 && tempSaucer.x >xMax){
            remove = true;
         }else if (tempSaucer.dx <0 &&tempSaucer.x<xMin-tempSaucer.width){
            remove = true;
         }

         //bounce saucers off over vertical edges
         if (tempSaucer.y > yMax || tempSaucer.y<yMin-tempSaucer.width) {
            tempSaucer.dy *= -1
         }


         if (remove==true) {
            //remove the saucer
            ConsoleLog.log("saucer removed")
            saucers.splice(saucerCtr,1);
            tempSaucer = null;
         }

      }

   }

   function updateSaucerMissiles() {
      var tempSaucerMissile = {};
      var saucerMissileLength = saucerMissiles.length-1;
      for (var saucerMissileCtr = saucerMissileLength;saucerMissileCtr>=0;
       saucerMissileCtr--){
         //ConsoleLog.log("update player missile" + playerMissileCtr)
         tempSaucerMissile = saucerMissiles[saucerMissileCtr];
         tempSaucerMissile.x += tempSaucerMissile.dx*frameRateCounter.step;
         tempSaucerMissile.y += tempSaucerMissile.dy*frameRateCounter.step;
         if (tempSaucerMissile.x > xMax) {
            tempSaucerMissile.x =- tempSaucerMissile.width;
         }else if (tempSaucerMissile.x<-tempSaucerMissile.width){
            tempSaucerMissile.x = xMax;
         }

         if (tempSaucerMissile.y > yMax) {
            tempSaucerMissile.y =- tempSaucerMissile.height;
         }else if (tempSaucerMissile.y<-tempSaucerMissile.height){
            tempSaucerMissile.y = yMax;
         }

         tempSaucerMissile.lifeCtr++;
         if (tempSaucerMissile.lifeCtr > tempSaucerMissile.life){
            //remove
            saucerMissiles.splice(saucerMissileCtr,1)
            tempSaucerMissile = null;
         }
      }
   }

   function updateParticles() {

      var particleLength=particles.length-1;
      ConsoleLog.log("particle=" + particleLength)
      ConsoleLog.log("particlePool=" + particlePool.length)
      for (var particleCtr=particleLength;particleCtr>=0;particleCtr--){
         var remove = false;
         tempParticle = particles[particleCtr];
         tempParticle.x += tempParticle.dx*frameRateCounter.step;
         tempParticle.y += tempParticle.dy*frameRateCounter.step;

         tempParticle.lifeCtr++;
         
         if (tempParticle.lifeCtr > tempParticle.life){
            remove = true;

         } else if ((tempParticle.x > xMax) || (tempParticle.x<xMin)
           || (tempParticle.y > yMax) || (tempParticle.y<yMin)){
            remove=true;

         }

         if (remove) {
            particlePool.push(tempParticle)
            particles.splice(particleCtr,1)

         }

      }

   }

   function renderPlayerShip(x,y,rotation, scale) {
      //transformation
      context.save(); //save current state in stack
      context.globalAlpha = parseFloat(player.alpha);
      var angleInRadians = rotation * Math.PI / 180;
      var sourceX = Math.floor((player.rotation/10) % 10) * 32;
      var sourceY = Math.floor((player.rotation/10) /10) *32;
      if (player.thrust){
         context.drawImage(shipTiles2, sourceX, sourceY, 32,32, 
          player.x,player.y,32,32);
      }else{
         context.drawImage(shipTiles, sourceX, sourceY, 32,32,
          player.x,player.y,32,32);
      }

      //restore context
      context.restore(); //pop old state on to screen

      context.globalAlpha = 1;

   }

   function renderPlayerMissiles() {
      var tempPlayerMissile = {};
      var playerMissileLength=playerMissiles.length-1;
      //ConsoleLog.log("render playerMissileLength=" + playerMissileLength);
      for (var playerMissileCtr=playerMissileLength;playerMissileCtr>=0;
       playerMissileCtr--){
         //ConsoleLog.log("draw player missile " + playerMissileCtr)
         tempPlayerMissile = playerMissiles[playerMissileCtr];
         context.save(); //save current state in stack
         var sourceX=Math.floor(1 % 4) * tempPlayerMissile.width;
         var sourceY=Math.floor(1 / 4) * tempPlayerMissile.height;

         context.drawImage(particleTiles, sourceX, sourceY,
          tempPlayerMissile.width,tempPlayerMissile.height, 
          tempPlayerMissile.x,tempPlayerMissile.y, 
          tempPlayerMissile.width,tempPlayerMissile.height);

         context.restore(); //pop old state on to screen
      }
   }

   function renderRocks() {
      var tempRock = {};
      var rocksLength = rocks.length-1;
      for (var rockCtr = rocksLength;rockCtr>=0;rockCtr--){
         context.save(); //save current state in stack
         tempRock = rocks[rockCtr];
         var sourceX = Math.floor((tempRock.rotation) % 5) * tempRock.width;
         var sourceY = Math.floor((tempRock.rotation) /5) *tempRock.height;

         switch(tempRock.scale){
            case 1:
               context.drawImage(largeRockTiles, sourceX, sourceY, 
                tempRock.width,tempRock.height,tempRock.x,tempRock.y,
                tempRock.width,tempRock.height);
               break;
            case 2:
               context.drawImage(mediumRockTiles, sourceX,
                sourceY,tempRock.width,tempRock.height,tempRock.x,tempRock.y,
                tempRock.width,tempRock.height);
               break;
            case 3:
               context.drawImage(smallRockTiles, sourceX,
                sourceY,tempRock.width,tempRock.height,tempRock.x,tempRock.y,
                tempRock.width,tempRock.height);
               break;

         }
         context.restore(); //pop old state on to screen

      }
   }

   function renderSaucers() {
      var tempSaucer = {};
      var saucerLength = saucers.length-1;
      for (var saucerCtr = saucerLength;saucerCtr>=0;saucerCtr--){
         //ConsoleLog.log("saucer: " + saucerCtr);
         tempSaucer = saucers[saucerCtr];

         context.save(); //save current state in stack
         var sourceX = 0;
         var sourceY = 0;
         context.drawImage(saucerTiles, sourceX, sourceY, 30,15, 
          tempSaucer.x,tempSaucer.y,30,15);
         context.restore(); //pop old state on to screen
      }
   }
   function renderSaucerMissiles() {
      var tempSaucerMissile = {};
      var saucerMissileLength = saucerMissiles.length-1;
      //ConsoleLog.log("saucerMissiles= " + saucerMissiles.length)
      for (var saucerMissileCtr=saucerMissileLength;saucerMissileCtr>=0;
       saucerMissileCtr--){
         //ConsoleLog.log("draw player missile " + playerMissileCtr)
         tempSaucerMissile = saucerMissiles[saucerMissileCtr];
         context.save(); //save current state in stack
         var sourceX = Math.floor(0 % 4) * tempSaucerMissile.width;
         var sourceY = Math.floor(0 / 4) * tempSaucerMissile.height;

         context.drawImage(particleTiles, sourceX, sourceY, 
          tempSaucerMissile.width,tempSaucerMissile.height,
          tempSaucerMissile.x,tempSaucerMissile.y,tempSaucerMissile.width,
          tempSaucerMissile.height);

         context.restore(); //pop old state on to screen

      }
   }

   function renderParticles() {

      var tempParticle = {};
      var particleLength = particles.length-1;
      for (var particleCtr=particleLength;particleCtr>=0;particleCtr--){
         tempParticle = particles[particleCtr];
         context.save(); //save current state in stack

         var tile;

         //console.log("part type=" + tempParticle.type)
         switch(tempParticle.type){
            case 0: // saucer
               tile = 0;
               break;
            case 1: //large rock
               tile = 2
               break;
            case 2: //medium rock
               tile = 3;
               break;
            case 3: //small rock
               tile = 0;
               break;
            case 4: //player
               tile = 1;
               break;

         }

         var sourceX = Math.floor(tile % 4) * tempParticle.width;
         var sourceY = Math.floor(tile / 4) * tempParticle.height;

         context.drawImage(particleTiles, sourceX, sourceY, 
          tempParticle.width,tempParticle.height,tempParticle.x,
          tempParticle.y,tempParticle.width,tempParticle.height);

         context.restore(); //pop old state on to screen

      }
   }
   
   function checkCollisions() {

      //loop through rocks then missiles.
      //There will always be rocks and a ship,
      //but there will not always be missiles.
      var tempRock = {};
      var rocksLength = rocks.length-1;
      var tempPlayerMissile = {};
      var playerMissileLength = playerMissiles.length-1;
      var saucerLength = saucers.length-1;
      var tempSaucer = {};
      var saucerMissileLength = saucerMissiles.length-1;

      rocks: for (var rockCtr=rocksLength;rockCtr>=0;rockCtr--){
         tempRock = rocks[rockCtr];

         missiles:for (var playerMissileCtr=playerMissileLength;
          playerMissileCtr>=0;playerMissileCtr--){
            tempPlayerMissile = playerMissiles[playerMissileCtr];

            if (boundingBoxCollide(tempRock,tempPlayerMissile)){
                  //ConsoleLog.log("hit rock");
                  createExplode(tempRock.x+tempRock.halfWidth,
                   tempRock.y+tempRock.halfHeight,10,tempRock.scale);
                  if (tempRock.scale<3) {
                     splitRock(tempRock.scale+1, tempRock.x, tempRock.y);
                  }
                  addToScore(tempRock.scoreValue);
                  playerMissiles.splice(playerMissileCtr,1);
                  tempPlayerMissile = null;


                  rocks.splice(rockCtr,1);
                  tempRock = null;

                  break rocks;
                  break missiles;
               }
            }

         saucers:for (var saucerCtr=saucerLength;saucerCtr>=0;saucerCtr--){
            tempSaucer = saucers[saucerCtr];

            if (boundingBoxCollide(tempRock,tempSaucer)){
                  //ConsoleLog.log("hit rock");
                  createExplode(tempSaucer.x+tempSaucer.halfWidth,
                   tempSaucer.y+tempSaucer.halfHeight,10,0);
                  createExplode(tempRock.x+tempRock.halfWidth,
                   tempRock.y+tempRock.halfHeight,10,tempRock.scale);

                  if (tempRock.scale<3) {
                     splitRock(tempRock.scale+1, tempRock.x, tempRock.y);
                  }

                  saucers.splice(saucerCtr,1);
                  tempSaucer = null;

                  rocks.splice(rockCtr,1);
                  tempRock = null;

                  break rocks;
                  break saucers;
               }
            }
         //saucer missiles against rocks
         //this is done here so we don't have to loop through
         //rocks again as it would probably
         //be the biggest array
         saucerMissiles:for (var saucerMissileCtr=saucerMissileLength;
                          saucerMissileCtr>=0;saucerMissileCtr--){

            tempSaucerMissile = saucerMissiles[saucerMissileCtr];
            if (boundingBoxCollide(tempRock,tempSaucerMissile)){
                  //ConsoleLog.log("hit rock");

                  createExplode(tempRock.x+tempRock.halfWidth,
                   tempRock.y+tempRock.halfHeight,10,tempRock.scale);
                  if (tempRock.scale<3) {
                     splitRock(tempRock.scale+1, tempRock.x, tempRock.y);
                  }

                  saucerMissiles.splice(saucerCtr,1);
                  tempSaucerMissile = null;

                  rocks.splice(rockCtr,1);
                  tempRock = null;

                  break rocks;
                  break saucerMissiles;
               }
            }

         //check player against rocks

         if (boundingBoxCollide(tempRock,player) && player.safe==false){
            //ConsoleLog.log("hit player");
            createExplode(tempRock.x+tempRock.halfWidth,
             tempRock.halfHeight,10,tempRock.scale);
            addToScore(tempRock.scoreValue);
            if (tempRock.scale<3) {
               splitRock(tempRock.scale+1, tempRock.x, tempRock.y);
            }
            rocks.splice(rockCtr,1);
            tempRock=null;

            playerDie();
         }

      }

      //now check player against saucers and then saucers against player missiles
      //and finally player against saucer missiles
      playerMissileLength = playerMissiles.length-1;
      saucerLength = saucers.length-1;
      saucers:for (var saucerCtr=saucerLength;saucerCtr>=0;saucerCtr--){
         tempSaucer = saucers[saucerCtr];

         missiles:for (var playerMissileCtr=playerMissileLength;
          playerMissileCtr>=0;playerMissileCtr--){

         tempPlayerMissile = playerMissiles[playerMissileCtr];

            if (boundingBoxCollide(tempSaucer,tempPlayerMissile)){
               //ConsoleLog.log("hit rock");
               createExplode(tempSaucer.x+tempSaucer.halfWidth,
                tempSaucer.y+tempSaucer.halfHeight,10,0);
               addToScore(tempSaucer.scoreValue);

               playerMissiles.splice(playerMissileCtr,1);
               tempPlayerMissile = null;

               saucers.splice(saucerCtr,1);
               tempSaucer = null;

               break saucers;
               break missiles;
            }
         }

         //player against saucers
         if (boundingBoxCollide(tempSaucer,player) & player.safe==false){
            ConsoleLog.log("hit player");
            createExplode(tempSaucer.x+16,tempSaucer.y+16,10,tempRock.scale);
            addToScore(tempSaucer.scoreValue);

            saucers.splice(rockCtr,1);
            tempSaucer = null;

            playerDie();
         }
      }

      //saucerMissiles against player
      saucerMissileLength = saucerMissiles.length-1;

      saucerMissiles:for (var saucerMissileCtr=saucerMissileLength;
                      saucerMissileCtr>=0;saucerMissileCtr--){

         tempSaucerMissile = saucerMissiles[saucerMissileCtr];

         if (boundingBoxCollide(player,tempSaucerMissile) & player.safe==false){
            ConsoleLog.log("saucer missile hit player");

            playerDie();
            saucerMissiles.splice(saucerCtr,1);
            tempSaucerMissile = null;

            break saucerMissiles;
         }
      }

   }

   function firePlayerMissile(){

      //ConsoleLog.log("fire playerMissile");
      var newPlayerMissile = {};
      newPlayerMissile.dx = 5*Math.cos(Math.PI*(player.rotation)/180);
      newPlayerMissile.dy = 5*Math.sin(Math.PI*(player.rotation)/180);
      newPlayerMissile.x = player.x+player.halfWidth;
      newPlayerMissile.y = player.y+player.halfHeight;
      newPlayerMissile.life = 60;
      newPlayerMissile.lifeCtr = 0;
      newPlayerMissile.width = 2;
      newPlayerMissile.height = 2;
      newPlayerMissile.hitHeight = 2;
      newPlayerMissile.hitWidth = 2;
      playerMissiles.push(newPlayerMissile);
   }

   function fireSaucerMissile(saucer) {
      var newSaucerMissile = {};
      newSaucerMissile.x = saucer.x+.5*saucer.width;
      newSaucerMissile.y = saucer.y+.5*saucer.height;
      newSaucerMissile.width = 2;
      newSaucerMissile.height = 2;
      newSaucerMissile.hitHeight = 2;
      newSaucerMissile.hitWidth = 2;
      newSaucerMissile.speed = saucer.missileSpeed;

      //ConsoleLog.log("saucer fire");
      //fire at player from small saucer
      var diffx = player.x-saucer.x;
      var diffy = player.y-saucer.y;
      var radians = Math.atan2(diffy, diffx);
      var degrees = 360 * radians / (2 * Math.PI);
      newSaucerMissile.dx = saucer.missileSpeed*Math.cos(Math.PI*(degrees)/180);
      newSaucerMissile.dy = saucer.missileSpeed*Math.sin(Math.PI*(degrees)/180);
      newSaucerMissile.life = 160;
      newSaucerMissile.lifeCtr = 0;
      saucerMissiles.push(newSaucerMissile);
   }

   function playerDie() {

      ConsoleLog.log("player die");
      createExplode(player.x+player.halfWidth, player.y+player.halfWidth,50,4);
      resetPlayer();
      switchGameState(GAME_STATE_PLAYER_DIE);

   }

   function createExplode(x,y,num,type) {

      playSound(SOUND_EXPLODE,.5);
      for (var partCtr=0;partCtr<num;partCtr++){
         if (particlePool.length > 0){

              newParticle = particlePool.pop();
         newParticle.dx = Math.random()*3;
            if (Math.random()<.5){
               newParticle.dx *= -1;
            }
         newParticle.dy = Math.random()*3;
         if (Math.random()<.5){
            newParticle.dy *= -1;
         }

         newParticle.life = Math.floor(Math.random()*30+30);
         newParticle.lifeCtr = 0;
         newParticle.x = x;
         newParticle.width = 2;
         newParticle.height = 2;
         newParticle.y = y;
         newParticle.type = type;
         //ConsoleLog.log("newParticle.life=" + newParticle.life);
         particles.push(newParticle);
         }

      }

   }

   function boundingBoxCollide(object1, object2) {

      var left1 = object1.x;
      var left2 = object2.x;
      var right1 = object1.x + object1.hitWidth;
      var right2 = object2.x + object2.hitWidth;
      var top1 = object1.y;
      var top2 = object2.y;
      var bottom1 = object1.y + object1.hitHeight;
      var bottom2 = object2.y + object2.hitHeight;

      if (bottom1 < top2) return(false);
      if (top1 > bottom2) return(false);

      if (right1 < left2) return(false);
      if (left1 > right2) return(false);

      return(true);

   };

   function splitRock(scale,x,y){
      for (var newRockctr=0;newRockctr<2;newRockctr++){
         var newRock = {};
         //ConsoleLog.log("split rock");

         if (scale==2){
            newRock.scoreValue = medRockScore;
            newRock.width = 32;
            newRock.height = 32;
            newRock.halfWidth = 16;
            newRock.halfHeight = 16;
            newRock.hitWidth = 24;
            newRock.hitHeight = 24;

         }else {
            newRock.scoreValue = smlRockScore;
            newRock.width = 24;
            newRock.height = 24;
            newRock.halfWidth = 12;
            newRock.halfHeight = 12;
            newRock.hitWidth = 16;
            newRock.hitHeight = 16;
         }

         newRock.scale = scale;
         newRock.x = x;
         newRock.y = y;
         newRock.dx = Math.random()*3;
         if (Math.random()<.5){
            newRock.dx *= -1;
         }
         newRock.dy = Math.random()*3;
         if (Math.random()<.5){
            newRock.dy *= -1;
         }
         if (Math.random()<.5){
               newRock.rotationInc = -1;
         }else{
            newRock.rotationInc = 1;
         }

         newRock.animationDelay = Math.floor(Math.random()*3+1);
         newRock.animationCount = 0;

         newRock.rotation = 0;
         ConsoleLog.log("new rock scale"+(newRock.scale));
         rocks.push(newRock);

      }

   }

   function addToScore(value){
      score += value;
   }

   document.onkeydown = function(e){

      e = e?e:window.event;
      //ConsoleLog.log(e.keyCode + "down");
      keyPressList[e.keyCode] = true;
   }

   document.onkeyup = function(e){
   //document.body.onkeyup = function(e){
      e = e?e:window.event;
      //ConsoleLog.log(e.keyCode + "up");
      keyPressList[e.keyCode] =  false;
   };



   //*** application start

    switchGameState(GAME_STATE_INIT);
    const FRAME_RATE = 40;
    frameRateCounter = new FrameRateCounter(FRAME_RATE);
    //**** application loop
    var intervalTime = 1000/FRAME_RATE;
    setInterval(runGame, intervalTime );

}

//***** object prototypes *****

//*** consoleLog util object
//create constructor
function ConsoleLog(){

}

//create function that will be added to the class
console_log = function(message) {
   if(typeof(console) !== 'undefined' && console != null) {
      console.log(message);
   }
}
//add class/static function to class by assignment
ConsoleLog.log = console_log;

//*** end console log object

//***  new FrameRateCounter object prototype

function FrameRateCounter(fps) {
   if (fps == undefined){

      this.fps = 40
   }else{
      this.fps = fps
   }

   this.lastFrameCount = 0;
   var dateTemp = new Date();
   this.frameLast = dateTemp.getTime();
   delete dateTemp;
   this.frameCtr = 0;
   this.lastTime = dateTemp.getTime();

   this.step = 1;

}

FrameRateCounter.prototype.countFrames = function() {

   var dateTemp = new Date();
   var timeDifference = dateTemp.getTime()-this.lastTime;
   this.step = (timeDifference/1000)*this.fps;
   this.lastTime = dateTemp.getTime();
   this.frameCtr++;

   if (dateTemp.getTime() >=this.frameLast+1000) {
      ConsoleLog.log("frame event");
      this.lastFrameCount = this.frameCtr;
      this.frameCtr = 0;
      this.frameLast = dateTemp.getTime();
   }
   delete dateTemp;

}
</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">

<canvas id="canvas" width="400" height="400">
 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.