Chapter 9. Canvas Games: Part II

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 focus on using bitmaps and tile sheets for our game graphics, and we add sound, using techniques introduced in Chapter 7.

Along the way, we update the FrameRateCounter from Chapter 8 by adding in a step timer. We 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 create another small turn-based strategy game using bitmaps. This game is roughly based on the classic computer game, Daleks.

Geo Blaster Extended

We 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 is the same, but adding bitmaps to replace paths enables us to optimize the game for rendering. Optimized rendering is very important when you are targeting limited-processor devices, such as mobile phones. We 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 apply the concepts from Chapter 4 to optimizing the rendering of the Geo Blaster Basic game. We do this by prerendering all our game graphics and transformations as bitmaps. We 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 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 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 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 need to be only five tiles each. Because 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 are used for the explosions and missiles the player and the saucer fire.

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 is used for the small rock and saucer explosions. The second tile is light blue, and it depicts the player’s missiles and the player explosion. The third tile is reddish pink (salmon, if you will), and it illustrates the large rock explosions. The final, purple tile is used for the medium rock explosions.

Now that we have our tiles in place, let’s look at the methods we’ll 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, because it is 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 returns the remainder of the division calculation. Following is the actual code (with variables replaced with literals) we 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 use for this calculation:

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

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

To copy this to the canvas, we 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 use to render the player ship. It replaces 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;

 }

You can find the entire source code for Geo Blaster Extended (Example A-2) in Appendix A.

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 the Chapter 8 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 drew only 36 tiles for the player ship, so we are using the value 10 for the rotational velocity. We certainly could use 72 or even 360 frames for the player ship rotation tiles. This is limited only 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 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 pose 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 also have new hitWidth and hitHeight attributes. We 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 take a quick look at how we 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 are all 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 render them as though they do. This allows 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 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 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 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 could 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 represents the number of frames between each tile change for a given rock. The animationCount variable restarts at 0 after each tile frame change and increases by 1 on each subsequent frame tick. When animationCount is greater than animationDelay, the rock.rotation value is increased (clockwise) or decreased (counterclockwise). Here is the new code 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;
      }
   }

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 is also rendered using a bitmap tile sheet, and its code is 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 can use a different-colored particle for each type of explosion. This way, the rocks, saucers, and player ship can all have uniquely colored explosions.

The new createExplode() function handles 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 add a new type attribute to them. When an explosion is triggered in the checkCollisions() function, the call to createExplode()now includes 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 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 use 0, and for the player, we 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 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 pass a number literal into the createExplode() function. In the saucer’s case, we pass in a 0. 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().

After we discuss adding sound and a particle pool to this game, we present the entire set of code (Example A-2), replacing the Geo Blaster Basic code. There is 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 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.

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 in the Opera browser.

The sounds for our game

We add 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 formats: .wav, .ogg, and .mp3.

Adding sound instances and management variables to the game

In the variable definition section of our game code, we create variables to work with the sound manager code from Chapter 7. We 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 assign a constant string to each, and to play the sound, we just 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 later:

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

Finally, we need an audioType variable, which we 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 the game assets while our state machine waited in an idle state. We add this code to our game in a gameStateInit()function.

Sound does not work the same in all browsers. In this example game, we are preloading all the images and sounds. For Internet Explorer 9 and 10, this preloading sometimes does not work. You can change the number of items to preload from 16 to 7 to test the game without sound in Internet Explorer browsers that have trouble with the preloading.

function gameStateInit() {
   loadCount = 0;
   itemsToLoad  = 16; // change to 7 for IE

   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 instances of each sound, even though they share the same sound file (or files). In this function, we also load our tile sheets. The application scope itemsToLoad variable checks against the application scope loadCount variable in the load event callback itemLoaded() function, which is shared by all assets to be loaded. This makes 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 and then add the sounds to our sound pool. Finally, we call the switchGameState() to send the game to the title screen.

Playing sounds

Sounds play using the playSound() function from Chapter 7. We will not reprint that function here, but it is in Example A-2, where we give the entire set of code for the game. We 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 is 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 because 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 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 need to add four application scope variables to our game to use pooling for our game particle:

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

The particlePool array holds the list of particle object instances that are waiting to be used. When createExplode() needs to use a particle, it first sees whether any are available in this array. If one is available, it is 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 sets the properties of the newParticle and then pushes it to the end of the existing particles array.

When a particle’s life has been exhausted, the updateParticles() function splices the particle from the particles array and pushes 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 is used in a new function called createObjectPools(). We 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 sees whether particlePool.length is greater than 0. If a particle is available, it is added to the particles array after its attributes are set. If no particle is available, none is used.

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 whether the particle’s life has been exhausted. If it has, the function places the particle back in the pool. Here is the code we add to updateParticles() to replenish the pool:

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

}

Adding a Step Timer

In Chapter 8, we created a simple FrameRateCounter object prototype that displayed the current frame rate as the game was running. We now extend the functionality of this counter to add a step timer. The step timer uses the time difference calculated between frames to create a step factor. This step factor is 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 and relatively consistent game play on browsers and systems that cannot maintain the frame rate needed to play the game effectively.

We update the constructor function for FrameRateCounter to accept in a new single parameter called fps. This value represents the frames per second 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 is used.

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

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

The lastTime variable contains 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 occurs 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 deficit in time between frame ticks, the step value is 1. If the current frame tick took longer than a single frame to finish, the value is greater than 1 (a deficit). If the current frame took less time than a single frame, the step value is 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 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 is multiplied by 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 updates 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 the major changes to turn Geo Blaster Basic into Geo Blaster Extended. Take a look at Example A-2, which has the entire code for the final game.

Creating a Dynamic Tile Sheet at Runtime

In Chapter 4, we briefly examined two principles we can use to help eliminate the need to precreate rotations of objects in tile sheets. Creating these types of tile sheets can be cumbersome and use up valuable time that’s better spent elsewhere in the project.

The idea is to take a single image of a game object (e.g., the first tile in the medium rock tile sheet), create a dynamic tile sheet at runtime, and store it in an array rather than using the prerendered image rotation tiles.

To accomplish this, we need to use a second canvas as well as the getImageData() and putImageData() Canvas functions. Recall from Chapter 4 that getImageData()throws a security error if the HTML page using it is not on a web server.

Currently, only the Safari browser doesn’t throw this error if the file is used on a local filesystem, so we have separated this functionality from the Geo Blaster Extended game and simply demonstrate how it could be used instead of replacing all the tile sheets in the game with this type of prerendering.

We start by creating two <canvas> elements on our HTML page:

<body>
<div>
<canvas id="canvas" width="256" height="256" style="position: absolute; top:
50px; left: 50px;">
 Your browser does not support HTML5 Canvas.
</canvas>

<canvas id="canvas2" width="32" height="32"  style="position: absolute; top:
 256px; left: 50px;">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>

The first <canvas>, named canvas, represents our hypothetical game screen, which displays the precached dynamic tile sheet animation.

The second <canvas>, named canvas2, is used as a drawing surface to create the dynamic tile frames for our tile sheet.

We need to separate context instances in the JavaScript, one for each <canvas>:

var theCanvas = document.getElementById("canvas");
var context = theCanvas.getContext("2d");
var theCanvas2 = document.getElementById("canvas2");
var context2= theCanvas2.getContext("2d");

We use the mediumrocks.png file (Figure 9-9) from the Geo Blaster Extended game as our source for the dynamic tile sheet. Don’t let this confuse you. We will not use all five tiles on this tile sheet—only the first tile.

The mediumrocks.png tile sheet
Figure 9-9. The mediumrocks.png tile sheet

In Geo Blaster Extended, we used all five tiles to create a simulated rotation animation. Here, we use only the first tile. We draw this first tile, rotate it on theCanvas2 by 10 degrees, and then copy the current imageData pixels from this canvas to an array of imageData instances called rotationImageArray.

We then repeat this process by rotating theCanvas2 by 10 more degrees and in a loop until we have 36 individual frames of imageData representing the rotation animation for our medium rock in an array:

