You are previewing HTML5 Canvas.

HTML5 Canvas

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

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 will create a simple turn-based maze-chase game. Our game, Micro Tank Maze, will be based loosely on Daleks, but we will 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 is tasked with getting to the goal object without running into any walls or any of the enemy tanks. On each turn, the player and all enemy tanks will 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 will start 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 will keep 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 we 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 will follow or chase the player to a fault. Most of the time (75%), each enemy tank will stupidly follow 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 will randomly choose 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’ll hit the major points, but we don’t have space to discuss every detail.

The tile sheet (tanks_sheet.png) we are going to use will look 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 will be 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 will cause any tank moving on it to be destroyed. Tile 30, the second to last tile on the sheet, will be 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 will be made up of the first eight green tank tiles. Each tile will be used to simulate the tank treads moving from tile to tile.

Enemy tiles

The enemy will be made up of the second eight blue tank tiles. These tiles will be used to animate the tank treads as it moves from tile to tile.

Our game code will store 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 will be loaded into an application scope Image instance and given the name tileSheet:

var tileSheet;

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

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

Next, we will examine the setup of the game playfield.

The Playfield

The game playfield will be 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 will be placed randomly onto the grid. The playField[] array will hold 15 row arrays each with 15 columns. This gives us 225 tiles that can be easily accessed with the simple playField[row][col] syntax.

Creating the board

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

Next, we randomly place all of the enemy tank tiles. Unlike the wall tiles, the tank tiles will not replace road tiles in the playField array. Instead, they will be placed into 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 will create another array called items.

The items array will also be 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 will only be used 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.

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

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

All the data about the playField will be 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 will need to know the maximum number of each type of tile. These will also be application scope variables:

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

The Player

The player and all of its current attributes will be 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 will move to next, after a successful key press in that direction.

player.nextCol

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

player.currentTile

The id of the current tile used to display the player from the playerTiles array.

player.rotation

The player starts pointed up, so this will be the 0 rotation. When the player moves in one of the four basic directions, this rotation will change and will be used to move the player in the direction it is facing.

player.speed

The number of pixels the player object will move 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 will be replaced on the playField by an explosion sprite. With dead set to true, it will not be 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, as 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 will have its own set of attributes that are very similar to those of the player. Like the player, each enemy will be 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;

There are a couple extra things worth pointing out in this code. The first is that each enemy object needs an attribute called moveComplete. This is used in the animateEnemy() game state function. When the entire enemy battalion has moved to its new location, the game will transition to the next game state. This is discussed in detail in the next section .

Also, notice that the new enemy objects are added to the enemy array, as well as to the items multidimensional array. This ensures that the player and the goal cannot be placed on to an enemy location. Once the enemy moves from its initial location, the playField array will still have 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 will be 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, since 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 will be rendered on top of the playfield when an enemy tank or the player’s hit attribute has been set to true. The explosion tiles will animate through a list of five tiles and then be 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 will 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);
const 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 will use a function reference state machine to run our current game state. The switchGameState() function will be used to transition to a new game state. Let’s begin by discussing this function briefly, and then moving through the rest of the game functions.

Note

We will 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-3). By now, you have seen most of the code and ideas used to create this game logic. We will break out the new ideas and code in the sections that follow.

GAME_STATE_INIT

This state loads in the assets we need for our game. We are loading in 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 of the game arrays and objects and then calls the createPlayField() function. The createPlayField() function creates the playField and enemy arrays for the new game, as well as sets the player object’s starting location. Once it has finished, it calls the renderPlayField() function a single time to display the initial board on the game screen.

Once this completes, the state machine is now ready to start the real game loop. This is done 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. Once the player has done so, the switch statement checks to see which arrow was pressed. Based on the direction pressed, the checkBounds() function is called.

Note

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 in 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 will be used to 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. Since 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 will change 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 been 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 will be set to true. If the player is on a wall tile, the player.hit will be set to true.

We then loop though all of 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 variable called directionsToTest.

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

In either case, it must check all of 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.