var rotationImageArray = [];
var animationFrame = 0;
var tileSheet = new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src = "mediumrocks.png";

The rotationImageArray variable holds the generated imageData instances, which we create by using a rotation transformation on theCanvas2.

The animationFrame is used when redisplaying the rotation animation frames in rotationImageArray back to the first theCanvas to demo the animation.

When the tileSheet is loaded, the eventSheetLoaded() function is called, which in turn calls the startup() function. The startup() function uses a loop to create the 36 frames of animation:

function startUp(){

   for (var ctr=0;ctr<360;ctr+=10){
     context2.fillStyle = "#ffffff";
     context2.fillRect(0,0,32,32);
     context2.save();
     context2.setTransform(1,0,0,1,0,0)
     var angleInRadians = ctr * Math.PI / 180;
     context2.translate(16, 16);
     context2.rotate(angleInRadians);
     context2.drawImage(tileSheet, 0, 0,32,32,-16,-16,32,32);
     context2.restore();
     var imagedata = context2.getImageData(0, 0, 32, 32)
     rotationImageArray.push(imagedata);
   }
   setInterval(drawScreen, 100 );
}

This loop first clears theCanvas2 with a white color and then saves it to the stack. We then translate to the center of our object and rotate the canvas by the current ctr value (an increment of 10). Next, we draw the first tile from mediumrocks.png and save the result in a new local imageData instance, using the getImageData() function.

This is where the security error will be thrown if the domain of the image and the domain of the HTML file are not the same. On a local machine (not running on a local web server, but from the filesystem), this error will be thrown on all browsers but Safari (currently).

Finally, the new imageData is pushed into the rotationImageArray. When the loop is complete, we set up an interval to run and call the drawScreen() function every 100 milliseconds.

To display the animation on the first canvas, we use this timer loop interval and call putImageData() to draw each frame in succession, creating the simulation of animation. As with the tile sheet, we didn’t have to use 36 frames of animation; we could use just five. Naturally, the animation is much smoother with more frames, but this process shows how easy it is to create simple transformation animations on the fly rather than precreating them in image files:

function drawScreen() {

      //context.fillStyle = "#ffffff";
      //context.fillRect(50,50,32,32);
      context.putImageData(rotationImageArray[animationFrame],50,50);
      animationFrame++;
      if (animationFrame ==rotationImageArray.length){
         animationFrame=0;
      }
}

Example 9-1 shows the entire code.

Example 9-1. A dynamic tile sheet example
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH9EX2: Canvas Copy</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 canvasApp(){

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

      var theCanvas2 = document.getElementById("canvas2");
      var context2= theCanvas2.getContext("2d");
   }
   var rotationImageArray = [];
   var tileSheet = new Image();
   var animationFrame = 0;
   tileSheet.addEventListener('load', eventSheetLoaded , false);
   tileSheet.src = "mediumrocks.png";
   function eventSheetLoaded() {
      startUp();
   }

   function startUp(){
      //context.drawImage(tileSheet, 0, 0);
      //context2.drawImage(theCanvas, 0, 0,32,32,0,0,32,32);

      for (var ctr=0;ctr<360;ctr+=10){
         context2.fillStyle="#ffffff";
         context2.fillRect(0,0,32,32);

         context2.save();
         context2.setTransform(1,0,0,1,0,0)
         var angleInRadians = ctr * Math.PI / 180;
         context2.translate(16, 16);
         context2.rotate(angleInRadians);
         context2.drawImage(tileSheet, 0, 0,32,32,-16,-16,32,32);
         context2.restore();

         var imagedata = context2.getImageData(0, 0, 32, 32);

         rotationImageArray.push(imagedata);
      }
      setInterval(drawScreen, 100 );
   }

   function drawScreen() {
      //context.fillStyle="#ffffff";
      //context.fillRect(50,50,32,32);
      context.putImageData(rotationImageArray[animationFrame],50,50);
      animationFrame++;
      if (animationFrame ==rotationImageArray.length){
         animationFrame = 0;
      }
   }

}

</script>
</head>
<body>
<div>
<canvas id="canvas" width="256" height="256" style="position: absolute; top:
 50px; left: 50px;">
 Your browser does not support the HTML 5 Canvas.
</canvas>

<canvas id="canvas2" width="32" height="32" style="position: absolute; top:
 256px; left: 50px;">
 Your browser does not support HTML5 Canvas.
</canvas>

</div>
</body>
</html>

In the rest of the chapter, we look at creating a simple tile-based game using some of the techniques first discussed in Chapter 4.

A Simple Tile-Based Game

Let’s move from Asteroids to another classic game genre, the tile-based maze-chase game. When you’re discussing early tile-based games, undoubtedly Pac-Man enters the conversation. Pac-Man was one of the first commercially successful tile-based games, although it certainly was not the first of its kind. The maze-chase genre was actually well covered by budding game developers before microcomputers were even thought possible. Many minicomputer and mainframe tile-based games, such as Daleks, were crafted in the ’60s and ’70s. In this section, we create a simple turn-based maze-chase game. Our game, Micro Tank Maze, is based loosely on Daleks, but we use the tank sprites from Chapter 4. Figure 9-10 is a screenshot from the finished game.

Micro Tank Maze in action
Figure 9-10. Micro Tank Maze in action

Micro Tank Maze Description

Micro Tank Maze is a simple turn-based strategy game played on a 15×15 tile-based grid. At the beginning of each game, the player (the green tank), 20 enemy tanks (the blue tanks), 25 wall tiles, and a single goal tile (the phoenix) are randomly placed on the grid. The rest of the grid is simply road tiles on which the tanks move. The player must get to the goal object without running into any walls or any of the enemy tanks. On each turn, the player and all enemy tanks move a single space (tile) on the grid. Neither the player nor the enemy tanks can move off the grid edges. If the player runs into a wall tile or an enemy tank, his game is over. If an enemy tank runs into a wall or another tank, it is destroyed and removed from the game board. If an enemy tank runs into the player tank, it and the player are destroyed. If the player hits the goal tile without an enemy tank also hitting the tile on the same turn, the player wins.

Game progression

Each time the player collects the goal object and wins the game, the next game starts with one more enemy tank (up to 50 enemy tanks). The ultimate goal of the game is to see how many times you (the player) can win before your tank is finally destroyed. The game keeps a session-based high score, and even if you lose, you always start from the last completed level.

This is a simple game, and much more can be added to it to enhance the gaming experience. In this chapter, though, we want to cover the basics of creating a tile-based game on HTML5 Canvas. By combining what you have learned throughout this book, you should have enough skill and knowledge to extend this simple contest into a much more robust game-play experience.

Game strategy

The player must try to reach the goal while avoiding the enemy tanks. The enemy follows or chases the player to a fault. Most of the time (75%), each enemy tank stupidly follows the player, even if that means moving into a wall and destroying itself. The player then has the advantage of intelligence to compensate for the large number of tanks the enemy employs. The other 25% of the time, an enemy tank randomly chooses a direction to move in.

Now, let’s get into the game by looking at the tile sheet we will be using.

The Tile Sheet for Our Game

Make sure you’ve read Chapter 4 and the Chapter 8 section, “A Basic Game Framework”, before moving on. Even though Micro Tank Maze is a relatively simple game, it is still quite a few lines of code. We hit the major points, but we don’t have space to discuss every detail.

The tile sheet (tanks_sheet.png) we use looks very familiar if you’ve read Chapter 4. Figure 9-11 shows tanks_sheet.png.

The Micro Tank Maze tile sheet
Figure 9-11. The Micro Tank Maze tile sheet

We are using only a very small portion of these tiles for Micro Tank Maze.

Road tile

This is the tile on which the player and the enemy tanks can move. Tile 0, the road tile, is in the top-left corner.

Wall tile

The wall tile causes any tank moving on it to be destroyed. Tile 30, the second-to-last tile on the sheet, is the wall tile.

Goal tile

This is the tile the player must reach to win the game. It is the last tile in the second-to-last row (the phoenix).

Player tiles

The player is made up of the eight green tank tiles. Each tile simulates the tank treads moving from tile to tile.

Enemy tiles

The enemy is made up of the eight blue tank tiles. These tiles animate the tank treads as the tank moves from tile to tile.

Our game code stores the tile IDs needed for each of these game objects in application scope variables:

var playerTiles = [1,2,3,4,5,6,7,8];
var enemyTiles = [9,10,11,12,13,14,15,16];
var roadTile = 0;
var wallTile = 30;
var goalTile = 23;
var explodeTiles = [17,18,19,18,17];

The tile sheet is loaded into an application scope Image instance and given the name tileSheet:

var tileSheet;

In the application’s initialization state, we load and assign the Image instance:

tileSheet = new Image();
tileSheet.src = "tanks_sheet.png";

Next, we examine the setup of the game playfield.

The Playfield

The game playfield is a 15×15 grid of 32×32 tiles. This is a total of 225 tiles with a width and height of 480 pixels each. Every time we start a new game, all the objects are placed randomly on the grid. The playField[] array holds 15 row arrays each with 15 columns. This gives us 225 tiles that can be easily accessed with the simple playField[row][col] syntax.

We first place a road tile on each of the 225 playField array locations. We then randomly place all the wall tiles. (These actually replace some of the road tiles at locations in the playField array.)

Next, we randomly place all the enemy tank tiles. Unlike the wall tiles, the tank tiles do not replace road tiles in the playField array. Instead, they are placed in an array of their own called enemy. To ensure that neither the player nor the goal object occupies the same tile space as the enemy tanks, we create another array, items.

The items array is also a 15×15 two-dimensional array of rows and columns, which can be considered the second layer of playfield data. Unlike the playField array, it is used only to make sure no two objects (player, enemy, or goal) occupy the same space while building the playfield. We must do this because the player and enemy objects are not added to the playField array.

After we have placed the enemy, we randomly place the player at a spot that is not currently occupied by an enemy or a wall. Finally, we place the goal tile in a spot not taken by the player, a wall, or an enemy tank.

The code for this is in the createPlayField() function. If you would like to review it now, go to the “Micro Tank Maze Complete Game Code” section (Example 9-2).

All the data about the playField is stored in application scope variables:

//playfield
var playField = [];
var items = [];
var xMin = 0;
var xMax = 480;
var yMin = 0;
var yMax = 480;

To create the playField, the game code needs to know the maximum number of each type of tile. These are also application scope variables:

var wallMax = 25;
var playerMax = 1;
var enemyMax = 20;
var goalMax = 1;

The Player

The player and all its current attributes are contained in the player object. Even a game as simple as Micro Tank Maze requires quite a few attributes. Here is a list and description of each:

player.row

The current row on the 15×15 playField grid where the player resides.

player.col

The current column on the 15×15 playField grid where the player resides.

player.nextRow

The row the player moves to next, after a successful key press in that direction.

player.nextCol

The column the player moves to next, after a successful key press in that direction.

player.currentTile

The id of the current tile that displays the player from the playerTiles array.

player.rotation

The player starts pointed up, so this is the 0 rotation. When the player moves in one of the four basic directions, this rotation changes and moves the player in the direction it is facing.

player.speed

The number of pixels the player object moves on each frame tick.

player.destinationX

The final x location for the 32×32 player object while it is moving to a new tile. It represents the top-left corner x location for this new location. During the player movement and animation phase of the game, this value determines when the player has arrived at its new x-axis location.

player.destinationY

The final y location for the 32×32 player object while it is moving to a new tile. It represents the top-left corner y location for this new location. During the player movement and animation phase of the game, this value determines when the player has arrived at its new y-axis location.

player.x

The current x location of the top-left corner of the 32×32 player object.

player.y

The current y location of the top-left corner of the 32×32 player object.

player.dx

The player’s change in x direction on each frame tick while it is animating. This will be −1, 0, or 1, depending on the direction in which the player is moving.

player.dy

The player’s change in y direction on each frame tick while it is animating. This will be −1, 0, or 1, depending on the direction in which the player is moving.

player.hit

Set to true when the player moves to a new square that is occupied by an enemy tank or a wall.

player.dead

When player.hit is true, it is replaced on the playField by an explosion sprite. With dead set to true, it is not rendered to the game screen.

player.win

Set to true if the player collects the goal object.

The enemy and the player share many of the same attributes because they both use the same type of calculations to move about the grid. Now let’s examine how the enemy object is constructed.

The Enemy

Each enemy object has its own set of attributes that are very similar to those of the player. Like the player, each enemy is an object instance.

Here is the code from the createPlayField() function that sets up the attributes for a new enemy object:

EnemyLocationFound = true;
var tempEnemy = {};
tempEnemy.row = randRow;
tempEnemy.col = randCol;
tempEnemy.nextRow = 0;
tempEnemy.nextCol = 0;
tempEnemy.currentTile = 0;
tempEnemy.rotation = 0;
tempEnemy.x = tempEnemy.col*32;
tempEnemy.y = tempEnemy.row*32;
tempEnemy.speed = 2;
tempEnemy.destinationX = 0;
tempEnemy.destinationY = 0;
tempEnemy.dx = 0;
tempEnemy.dy = 0;
tempEnemy.hit = false;
tempEnemy.dead = false;
tempEnemy.moveComplete = false;
enemy.push(tempEnemy);
items[randRow][randCol] = 1;

A few extra things are worth pointing out in this code. The first is that each enemy object needs a moveComplete attribute. This is used in the animateEnemy() game state function. When the entire enemy battalion has moved to its new location, the game transitions to the next game state. This is discussed in detail in the next section, “Turn-Based Game Flow and the State Machine”.

Notice, too, that the new enemy objects are added to the enemy array and to the items multidimensional array. This ensures that the player and the goal cannot be placed on an enemy location. After the enemy moves from its initial location, the playField array still has a road tile to show in its place. We call the player and the enemy moving-object tiles because they can move about the game board. When they move, they must uncover the road tile in the spot they were in before moving.

Now let’s take a quick look at the goal tile to solidify your understanding of the difference between the playField and the moving object tiles.

The Goal

The tile ID of the goal tile is stored in the playField array along with the road and wall tiles. It is not considered a separate item because, unlike the player and enemy objects, it does not need to move. As we have described previously, because the enemy and player tiles move on top of the playfield, they are considered moving items and not part of the playfield.

The Explosions

The explosion tiles are unique. They are rendered on top of the playfield when an enemy tank or the player’s hit attribute has been set to true. The explosion tiles animate through a list of five tiles and then are removed from the game screen. Again, tiles for the explosion are set in the explodeTiles array:

var explodeTiles = [17,18,19,18,17];

Next, we examine the entire game flow and state machine to give you an overall look at how the game logic is designed.

Turn-Based Game Flow and the State Machine

Our game logic and flow is separated into 16 discrete states. The entire application runs on a 40-frames-per-second interval timer:

switchGameState(GAME_STATE_INIT);
var FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;
setInterval(runGame, intervalTime )

As with the other games, in Chapter 8 and earlier in this chapter, we use a function reference state machine to run our current game state. The switchGameState() function transitions to a new game state. Let’s discuss this function briefly and then move through the rest of the game functions.

We do not reprint each line of code or dissect it in detail here. Use this section as a guide for perusing the entire set of game code included at the end of this chapter (in Example 9-2). By now, you have seen most of the code and ideas that create this game logic. We break out the new ideas and code in the sections that follow.

GAME_STATE_INIT

This state loads the assets we need for our game. We are loading only a single tile sheet and no sounds for Micro Tank Maze.

After the initial load, it sends the state machine to the GAME_STATE_WAIT_FOR_LOAD state until the load event has occurred.

GAME_STATE_WAIT_FOR_LOAD

This state simply makes sure that all the items in GAME_STATE_INIT have loaded properly. It then sends the state machine to the GAME_STATE_TITLE state.

GAME_STATE_TITLE

This state shows the title screen and then waits for the space bar to be pressed. When this happens, it sends the state machine to GAME_STATE_NEW_GAME.

GAME_STATE_NEW_GAME