Once 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 them 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 will first check to see whether the player is dead, and then check to see whether he has won. That means that 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 using the “center of a tile” logic. This logic relies on making calculations once 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 used to determine 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 that 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 the playField 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 the 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 the 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 will render 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 the 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, then we draw the playField, and 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 the 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-3.

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 into the directionsToTest array.

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

    Note

    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 where an object blocks the direction the enemy tank wants to move in. This is 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 to see 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 to see whether it is less than the chanceRandomEnemyMovement value. If it is, it will ignore the directionsToTest array and simply try 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-3, you will find the entire set of code for this game.

Micro Tank Maze Complete Game Code

Example 9-3. Micro Tank Maze full source code listing

 <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH(EX3: Micro Tank Maze Game</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{
       theCanvas = document.getElementById("canvas");
       context = theCanvas.getContext("2d");
   }

   //application states
   const GAME_STATE_INIT = 0;
   const GAME_STATE_WAIT_FOR_LOAD = 10;
   const GAME_STATE_TITLE = 20;
   const GAME_STATE_NEW_GAME = 30;
   const GAME_STATE_WAIT_FOR_PLAYER_MOVE = 40;
   const GAME_STATE_ANIMATE_PLAYER = 50;
   const GAME_STATE_EVALUATE_PLAYER_MOVE = 60;
   const GAME_STATE_ENEMY_MOVE = 70;
   const GAME_STATE_ANIMATE_ENEMY = 80;
   const GAME_STATE_EVALUATE_ENEMY_MOVE = 90;
   const GAME_STATE_EVALUATE_OUTCOME = 100;
   const GAME_STATE_ANIMATE_EXPLODE = 110;
   const GAME_STATE_CHECK_FOR_GAME_OVER = 120;
   const GAME_STATE_PLAYER_WIN = 130;
   const GAME_STATE_PLAYER_LOSE = 140;
   const GAME_STATE_GAME_OVER = 150;

   var currentGameState = 0;
   var currentGameStateFunction = null;

   //loading
   var loadCount = 0;
   var itemsToLoad  = 1;

   //keyPresses
   var keyPressList = [];

   var tileSheet;

   var mapRows = 15;
   var mapCols = 15;

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

   //tiles
   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];

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

   var enemyMoveCompleteCount=0;

   //objects
   var player = {};
   var enemy = [];
   var explosions = [];

   //screens
   var screenStarted = false;
   var score = 0;
   var enemyScore = 10;
   var goalScore = 50;
   var highScore = 0;

   var chanceRandomEnemyMovement = 25;

   function runGame(){
      currentGameStateFunction();
   }

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

         case GAME_STATE_INIT:
            currentGameStateFunction = gameStateInit;
            break;
         case GAME_STATE_WAIT_FOR_LOAD:
            currentGameStateFunction = gameStateWaitForLoad;
            break;
         case GAME_STATE_TITLE:
             currentGameStateFunction = gameStateTitle;
             break;
         case GAME_STATE_NEW_GAME:
             currentGameStateFunction = gameStateNewGame;
             break;
         case GAME_STATE_WAIT_FOR_PLAYER_MOVE:
             currentGameStateFunction = gameStateWaitForPlayerMove;
             break;
         case GAME_STATE_ANIMATE_PLAYER:
             currentGameStateFunction = gameStateAnimatePlayer;
             break;
         case GAME_STATE_EVALUATE_PLAYER_MOVE:
             currentGameStateFunction = gameStateEvaluatePlayerMove;
             break;
         case GAME_STATE_ENEMY_MOVE:
             currentGameStateFunction = gameStateEnemyMove;
             break;
         case GAME_STATE_ANIMATE_ENEMY:
             currentGameStateFunction = gameStateAnimateEnemy;
             break;
         case GAME_STATE_EVALUATE_ENEMY_MOVE:
             currentGameStateFunction = gameStateEvaluateEnemyMove;
             break;
         case GAME_STATE_EVALUATE_OUTCOME:
            currentGameStateFunction = gameStateEvaluateOutcome;
            break;
         case GAME_STATE_ANIMATE_EXPLODE:
             currentGameStateFunction = gameStateAnimateExplode;
             break;
         case GAME_STATE_CHECK_FOR_GAME_OVER:
            currentGameStateFunction = gameStateCheckForGameOver;
            break;
         case GAME_STATE_PLAYER_WIN:
             currentGameStateFunction = gameStatePlayerWin;
             break;
         case GAME_STATE_PLAYER_LOSE:
             currentGameStateFunction = gameStatePlayerLose;
             break;

      }

   }

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

   function gameStateInit() {

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

      switchGameState(GAME_STATE_WAIT_FOR_LOAD);

   }

    function itemLoaded(event) {

      loadCount++;
      ////console.log("loading:" + loadCount)
      if (loadCount >= itemsToLoad) {

         switchGameState(GAME_STATE_TITLE)

      }

   }

   function gameStateTitle() {
      if (screenStarted !=true){
         fillBackground();
         setTextStyleTitle();
         context.fillText  ("Micro Tank Maze", 160, 70);
         context.fillText  ("Press Space To Play", 150, 140);

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

         }

      }

   }

   function gameStatePlayerWin(){

      if (!screenStarted){

         score += goalScore;
         fillBackground();
         setTextStyleTitle();
         context.fillText  ("YOU WON THE GAME!", 135, 70);
         context.fillText  ("Final Score: " + score, 150, 100);
         context.fillText  ("Number of enemy: " + enemyMax, 150,130);

         if (score > highScore){
            highScore = score;
            context.fillText  ("NEW HIGH SCORE!", 150,160);
         }

         context.fillText  ("High Score: " + score, 150, 190);

         screenStarted = true;

         enemyMax++;
         if (enemyMax >50){
            enemyMax = 50;
         }
         context.fillText  ("Number of enemy for next game: " +
           enemyMax, 100,220);

         context.fillText  ("Press Space To Play", 150, 300);

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

         }

      }

   }

   function gameStatePlayerLose(){
      if (!screenStarted){
         fillBackground();
         setTextStyleTitle();
         context.fillText  ("SORRY, YOU LOST THE GAME!", 100, 70);
         context.fillText  ("Final Score: " + score, 150, 100);
         context.fillText  ("Number of enemy: " + enemyMax, 150,130);

         if (score > highScore){
            highScore = score;
            context.fillText  ("NEW HIGH SCORE!", 150,160);
         }

         context.fillText  ("High Score: " + score, 150, 190);

         screenStarted = true;

         context.fillText  ("Number of enemy for next game: " + 
          enemyMax, 100,220);
         context.fillText  ("Press Space To Play", 150, 300);
      }else{
         //wait for space key click
         if (keyPressList[32]==true){
            //console.log("space pressed");
            switchGameState(GAME_STATE_NEW_GAME);
            screenStarted = false;

         }

      }

   }

   function gameStateNewGame(){
      score = 0;
      enemy = [];
      explosions = [];
      playField = [];
      items = [];
      resetPlayer();
      createPlayField();
      renderPlayField();

      switchGameState(GAME_STATE_WAIT_FOR_PLAYER_MOVE);

   }

   function createPlayField(){
      var wallCount = 0;
      var playerCount = 0;
      var enemyCount = 0;
      var goalCount = 0;
      var roadCount = 0;

      //fill with road
      for (var rowCtr=0;rowCtr<15;rowCtr++){
         var tempRow = [];
         for (colCtr=0;colCtr<15;colCtr++) {
            tempRow.push(roadTile)
         }

         playField.push(tempRow);

      }
      //console.log("playField=" + playField);

      //create items array
      for (rowCtr=0;rowCtr<15;rowCtr++){
         var tempRow = [];
         for (colCtr=0;colCtr<15;colCtr++) {
            tempRow.push(0);
         }

         items.push(tempRow);

      }

      var randRow;
      var randCol;
      //placewalls
      for (var wallCtr=0;wallCtr<wallMax;wallCtr++){
         var wallLocationFound = false;
         while(!wallLocationFound){
            randRow = Math.floor(Math.random()*15);
            randCol = Math.floor(Math.random()*15);
            if (playField[randRow][randCol]==roadTile){
               playField[randRow][randCol] = wallTile;
               wallLocationFound = true;
            }
         }
      }

      //place enemy
      for (var enemyCtr=0;enemyCtr<enemyMax;enemyCtr++){
         var enemyLocationFound = false;
         while(!enemyLocationFound){
            randRow = Math.floor(Math.random()*15);
            randCol = Math.floor(Math.random()*15);
            if (playField[randRow][randCol]==roadTile){
               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;
            }
         }
      }

      //place player
      var playerLocationFound = false;
      while(!playerLocationFound){
         randRow = Math.floor(Math.random()*15);
         randCol = Math.floor(Math.random()*15);
         if (playField[randRow][randCol]==roadTile && 
          items[randRow][randCol]==0){
            playerLocationFound = true;
            player.col = randCol;
            player.row = randRow;
            player.x = player.col*32;
            player.y = player.row*32;
            items[randRow][randCol] = 1;
         }
      }

      //place goal
      var goalLocationFound = false;
      while(!goalLocationFound){
         randRow = Math.floor(Math.random()*15);
         randCol = Math.floor(Math.random()*15);
         if (playField[randRow][randCol]==roadTile && 
          items[randRow][randCol]==0){
            playField[randRow][randCol] = goalTile;
            goalLocationFound = true;
         }
      }

      //console.log("playField=" + playField);

   }

   function resetPlayer(){
      player.row = 0;
      player.col = 0;
      player.nextRow = 0;
      player.nextCol = 0;
      player.currentTile = 0;
      player.rotation = 0;
      player.speed = 2;
      player.destinationX = 0;
      player.destinationY = 0;
      player.x = 0;
      player.y = 0;
      player.dx = 0;
      player.dy = 0;
      player.hit = false;
      player.dead = false;
      player.win = false;
   }

   function gameStateWaitForPlayerMove() {
      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();
         }
      }
   }

   function setPlayerDestination(){
      player.destinationX = player.nextCol*32;
      player.destinationY = player.nextRow*32;
      switchGameState(GAME_STATE_ANIMATE_PLAYER);
   }

   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);

      }

   }

   function gameStateAnimatePlayer(){
      player.x += player.dx*player.speed;
      player.y += player.dy*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);
      }

   }

   function gameStateEvaluatePlayerMove(){
      player.row = player.nextRow;
      player.col = player.nextCol;

      if (playField[player.row][player.col]==wallTile){
         player.hit = true;
      }else if (playField[player.row][player.col]==goalTile){
         player.win = true;
      }

      for (var eCtr=enemy.length-1;eCtr>=0;eCtr--){
         if (player.row==enemy[eCtr].row && player.col==enemy[eCtr].col){
            enemy[eCtr].hit = true;
            player.hit = true;
         }
      }

      switchGameState(GAME_STATE_ENEMY_MOVE);

   }

   function gameStateEnemyMove(){
      for (var eCtr=0;eCtr<enemy.length;eCtr++){
         var tempEnemy = enemy[eCtr];
         if (!tempEnemy.hit){

            var directionsToTest=[];
            var hDiff = tempEnemy.col - player.col;
            var vDiff = tempEnemy.row - player.row;

            if (Math.abs(vDiff) < Math.abs(hDiff)){
               if (vDiff > 0){
                  directionsToTest.push("up");
                  directionsToTest.push("down");
               }else if (vDiff <0){
                  directionsToTest.push("down");
                  directionsToTest.push("up");
               }

               if (hDiff >0){
                  directionsToTest.push("left");
                  directionsToTest.push("right");
               }else if (hDiff <0){
                  directionsToTest.push("right");
                  directionsToTest.push("left");
               }
            }else if (Math.abs(hDiff) < Math.abs(vDiff)) {
               if (hDiff >0){
                  directionsToTest.push("left");
                  directionsToTest.push("right");
               }else if (hDiff<0){
                  directionsToTest.push("right");
                  directionsToTest.push("left");
               }else if (vDiff > 0){
                  directionsToTest.push("up");
                  directionsToTest.push("down");
               }else if (vDiff <0){
                  directionsToTest.push("down");
                  directionsToTest.push("up");
               }
            }else if (Math.abs(hDiff) == Math.abs(vDiff)) {
               //make an educated random guess
               if (Math.floor(Math.random()*2)==0){
                  //try vertical first
                  if (vDiff >0){
                     directionsToTest.push("up");
                     directionsToTest.push("down");
                  }else if (vDiff<0){
                     directionsToTest.push("down");
                     directionsToTest.push("up");
                  }
               }else{
                  //try vertical first
                  if (hDiff >0){
                     directionsToTest.push("left");
                     directionsToTest.push("right");
                  }else if (hDiff<0){
                     directionsToTest.push("right");
                     directionsToTest.push("left");
                  }
               }
            }
            var chooseRandom = false;
            var moveFound = false;
            var movePtr = 0;
            var move;

            if (Math.floor(Math.random()*100)> chanceRandomEnemyMovement){

               //not random movement

               while(!moveFound){

                  move = directionsToTest[movePtr];

                  switch(move){
                     case "up":
                        if (checkBounds(-1,0,tempEnemy)){
                           moveFound = true;

                        }
                        break;

                     case "down":
                        if (checkBounds(1,0,tempEnemy)){
                           moveFound = true;

                        }
                        break;

                     case "left":
                        if (checkBounds(0,-1, tempEnemy)){
                           moveFound = true;

                        }
                        break;
                     case "right":
                        if (checkBounds(0,1,tempEnemy)){
                           moveFound = true;

                        }
                        break
                  }

                  movePtr++
                  if (movePtr==directionsToTest.length){
                     //do not move if no move found
                     //this should be impossible
                     chooseRandom = true;

                  }
               }

            }else{
               chooseRandom = true;
            }
               //pick random direction to test;
            if (chooseRandom) {

               while(!moveFound){
                  switch(Math.floor(Math.random()*4)){
                     case 0:
                        if (checkBounds(-1,0,tempEnemy)){
                           moveFound = true;

                        }else{

                        }
                        break;

                     case 1:
                        if (checkBounds(1,0,tempEnemy)){
                           moveFound = true;

                        }else{

                        }
                        break;

                     case 2:
                        if (checkBounds(0,-1, tempEnemy)){
                           moveFound = true;

                        }else{

                        }
                        break;
                     case 3:
                        if (checkBounds(0,1,tempEnemy)){
                           moveFound = true;

                        }else{

                        }
                        break
                  }
               }

            }


            tempEnemy.destinationX = tempEnemy.nextCol*32;
            tempEnemy.destinationY = tempEnemy.nextRow*32;


         }else{
            tempEnemy.nextCol = tempEnemy.col;
            tempEnemy.nextRow = tempEnemy.row;
            tempEnemy.destinationX = tempEnemy.nextCol*32;
            tempEnemy.destinationY = tempEnemy.nextRow*32;
         }


      }
      switchGameState(GAME_STATE_ANIMATE_ENEMY);
   }

   function gameStateAnimateEnemy(){
      for (var eCtr=enemy.length-1;eCtr>=0;eCtr--){
         var tempEnemy = enemy[eCtr];

         if (!tempEnemy.moveComplete){

            tempEnemy.x += tempEnemy.dx*tempEnemy.speed;
            tempEnemy.y += tempEnemy.dy*tempEnemy.speed;
            tempEnemy.currentTile++;
            if (tempEnemy.currentTile==enemyTiles.length){
               tempEnemy.currentTile = 0;
            }
            renderPlayField();
            if (tempEnemy.x==tempEnemy.destinationX &&
             tempEnemy.y==tempEnemy.destinationY){
               tempEnemy.moveComplete = true;
               enemyMoveCompleteCount++;
            }
         }
      }

      if (enemyMoveCompleteCount >= enemy.length){
         enemyMoveCompleteCount = 0;
         for (var eCtr=0;eCtr<enemy.length;eCtr++){
            var tempEnemy = enemy[eCtr];
            tempEnemy.moveComplete = false;
         }
         switchGameState(GAME_STATE_EVALUATE_ENEMY_MOVE);

      }

   }

   function gameStateEvaluateEnemyMove(){
      for (var eCtr=enemy.length-1;eCtr>=0;eCtr--){
         var tempEnemy = enemy[eCtr];
         tempEnemy.row = tempEnemy.nextRow;
         tempEnemy.col = tempEnemy.nextCol;

         if (playField[tempEnemy.row][tempEnemy.col]==wallTile){
            tempEnemy.hit = true;
         }

         if (player.row==tempEnemy.row && player.col==tempEnemy.col){
            tempEnemy.hit = true;
            player.hit = true;

         }

         //check against other enemy
         for (var eCtr2=enemy.length-1;eCtr2>=0;eCtr2--){
            var tempEnemy2 = enemy[eCtr2];

            if (tempEnemy.row==tempEnemy2.row && 
             tempEnemy.col==tempEnemy2.col && eCtr != eCtr2){
               tempEnemy.hit = true;
               tempEnemy2.hit = true;
            }

         }

      }
      switchGameState(GAME_STATE_EVALUATE_OUTCOME);
   }

   function gameStateEvaluateOutcome(){
      if (player.hit){
         player.dead = true;
         createExplode(player);
      }

      for (var eCtr=enemy.length-1;eCtr>=0;eCtr--){
         var tempEnemy = enemy[eCtr];
         if (tempEnemy.hit){
            score += enemyScore;
            tempEnemy.dead  = true;
            createExplode(tempEnemy)
            enemy.splice(eCtr,1);
            tempEnemy = null;
         }
      }

      switchGameState(GAME_STATE_ANIMATE_EXPLODE);
   }

   function createExplode(object){
      var newExplode = {};
      newExplode.currentTile = 0;
      newExplode.row = object.row;
      newExplode.col = object.com;
      newExplode.x = object.x;
      newExplode.y = object.y;
        newExplode.rotation = 0;
      explosions.push(newExplode);
   }


   function gameStateAnimateExplode(){
      for (var eCtr=explosions.length-1;eCtr>=0;eCtr--){
         var tempExplosion = explosions[eCtr];
         renderPlayField();
         tempExplosion.currentTile++;
         if (tempExplosion.currentTile == explodeTiles.length){
            explosions.splice(eCtr,1);
            tempExplode = null;
         }
      }

      if (explosions.length==0){
         switchGameState(GAME_STATE_CHECK_FOR_GAME_OVER);
      }
   }

   function gameStateCheckForGameOver() {
      if (player.dead){
         switchGameState(GAME_STATE_PLAYER_LOSE);
      }else if (player.win){
         switchGameState(GAME_STATE_PLAYER_WIN)
      }else{
         switchGameState(GAME_STATE_WAIT_FOR_PLAYER_MOVE);
      }
   }

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

   function drawPlayer(){
      if (!player.dead){
         context.save();

         context.setTransform(1,0,0,1,0,0);
         context.translate(player.x+16, player.y+16);
         var angleInRadians = player.rotation * Math.PI / 180;
         context.rotate(angleInRadians);

         var sourceX = Math.floor(playerTiles[player.currentTile] % 8) * 32;
         var sourceY = Math.floor(playerTiles[player.currentTile] /8) *32;

         context.drawImage(tileSheet, sourceX, sourceY,32,32,-16,-16,32,32);

         context.restore();
      }

   }

   function drawEnemy(){
      for (var eCtr=enemy.length-1;eCtr>=0;eCtr--){
         tempEnemy = enemy[eCtr];
         if (!tempEnemy.dead){
            context.save();

            context.setTransform(1,0,0,1,0,0);
            context.translate(tempEnemy.x+16, tempEnemy.y+16);
            var angleInRadians = tempEnemy.rotation * Math.PI / 180;
            context.rotate(angleInRadians);

            var sourceX = Math.floor(enemyTiles[tempEnemy.currentTile] % 8) * 32;
            var sourceY = Math.floor(enemyTiles[tempEnemy.currentTile] /8) *32;

            context.drawImage(tileSheet, sourceX, sourceY,32,32,-16,-16,32,32);

            context.restore();
         }
      }
   }

   function drawExplosions(){
      for (var eCtr=explosions.length-1;eCtr>=0;eCtr--){
         tempExplosion = explosions[eCtr];

         context.save();

         var sourceX = Math.floor(explodeTiles[tempExplosion.currentTile]
          % 8) * 32;
         var sourceY = Math.floor(explodeTiles[tempExplosion.currentTile] /8) *32;

         context.drawImage(tileSheet, sourceX, sourceY,32,32, 
          tempExplosion.x,tempExplosion.y,32,32);

         context.restore();

      }
   }

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

   }

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

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

   }

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

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

}

//***  new FrameRateCounter   object prototype

function FrameRateCounter(fps) {
   if (fps == undefined){
      this.fps = 40
   }else{
      this.fps = fps
   }
   this.lastFrameCount = 0;
   var dateTemp = new Date();

   this.frameLast = dateTemp.getTime();
   delete dateTemp;
   this.frameCtr = 0;
   this.lastTime = dateTemp.getTime();
   this.step = 1;
}
FrameRateCounter.prototype.countFrames=function() {

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

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

}

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

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