This state resets all the game arrays and objects and then calls the createPlayField() function. The createPlayField() function creates the playField and enemy arrays for the new game and sets the player object’s starting location. When it has finished, it calls the renderPlayField() function a single time to display the initial board on the game screen.

When this completes, the state machine is now ready to start the real game loop by moving the game state machine to the GAME_STATE_WAIT_FOR_PLAYER_MOVE state.

GAME_STATE_WAIT_FOR_PLAYER_MOVE

This state waits for the player to press one of the four arrow buttons. When the player has done so, the switch statement checks which arrow was pressed. Based on the direction pressed, the checkBounds() function is called.

This state contains a bit of the new code for tile movement logic that we have not seen previously in this book. See the upcoming section, “Simple Tile Movement Logic Overview”, for more details on these concepts.

The checkBounds() function accepts three parameters:

  • The number to increment the row the player is currently in

  • The number to increment the column the player is currently in

  • The object being tested (either the player or one of the enemy tanks)

The sole purpose of this function is to determine whether the object being tested can move in the desired direction. In this game, the only illegal moves are off the side of the screen. In games such as Pac-Man, this would check to make sure that the tile was not a wall tile. Our game does not do this because we want the player and the enemy objects to be able to move mistakenly onto the wall tiles (and be destroyed).

If a valid move is found for the player in the direction pressed, the setPlayerDestination() function is called. This function simply sets the player.destinationX and player.destinationY attributes based on the new tile location.

checkBounds() sets the player.nextRow and player.nextCol attributes. The setPlayerDestination() function multiplies the player.nextRow and the player.nextCol by the tile size (32) to determine the player.destinationX and player.destinationY attributes. These move the player to its new location.

GAME_STATE_ANIMATE_PLAYER is then set as the current game state.

GAME_STATE_ANIMATE_PLAYER

This function moves the player to its destinationX and destinationY locations. Because this is a turn-based game, we don’t have to do any other processing while this movement is occurring.

On each iteration, the player.currentTile is incremented by 1. This changes the tile that is rendered to be the next tile in the playerTiles array. When destinationX and destinationY are equal to the x and y values for the player, the movement and animation stop, and the game state is changed to the GAME_STATE_EVALUATE_PLAYER_MOVE state.

GAME_STATE_EVALUATE_PLAYER_MOVE

Now that the player has moved to the next tile, the player.row and player.col attributes are set to player.nextRow and player.nextCol, respectively.

Next, if the player is on a goal tile, the player.win attribute is set to true. If the player is on a wall tile, the player.hit is set to true.

We then loop though all the enemy objects and see whether any occupy the same tile as the player. If they do, both the player and the enemy hit attributes are set to true.

Next, we move the game to the GAME_STATE_ENEMY_MOVE state.

GAME_STATE_ENEMY_MOVE

This state uses the homegrown chase AI—discussed in “Simple Homegrown AI Overview”—to choose a direction in which to move each enemy tank. It does this by looping through all the tanks and applying the logic to them individually.

This function first uses a little tile-based math to determine where the player is in relation to an enemy tank. It then creates an array of directions to test based on these calculations. It stores these as string values in a directionsToTest variable.

Next, it uses the chanceRandomMovement value (25%) to determine whether it will use the list of directions it just compiled or throw them out and simply choose a random direction to move in.

In either case, it must check all the available directions (either in the list of directionsToMove or in all four directions for random movement) to see which is the first that will not move the tank off the side of the screen.

When it has the direction to move in, it sets the destinationX and destinationY values of the enemy tank, using the same tile size * x and tile size * y trick used for the player.

Finally, it sets the game state to GAME_STATE_ANIMATE_ENEMY.

GAME_STATE_ANIMATE_ENEMY

Like GAME_STATE_ANIMATE_PLAYER, this state moves and animates the tank to its new location represented by its destinationX and destinationY values. It must do this for each of the enemy tanks, so it uses the enemyMoveCompleteCount variable to keep count of how many of the enemy tanks have finished their moves.

When all the enemy tanks have completed their moves, the game state is changed to the GAME_STATE_EVALUATE_ENEMY_MOVE state.

GAME_STATE_EVALUATE_ENEMY_MOVE

Like GAME_STATE_EVALUATE_PLAYER_MOVE, this state looks at the location of each tank to determine which ones need to be destroyed.

If a tank occupies the same tile as the player, a wall, or another tank, the tank is “to be destroyed.” If the player and enemy tank occupy the same tile, the player is also “to be destroyed.” This “to be destroyed” state is set by placing true in the hit attribute of the enemy tank or the player.

The game is then moved to the GAME_STATE_EVALUATE_OUTCOME state.

GAME_STATE_EVALUATE_OUTCOME

This function looks at each of the enemy tanks and the player tank to determine which have a hit attribute set to true. If any do, that tank’s dead attribute is set to true, and an explosion is created by calling createExplode() and passing in the object instance (player or enemy tank). In the case of the enemy, a dead enemy is also removed from the enemy array.

The GAME_STATE_ANIMATE_EXPLODE state is called next.

GAME_STATE_ANIMATE_EXPLODE

If the explosions array length is greater than 0, this function loops through each instance and animates it, using the explodeTiles array. Each explosion instance is removed from the explosions array after it finishes its animation. When the explosions array length is 0, the game moves to the GAME_STATE_CHECK_FOR_GAME_OVER state.

GAME_STATE_CHECK_FOR_GAME_OVER

This state first checks whether the player is dead and then checks to see whether she has won. The player cannot win if an enemy tank makes it to the goal on the same try as the player.

If the player has lost, the state changes to GAME_STATE_PLAYER_LOSE; if the player has won, it moves to the GAME_STATE_PLAYER_WIN state. If neither of those has occurred, the game is set to GAME_STATE_WAIT_FOR_PLAYER_MOVE. This starts the game loop iteration over, and the player begins her next turn.

GAME_STATE_PLAYER_WIN

If the player wins, the maxEnemy is increased for the next game. The player’s score is also checked against the current session high score to determine whether a new high score has been achieved. This state waits for a space bar press and then moves to the GAME_STATE_NEW_GAME state.

GAME_STATE_PLAYER_LOSE

The player’s score is checked against the current session high score to determine whether a new high score has been achieved. This state waits for a space bar press and then moves to the GAME_STATE_NEW_GAME state.

Simple Tile Movement Logic Overview

Micro Tank Maze employs simple tile-to-tile movement by using the “center of a tile” logic. This logic relies on making calculations when the game character has reached the center of a tile. The origin point of our game character tiles is the top-left corner. Because of this, we can easily calculate that a game character is in the center of a tile when its x and y coordinates are equal to the destination tile’s x and y coordinates.

When the user presses a movement key (up, down, right, or left arrow), we first must check whether the player is trying to move to a legal tile on the playField. In Micro Tank Maze, all tiles are legal. The only illegal moves are off the edges of the board. So, if the player wants to move up, down, left, or right, we must first check the tile in that direction based on the key pressed in the gameStateWaitForPlayerMove() function. Here is the switch statement that determines whether the player pressed an arrow key:

if (keyPressList[38]==true){
       //up
       if (checkBounds(-1,0, player)){
       setPlayerDestination();
       }
    }else if (keyPressList[37]==true) {
       //left
       if (checkBounds(0,-1, player)){
       setPlayerDestination();
       }
    }else if (keyPressList[39]==true) {
       //right
       if (checkBounds(0,1, player)){
       setPlayerDestination();
       }
    }else if  (keyPressList[40]==true){
       //down
       if (checkBounds(1,0, player)){
       setPlayerDestination();
       }
    }

Notice that the checkBounds() function takes a row increment and then a column increment to test. It is important to note that we don’t access tiles in the same manner we would access pixels on the screen. Tiles in the playField array are accessed by addressing the vertical (row) and then the horizontal (column) (using [row][column], not [column][row]). This is because a simple array is organized into a set of rows. Each row has a set of 15 columns. Therefore, we do not access a tile in playField by using the [horizontal][vertical] coordinates. Instead, we use the [row][column] syntax that simple arrays use to powerful and elegant effect.

In the checkBounds() function, enter the row increment, then the column increment, and then the object to be tested. If this is a legal move, the checkBounds() function sets nextRow and nextCol to be row+rowInc and col+colInc, respectively:

function checkBounds(rowInc, colInc, object){
   object.nextRow = object.row+rowInc;
   object.nextCol = object.col+colInc;

   if (object.nextCol >=0 && object.nextCol<15 &&
    object.nextRow>=0 && object.nextRow<15){
      object.dx = colInc;
      object.dy = rowInc;

      if (colInc==1){
      object.rotation = 90;
      }else if (colInc==-1){
      object.rotation = 270;
      }else if (rowInc==-1){
      object.rotation = 0;
      }else if (rowInc==1){
      object.rotation = 180;
      }

      return(true);

   }else{
      object.nextRow = object.row;
      object.nextCol = object.col;
      return(false);

   }

}

If the move is legal, the dx (delta, or change in x) and dy (delta, or change in y) are set to colInc and rowInc, respectively.

The animatePlayer() function is called next. Its job is to move the player object to its new location while running through its animation frames. Here is the code from the animatePlayer() function:

player.x += player.dx*player.speed;
player.currentTile++;if (player.currentTile==playerTiles.length){
   player.currentTile = 0;
}
renderPlayField();
if (player.x==player.destinationX && player.y==player.destinationY){
   switchGameState(GAME_STATE_EVALUATE_PLAYER_MOVE);
}

First, the player object’s x and y locations are increased by the player.speed * player.dx (or dy). The tile size is 32, so we must use a speed value that is evenly divided into 32. The values 1, 2, 4, 8, 16, and 32 are all valid.

This function also runs though the playerTiles array on each game loop iteration. This renders the tank tracks moving, simulating a smooth ride from one tile to the next.

Next, let’s take a closer look at how we render playField.

Rendering Logic Overview

Each time the game renders objects to the screen, it runs through the entire render() function. It does this to ensure that even the nonmoving objects are rendered back to the game screen. The render() function looks like this:

function renderPlayField() {
   fillBackground();
   drawPlayField();
   drawPlayer();
   drawEnemy();
   drawExplosions();
}

First, we draw the plain black background, and then we draw playField. After that, we draw the game objects. drawPlayField() draws the map of tiles to the game screen. This function is similar to the functions in Chapter 4 but with some additions for our game. Let’s review how it is organized:

function drawPlayField(){
   for (rowCtr=0;rowCtr<15;rowCtr++){

      for (colCtr=0;colCtr<15;colCtr++) {
      var sourceX = Math.floor((playField[rowCtr][colCtr]) % 8) * 32;
      var sourceY = Math.floor((playField[rowCtr][colCtr]) /8) *32;

      if (playField[rowCtr][colCtr] != roadTile){
          context.drawImage(tileSheet, 0, 0,32,32,colCtr*32,rowCtr*32,32,32);
      }
      context.drawImage(tileSheet, sourceX, sourceY, 32,32,
       colCtr*32,rowCtr*32,32,32);
       }
    }
 }

The drawPlayField() function loops through the rows in the playField array and then through each column inside each row. If the tile ID number at playField[rowCtr][colCtr] is a road tile, it simply paints that tile at the correct location on playField. If the tile ID is a game object (not a road tile), it first paints a road tile in that spot and then paints the object tile in that spot.

Simple Homegrown AI Overview

The enemy tanks chase the player object based on a set of simple rules. We have coded those rules into the gameStateEnemyMove() function, which is one of the longest and most complicated functions in this book. Let’s first step through the logic used to create the function, and then you can examine it in Example 9-2.

This function starts by looping through the enemy array. It must determine a new tile location on which to move each enemy. To do so, it follows some simple rules that determine the order in which the testBounds() function will test the movement directions:

  1. First, it tests to see whether the player is closer to the enemy vertically or horizontally.

  2. If vertically, and the player is above the enemy, it places up and then down in the directionsToTest array.

  3. If vertically, and the player is below the enemy, it places down and then up in the directionsToTest array.

    The up and then down, or down and then up, directions are pushed into the directionsTest array to simplify the AI. The logic here is if the player is up from the enemy, but the enemy is blocked by an object, the enemy will try the opposite direction first. In our game, there will be no instance when an object blocks the direction the enemy tank wants to move in, because the only illegal direction is trying to move off the bounds of the screen. If we add tiles to our playfield that block the enemy, this entire set of AI code suddenly becomes very useful and necessary. We have included this entire homegrown chase AI in our game in case more of these tile types are added.

  4. It then looks where to add the left and right directions. It does this based on which way will put it closer to the player.

  5. If the horizontal direction and not the vertical direction is the shortest, it runs through the same type of logic, but this time using left and then right, then up and then down.

  6. When this is complete, all four directions will be in the directionsToTest array.

Next, the logic finds a number between 0 and 99 and checks whether it is less than the chanceRandomEnemyMovement value. If it is, it ignores the directionsToTest array and simply tries to find a random direction to move in. In either case, all the directions (either in the directionsToTest array or in order up, down, left, and right) are tested until the testBounds() function returns true.

That’s all there is to this code. In Example 9-2, you find the entire set of code for this game.

Micro Tank Maze Complete Game Code

The full source code and assets for Micro Tank Maze are located at this site.

Scrolling a Tile-Based World

One of the advantages of using a tile-based world is that it can be virtually any size we want it to be. Although some memory and processor limitations (especially on mobile devices) might cause problems when trying to scroll or pan over an image that is much larger than the canvas size, as we did in Chapter 4, there is virtually no limit to the size of a game world that can be created with tiles.

The power comes from the use of the simple two-dimensional arrays we use to hold the game world. As we examined in Chapter 8, the concept of painting the game screen with tiles is pretty simple. Here’s a short review of the process of painting the screen from a tile-based grid.

First, a Tile Sheet That Contains the Tiles We Want to Paint to the Screen

We will be using the same tile sheet as in Chapter 8 (see Figure 9-12).

The tiles sheet for the scrolling examples
Figure 9-12. The tiles sheet for the scrolling examples

Next, we use the data from a two-dimensional array to paint these tiles to the game screen to create our game world.

Second, a Two-Dimensional Array to Describe Our Game World

We put this game world into an array. Each number in the array rows and columns represents a tile number in our game world. So, a 0 would paint the first tile in the tile sheet (the blue wall), a 1 would be gray road tile, and so on.

world.map=[
   [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
  ,[0,1,2,1,1,1,1,1,1,1,1,1,1,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,1,1,0,0,1,0,1,0]
  ,[0,2,1,1,1,1,0,0,0,1,1,1,1,1,0]
  ,[0,1,0,0,0,1,0,0,0,1,0,0,0,1,0]
  ,[0,1,1,1,2,1,0,0,0,1,1,1,1,1,0]
  ,[0,0,0,0,0,1,1,1,1,1,0,0,0,0,0]
  ,[0,1,1,1,1,1,0,0,0,1,1,1,1,1,0]
  ,[0,1,0,1,0,0,1,1,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,2,0,0,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,1,1,1,1,1,2,1,1,1,1,1,1,0]
  ,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
 ];

Third, Paint the Tile-Based World to the Canvas

Our world is built using a 15x15 grid. Figure 9-13 shows the entire world we will use in our scrolling examples.

A 15×15 tile map
Figure 9-13. A 15×15 tile map

For the scrolling examples, we use a canvas that is smaller than the world. We are using a small canvas (160×160) and a small world (480×480) simply to make sure that the code is easily understood in book form. In a real-world example, the tile-based world might be 100+ tiles in each dimension, drawn onto a canvas that is 640×480. Here is the HTML5 Canvas object we will be using:

<canvas id="canvas" width="160" height="160">

Coarse Scrolling vs. Fine Scrolling

There are two methods by which we can scroll the game screen: coarse scrolling and fine scrolling. Coarse scrolling is a method by which we scroll the screen a single tile at a time. Therefore, because our tiles are 32×32 pixels, we would scroll 32 pixels in the direction the user presses with the keys. We allow the user to use the up, down, left, and right arrow keys in our examples to move a viewable window over the tile world map.

Fine scrolling is a method by which the user scrolls just a few pixels at a time. This is a much more powerful and user-friendly method for scrolling the game screen and would be useful for most games.

Each method has its uses, though. Coarse scrolling can be applied to strategy-based games, board games, or any game in which scrolling a single tile at a time does not distract from the user experience. Fine scrolling, however, is useful for most scrolling applications because it allows simple physics, such as delta x and y changes, to be applied to the scrolling at a rate that is smaller than an entire tile size. A platform game such as Super Mario Brothers or a scrolling shooter such as Raiden would be good uses for fine scrolling.

Next, let’s define some of the concepts we will apply in our code.

The Camera Object

The viewable window the user will see is called the Camera. It displays just a portion of the tile-based world at a single time. We allow the user to move the camera with the arrow keys to scroll through our tile-map.

The camera will have these attributes:

camera.height=theCanvas.height;
camera.width=theCanvas.width;
camera.rows=camera.height / world.tileHeight;
camera.cols=camera.width / world.tileWidth;
camera.dx=0;
camera.dy=0;
camera.x=0;
camera.y=0;

The camera object is not complicated. It contains just the necessary attributes to move and paint it to the screen during a setTimeOut interval based on user key presses. Its height and width come directly from the Canvas size (160×160). The x and y values represent the upper left corner of the camera of the game world. In coarse scrolling, this is either 0 or a multiple of 32 (our tile height and width). The maximum value for the upper left corner of the camera on the x-axis is the world width (480 in our example) minus the camera width (160 in our example). This way, the camera never tries to paint tiles that do not exist in our world map array.

Fine scrolling is similar, but the values for x and y top left corner, or the camera, can each be 0 or any number up to a maximum we calculate to ensure that we are not trying to paint tiles that do not exist off the right side or bottom of the tile-map. In essence, we don’t need to scroll 32 pixels at a time but, rather, any number from 1 to 32 (or more, but that would result in extra coarse scrolling and is not examined here as a practical application).

The dx and dy values will be the number of pixels to move the x and y on an interval based on the user key press.

As you can see, the camera dynamically calculates the number of rows and columns it needs to be, based on the tileHeight and tileWidth from the world. On that note, let’s examine the world now.

The World Object

The world object contains just the necessary information to create the world the camera will scroll around on and display to the user. The actual world is never shown to the user as a visible object:

world.cols=15;
world.rows=15;
world.tileWidth=32;
world.tileHeight=32;
world.height=world.rows*world.tileHeight;
world.width=world.cols*world.tileWidth;
world.map=[];

The cols and rows depict the entire size of the world, and the tileHeight and tileWidth values are used in calculations when determining the camera position and painting the world tiles to the Camera. The height and width are calculated from the first four values, and the map array is filled in with the map data we examined previously.

Fine Scrolling the Row and Column Buffers

The secret in fine scrolling the canvas is in the row and column buffers. These contain an extra tile (outside the viewable Camera area) that is not needed in coarse scrolling. The buffer is very important because when we fine scroll, we are usually painting only part of the tiles that are on the left, right, top, and bottom edges of the viewable camera.

If camera.x and camera.y are both at 0 (the top left edge of the tile map), we don’t need a scroll buffer. If camera.x or camera.y is at ANY other position on the game map screen, we need a scroll buffer for whichever (or both) dimensions are greater than 0, but not at the far right or bottom edge of the viewable world (as described earlier). As you can probably imagine, when playing a game, these x and y values will seldom be 0. Let’s take a close look at some examples of this now, because it is the crux of how we calculate and paint the game screen when fine scrolling.

Here is the code we will use to decide whether to use a row or column buffer when we draw our tile map when fine scrolling:

if (camera.x<=0) {
    camera.x=0;
    colBuffer=0;
}else if (camera.x > (world.width - camera.width)-scrollRate) {
    camera.x=world.width - camera.width;
    colBuffer=0;
}else{
   colBuffer=1;
}

if (camera.y<=0) {
    camera.y=0;
    rowBuffer=0;
}else if (camera.y > (world.height - camera.height)-scrollRate) {
    camera.y=world.height - camera.height;
    rowBuffer=0;
}else{
    rowBuffer=1;
}

The algorithm finds the necessary colBuffer and rowBuffer values, depending on the x and y values of the camera object.

  1. If the camera x or y value is 0 or less than 0, we first set it to 0 (so that we are not trying to draw from the negative space of the game map that does not exist), and we set the corresponding colBuffer or rowBuffer to 0.

  2. If the x or y value is not 0 or greater than 0, we next check to see whether the camera will draw from outside the far right or far bottom of the tile map (those tiles do not exist). If that is true, we also set the corresponding rowBuffer or colBuffer to 0.

  3. If on either the x or y axis the camera is in the middle of the tile map, the corresponding colBuffer or rowBuffer is set to 1. This adds an extra tile row or column to the drawn screen and allows the partial tile to be displayed.

Rather than simply going through this line by line, let’s look at four examples and how they would be displayed and calculated in our code.

The camera top-left position

At the upper-left position in our game world, the values to be plugged in would be those in the list that follows. This is the simplest version of scrolling because, in effect, there is no scrolling.

  • scrollRate = 4

  • camera.x = 0

  • camera.y = 0

Subtracting camera.width (160) from world.width (480) = 320. Next, we subtract scrollRate from this result to use in calculating the value of colBuffer.

We use the same algorithm for the y axis to get rowBuffer: camera.y > (world.height – camera.height) – scrollRate:

(world.width - camera.width) - scrollRate = 316
(world.height - camera.height) - scrollRate = 316

In this example, because the window is at the top-left corner, these values (316, 316) are not needed, but we wanted to demonstrate them because they will be used in the examples. Because we are in the upper left corner of the map, we simply need to check for camera.x and camera.y being less than or equal to 0.

if (camera.x<=0) {
    camera.x=0;
    colBuffer=0;
colBuffer= 0
if (camera.y<=0) {
    camera.y=0;
    rowBuffer=0;
rowBuffer= 0

Figure 9-14 shows how the upper left corner would be drawn.

The fine scrolling camera at position 0,0
Figure 9-14. The fine scrolling camera at position 0,0

Now let’s take a look at the most common type of calculation. This occurs when the viable camera is not right at the edge or bottom of the game screen and not at the top left corner of either of the row or column tiles.

The camera scrolled position

The camera in a scrolled position means that it is not in the upper corner of the screen. In this example, we place the camera in about the middle of the screen.

scrollRate = 4
camera.x = 180
camera.y = 120

Subtracting camera.width (160) from world.width = 320. Next, we subtract scrollRate from this result to use in calculating the value of colBuffer.

We use the same algorithm for the y axis to get rowBuffer: camera.y > (world.height - camera.height) -scrollRate:

(world.width - camera.width) - scrollRate = 316
(world.height - camera.height) - scrollRate = 316
colBuffer= 1
rowBuffer= 1

In this case, we need to add a scroll buffer on each axis. Figure 9-15 shows what would be painted to the canvas at this camera position.

The fine scrolling camera at position 180,120 with scroll buffers
Figure 9-15. The fine scrolling camera at position 180,120 with scroll buffers

What you don’t see are the scroll buffers on each axis that actually allow for the fine scrolling to take place. Figure 9-15 shows what is painted to the canvas and the extra map image data that is not painted to the canvas. This figure has been zoomed in to show that we actually need to draw an extra row and extra column to the canvas when the camera is in a fine-scrolled state. To display the actual position of the scrolled tiles, we first use a matrix transformation to translate the canvas to the actual point we want to paint to the screen:

context.setTransform(1,0,0,1,0,0);
context.translate(-camera.x%world.tileWidth, -camera.y%world.tileHeight);

The mod (%) operation returns us just the number of pixels on each axis we need to move back in the negative direction to show the portion of all the tiles in Figure 9-15.

We then loop though all the tiles and paint them starting at that position. Therefore, the first tile in each row painted starts in the negative position, off the canvas, and only a portion of it is actually painted to the canvas. The last tile in each row is painted onto only a portion of the canvas. The corresponding paint operations on the columns work the same way. By doing this, we are fine scrolling by simply translating the canvas over the entire subset of tiles (including the extra buffer tiles, see Figure 9-16).

Here is the code that loops through the rows and tiles and paints them to the screen, starting at the newly translated point:

for (rowCtr=0;rowCtr<camera.rows+rowBuffer;rowCtr++) {
    for (colCtr=0;colCtr<camera.cols+colBuffer;colCtr++) {

        tileNum=(world.map[rowCtr+tiley][colCtr+tilex]);

        var tilePoint={};
        tilePoint.x=colCtr*world.tileWidth;
        tilePoint.y=rowCtr*world.tileHeight;
        var source={};
        source.x=Math.floor(tileNum % 5) * world.tileWidth;
        source.y=Math.floor(tileNum /5) *world.tileHeight;
        context.drawImage(tileSheet, source.x,
         source.y,world.tileWidth,world.tileHeight, tilePoint.x, tilePoint.y,
                  world.tileWidth, world.tileHeight);
            }

        }
The fine scrolling camera at position 180,120
Figure 9-16. The fine scrolling camera at position 180,120

Notice in the code that we add the rowBuffer value (in this case, 1) to the rowCtr loop, and we add the colBuffer value to the colCtr loop. Next, we look at edge cases (literally). These occur when the camera has been scrolled to the far right or far bottom of the tile map.

The camera far-right scrolled position

When the camera scrolls past the point where an extra tile would need to be on the far right of the game screen map, we need to set it back to the position it was and not try to paint an extra column in colBuffer, because the tile map world does not have any more tiles to display. If we didn’t do this, an error would be thrown in JavaScript, telling us that we had hit a null in the world.map array. In essence, we are subtracting the dx or scrollRate value from the current camera position.

We have seen this code previously, but here it is one more time. This edge case is the first else: (bold and larger type):

if (camera.x<=0) {
    camera.x=0;
    colBuffer=0;
}else if (camera.x > (world.width - camera.width)-scrollRate) {
    camera.x=world.width - camera.width;
    colBuffer=0;
}else{
    colBuffer=1;
}

Figure 9-16 shows an example of the right-side edge case.

The fine scrolling camera at position right-side edge
Figure 9-17. The fine scrolling camera at position right-side edge

The data for the algorithm would look like this:

scrollRate= 4
camera.x= 320
camera.y= 80

This returns buffer values as follows data:

colBuffer= 0
rowBuffer= 1

Because we are scrolled all the way to the right, the camera.x value is 320. When we add the camera.width of 160 to this value 320, we get 480. This just so happens to be the width of the world (world.width). Notice that we have to subtract the scrollRate to ensure that we are always comparing a value that is less than where the player wants to go; if we didn’t do this, we could actually throw a null pointer error in the column array lookup.

The camera far down scrolled position

When the camera scrolls past the point at which an extra tile would need to be on the far bottom of the game screen map, we need to set it back to the position it was and not try to paint an extra row in rowBuffer, because the tile map world does not have any more tiles to display. If we didn’t do this, an error would be thrown in JavaScript, telling us we had hit a null in the world.map array. In essence, we are subtracting the dy or scrollRate value from the current camera position.

We have seen this code previously, but here it is one more time. This edge case is the first else: (bold and larger type):

if (camera.y<=0) {
    camera.y=0;
    rowBuffer=0;
}else if (camera.y > (world.height - camera.height)-scrollRate) {
    camera.y=world.height - camera.height;
    rowBuffer=0;
}else{
    rowBuffer=1;
}

Figure 9-18 shows an example of the bottom edge case.

The fine scrolling camera at position bottom edge
Figure 9-18. The fine scrolling camera at position bottom edge

The data for the algorithm would look like this:

scrollRate= 4
camera.x= 40
camera.y= 320

This will return buffer values as follows data:

colBuffer= 1
rowBuffer= 0

Because we are scrolled all the way down, the camera.y value is 320. When we add camera.height of 160 to this value 320, we get 480. This just so happens to be the height of the world (world.height). Notice that we have to subtract scrollRate to ensure that we are always comparing a value that is less than where the player wants to go; if we didn’t do this, we could actually throw a null pointer error in the row array lookup.

Coarse Scrolling Full Code Example

Example 9-2, shows the full code listing for the Coarse Scrolling example. The code for this is a little bit simpler than the Fine Scrolling version because we do not need the rowBuffer and colBuffer variables. We also do not need to the matrix transformation to translate the Canvas. We simply need to paint the current set of tiles and will never need to paint any partial tiles as we would with Fine scrolling.

Example 9-2. Coarse scrolling
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH9 EX3 - Scrolling Test 1 coarse scrolling</title>
<script src="modernizr.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

    canvasApp();

}


</script>


<script language="Javascript">

function canvasSupport () {
      return Modernizr.canvas;
}


function canvasApp(){

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


    document.onkeydown=function(e){
        e=e?e:window.event;
        keyPressList[e.keyCode]=true;
    }

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

    //key presses
    var keyPressList=[];

    //images
    var tileSheet = new Image();
    tileSheet.src = "scrolling_tiles.png";

    //mapdata
    var world={};

    //camera
    var camera={}

    //key presses
    var keyPressList={};


    function init() {

        world.cols=15;
        world.rows=15;
        world.tileWidth=32;
        world.tileHeight=32;
        world.height=world.rows*world.tileHeight;
        world.width=world.cols*world.tileWidth;

        camera.height=theCanvas.height;
        camera.width=theCanvas.width;
        camera.rows=camera.height / world.tileHeight;
        camera.cols=camera.width / world.tileWidth;

        camera.dx=0;
        camera.dy=0;
        camera.x=0;
        camera.y=0;

        keyPressList=[];
        gameLoop()
    }

    function runGame() {
        camera.dx=0;
        camera.dy=0;
        //check input
        if (keyPressList[38]){
            console.log("up");
            camera.dy=-world.tileHeight;
        }
        if (keyPressList[40]){
            console.log("down");
            camera.dy=world.tileHeight;
        }

        if (keyPressList[37]){
            console.log("left");
            camera.dx=-world.tileWidth;
        }
        if (keyPressList[39]){
            console.log("right");
            camera.dx=world.tileWidth;
        }

        camera.x+=camera.dx;
        camera.y+=camera.dy;

        if (camera.x<0) {
            camera.x=0;

        }else if (camera.x > (world.width - camera.width)-world.tileWidth) {
            camera.x=world.width - camera.width;
        }

        if (camera.y<0) {
            camera.y=0;

        }else if (camera.y > (world.height - camera.height)-world.tileHeight) {
            camera.y=world.height - camera.height;
        }

        context.fillStyle = '#000000';
        context.fillRect(0, 0, theCanvas.width, theCanvas.height);

        //draw camera
        //calculate starting tile position

        var tilex=Math.floor(camera.x/world.tileWidth);
        var tiley=Math.floor(camera.y/world.tileHeight);
        var rowCtr;
        var colCtr;
        var tileNum;

        for (rowCtr=0;rowCtr<camera.rows;rowCtr++) {
            for (colCtr=0;colCtr<camera.cols;colCtr++) {

                tileNum=(world.map[rowCtr+tiley][colCtr+tilex]);
                var tilePoint={};
                tilePoint.x=colCtr*world.tileWidth;
                tilePoint.y=rowCtr*world.tileHeight;
                var source={};
                source.x=Math.floor(tileNum % 5) * world.tileWidth;
                source.y=Math.floor(tileNum /5) *world.tileHeight;
                context.drawImage(tileSheet, source.x, source.y,world.tileWidth,
                                  world.tileHeight,tilePoint.x,tilePoint.y,
                                  world.tileWidth,world.tileHeight);
            }

        }

    }

    world.map=[
   [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
  ,[0,1,2,1,1,1,1,1,1,1,1,1,1,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,1,1,0,0,1,0,1,0]
  ,[0,2,1,1,1,1,0,0,0,1,1,1,1,1,0]
  ,[0,1,0,0,0,1,0,0,0,1,0,0,0,1,0]
  ,[0,1,1,1,2,1,0,0,0,1,1,1,1,1,0]
  ,[0,0,0,0,0,1,1,1,1,1,0,0,0,0,0]
  ,[0,1,1,1,1,1,0,0,0,1,1,1,1,1,0]
  ,[0,1,0,1,0,0,1,1,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,2,0,0,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,1,1,1,1,1,2,1,1,1,1,1,1,0]
  ,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
 ];


    init();

    var FRAME_RATE=10;
    var intervalTime=1000/FRAME_RATE;

    function gameLoop() {
        runGame();
        window.setTimeout(gameLoop, intervalTime);
    }

}



</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="160" height="160">
 Your browser does not support the HTML5 Canvas.
</canvas>
</div>
</body>
</html>

When you try this in a browser, you can use the arrow keys to scroll the 160×160 camera around the game world. Each key press moves the window 32 pixels at a time in the direction pressed. Now let’s take a look at the full code listing for the fine scrolling version.

Fine Scrolling Full Code Example

Example 9-3 shows the full code listing for the Fine Scrolling example. Notice that this code adds in the colBuffer and rowBuffer variables as well as the matrix transformation secret that performs the actual smooth fine scrolling.

Example 9-3. Fine scrolling
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH9 EX4 Scrolling Test 2 fine scrolling</title>
<script src="modernizr.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

    canvasApp();

}

</script>


<script language="Javascript">

function canvasSupport () {
      return Modernizr.canvas;
}


function canvasApp(){

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


    document.onkeydown=function(e){
        e=e?e:window.event;
        keyPressList[e.keyCode]=true;
    }

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

    //key presses
    var keyPressList=[];

    //images
    var tileSheet = new Image();
    tileSheet.src = "scrolling_tiles.png";

    //mapdata
    var world={};

    //camera
    var camera={};

    //key presses
    var keyPressList={};

    var rowBuffer=1;
    var colBuffer=1;

    var scrollRate=4;

    function init() {

        world.cols=15;
        world.rows=15;
        world.tileWidth=32;
        world.tileHeight=32;
        world.height=world.rows*world.tileHeight;
        world.width=world.cols*world.tileWidth;

        camera.height=theCanvas.height;
        camera.width=theCanvas.width;
        camera.rows=camera.height / world.tileHeight;
        camera.cols=camera.width / world.tileWidth;

        camera.dx=0;
        camera.dy=0;
        camera.x=0;
        camera.y=0;

        keyPressList=[];
        //console.log("camera.rows=", camera.rows, "camera.cols=", camera.cols);
        gameLoop()
    }

    function runGame() {
        camera.dx=0;
        camera.dy=0;
        //check input
        if (keyPressList[38]){
            console.log("up");
            camera.dy=-scrollRate;
        }
        if (keyPressList[40]){
            console.log("down");
            camera.dy=scrollRate;
        }

        if (keyPressList[37]){
            console.log("left");
            camera.dx=-scrollRate;
        }
        if (keyPressList[39]){
            console.log("right");
            camera.dx=scrollRate;
        }

        camera.x+=camera.dx;
        camera.y+=camera.dy;




        if (camera.x<=0) {
            camera.x=0;
            colBuffer=0;
        }else if (camera.x > (world.width - camera.width)-scrollRate) {
            camera.x=world.width - camera.width;
            colBuffer=0;
        }else{
            colBuffer=1;
        }

        if (camera.y<=0) {
            camera.y=0;
            rowBuffer=0;
        }else if (camera.y > (world.height - camera.height)-scrollRate) {
            camera.y=world.height - camera.height;
            rowBuffer=0;
        }else{
            rowBuffer=1;
        }

        console.log("scrollRate=", scrollRate);

        var xDistance=(world.width - camera.width)-scrollRate;
        console.log("camera.x=", camera.x);
        console.log("(world.width - camera.width)-scrollRate =", xDistance);

        var yDistance=(world.height - camera.height)-scrollRate;
        console.log("camera.y=", camera.y);
        console.log("(world.height - camera.height)-scrollRate =", yDistance);

        console.log("colBuffer=", colBuffer);
        console.log("rowBuffer", rowBuffer);

        context.fillStyle = '#000000';
        context.fillRect(0, 0, theCanvas.width, theCanvas.height);

        //draw camera
        //calculate starting tile position

        var tilex=Math.floor(camera.x/world.tileWidth);
        var tiley=Math.floor(camera.y/world.tileHeight);
        var rowCtr;
        var colCtr;
        var tileNum;

        context.setTransform(1,0,0,1,0,0);
        context.translate(-camera.x%world.tileWidth, -camera.y%world.tileHeight);

        for (rowCtr=0;rowCtr<camera.rows+rowBuffer;rowCtr++) {
            for (colCtr=0;colCtr<camera.cols+colBuffer;colCtr++) {

                tileNum=(world.map[rowCtr+tiley][colCtr+tilex]);

                var tilePoint={};
                tilePoint.x=colCtr*world.tileWidth;
                tilePoint.y=rowCtr*world.tileHeight;
                var source={};
                source.x=Math.floor(tileNum % 5) * world.tileWidth;
                source.y=Math.floor(tileNum /5) *world.tileHeight;
                context.drawImage(tileSheet, source.x, source.y, world.tileWidth,
                                  world.tileHeight,tilePoint.x,tilePoint.y,
                                  world.tileWidth,world.tileHeight);
            }

        }

    }


    world.map=[
   [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
  ,[0,1,2,1,1,1,1,1,1,1,1,1,1,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,1,1,0,0,1,0,1,0]
  ,[0,2,1,1,1,1,0,0,0,1,1,1,1,1,0]
  ,[0,1,0,0,0,1,0,0,0,1,0,0,0,1,0]
  ,[0,1,1,1,2,1,0,0,0,1,1,1,1,1,0]
  ,[0,0,0,0,0,1,1,1,1,1,0,0,0,0,0]
  ,[0,1,1,1,1,1,0,0,0,1,1,1,1,1,0]
  ,[0,1,0,1,0,0,1,1,1,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,2,0,0,0,0,1,0,1,0]
  ,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0]
  ,[0,1,1,1,1,1,1,2,1,1,1,1,1,1,0]
  ,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
 ];


    init();

    var FRAME_RATE=10;
    var intervalTime=1000/FRAME_RATE;

    function gameLoop() {
        runGame();
        window.setTimeout(gameLoop, intervalTime);
    }

}



</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="160" height="160">
 Your browser does not support the HTML5 Canvas.
</canvas>
</div>
</body>
</html>

We have left in the console.log statements that show how the algorithm is detecting the rowBuffer and colBuffer values. These are the same values we saw previously in Example 9-3.

When you run this page in a browser, the arrow keys allow you to move the camera 4 pixels in each of the up, down, left, and right directions. You can change this value by setting scrollRate to a new value.

That’s all there is to both fine and coarse scrolling a tile-based screen. By using some of the examples in previous chapters, you can extend this to add path finding, physics, and even pixel-based collision detection to create any type of game you want that uses a scrolling screen.

What’s Next?

Throughout this book, we have used game and entertainment-related subjects to demonstrate canvas application building concepts. Over these last two chapters, we’ve sped up the game discussion and covered many game concepts directly by creating two unique games and optimizing a third with bitmaps and object pooling. We also introduced the powerful concept of tile-based coarse and fine scrolling and A* path finding to the mix. In doing so, we have applied many of the concepts from the earlier chapters in full-blown game applications and have added a new secret technique that can be applied to make some very powerful games and game engines. The techniques used to create a game on Canvas can be applied to almost any Canvas application, from image viewers to stock charting. The sky is really the limit because the canvas allows the developer a full suite of powerful, low-level capabilities that can be molded into any application.

In Chapter 10, we look at porting a simple game from Canvas into a native iPhone application and optimizing Geo Blaster Extended for a touch-based interface.