Chapter 8. Canvas Games: Part I

Games are the reason why many of us initially became interested in computers, and they continue to be a major driving force that pushes computer technology to new heights. In this chapter, we will examine how to build a mini game framework that can be used to create games on the canvas. We will explore many of the core building blocks associated with game development and apply them to HTML5 Canvas with the JavaScript API.

We don’t have the space to cover every type of game you might want to create, but we will discuss many elementary and intermediate topics necessary for most games. At the end of this chapter, we will have a basic clone of Atari’s classic Asteroids game. We will step through the creation of this game by first applying some of the techniques for drawing and transformations specific to our game’s visual objects. This will help get our feet wet by taking some of the techniques we covered in previous chapters and applying them to an arcade game application. Next, we will create a basic game framework that can be applied to any game we want to make on the canvas. Following this, we will dive into some game techniques and algorithms, and finally, we will apply everything we have covered to create the finished product.

Two-dimensional flying space shooter games are just the beginning of what can be accomplished on the Canvas. In the rest of the chapter, we will dig further into the 2D tile map structure we create in Chapter 4 and apply it to an application In doing so, we will take a look at using the A* path-finding algorithm to navigate the 2D tile map.

Why Games in HTML5?

Playing games in a browser has become one of the most popular activities for Internet users. HTML5 Canvas gives web developers an API to directly manage drawing to a specific area of the browser. This functionality makes game development in JavaScript much more powerful than ever before.

Canvas Compared to Flash

We’ve covered this topic in earlier chapters, but we expect that a large portion of readers might have previously developed games in Flash. If so, you will find that Canvas offers similar functionality in certain areas, but lacks some of the more refined features of Flash.

No Flash timeline

There is no frame-based timeline for animation intrinsic to Canvas. This means that we will need to code all of our animations using images and/or paths, and apply our own frame-based updates.

No display list

Flash AS3 offers the very powerful idea of an object display list; a developer can add hundreds of individual physical display objects to the game screen. HTML5 Canvas has only a single display object (the canvas itself).

What Does Canvas Offer?

Even though Canvas lacks some of the features that make the Flash platform very nice for game development, it also has some strengths.

A powerful single stage

HTML5 Canvas is closely akin to the Flash Stage. It is a rectangular piece of screen real estate that can be manipulated programmatically. Advanced Flash developers might recognize Canvas as a close cousin to both the BitmapData and Shape objects in ActionScript. We can draw directly to the canvas with paths and images and transform them on the fly.

Logical display objects

Canvas gives us a single physical display object, but we can create any number of logical display objects. We will use JavaScript objects to hold all of the logical data and methods we need to draw and transform our logical game objects to the physical canvas.

Our Basic Game HTML5 File

Before we start to develop our arcade game, let’s look at Example 8-1, the most basic HTML file we will use in this chapter (CH8EX1.html). We’ll start by using the basic HTML5 template we defined in Chapter 1. Our canvas will be a 200×200 square.

Example 8-1. The Basic HTML file for Chapter 8
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH8EX1: Filled Screen With Some Text</title>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasApp(){
var theCanvas = document.getElementById("canvas");
if (!theCanvas || !theCanvas.getContext) {
return;
}
var context = theCanvas.getContext("2d");
if (!context) {
return;
}
drawScreen();
function drawScreen() {
context.fillStyle = '#aaaaaa';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#000000';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Canvas!", 0, 0);
}
}
</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="200" height="200">
Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

This example will do nothing more than place a 200×200 gray box on the canvas and write “Canvas!” starting at 0,0. We will be replacing the drawScreen() function for most of the next few examples. Figure 8-1 illustrates Example 8-1.

Next, we will begin to make our Asteroids-like game, which we’ve named Geo Blaster Basic. See Figure 8-7 for an example of the final game in action.

Our Game’s Design

We are not going to assume that everyone who reads this chapter knows of or understands Atari’s classic arcade game Asteroids. So let’s start by taking a peek at the Asteroids game-play elements.

Asteroids, designed by Ed Logg and Lyle Rains, was released by Atari in 1979. The game pitted a lone triangular two-dimensional vector spaceship (the player ship) against screen after screen of asteroid rocks that needed to be dodged and destroyed. Every so often a space saucer would enter the screen attempting to destroy the player ship.

All asteroids started the game as large rocks; when they were hit, they would split into two medium-sized rocks. When hit by a player missile, these medium-sized rocks would then split into two small rocks. The small rocks would simply be destroyed when hit. (Small was the final size for all asteroids.)

When the player destroyed all the asteroids, a new screen of more and slightly faster asteroids would appear. This went on until the player exhausted his three ships. At each 10,000-point score mark, the player was awarded an extra ship.

All of the game objects moved (thrusting, rotating, and/or floating) freely across the entire screen, which represented a slice of space as a flat plane. When an object went off the side of the screen, it would reappear on the opposite side, in warp-like fashion.

Game Graphics: Drawing with Paths

Let’s jump into game development on Canvas by first taking a look at some of the graphics we will use in our game. This will help give us a visual feel for what type of code we will need to implement.

Needed Assets

For our Asteroids-like game, Geo Blaster Basic, we will need some very simple game graphics, including:

• A solid black background.

• A player ship that will rotate and thrust (move on a vector) across the game screen. There will be two frames of animation for this ship: a “static” frame and a “thrust” frame.

• A saucer that flies across the screen and shoots at the player.

• Some “rocks” for the player to shoot. We will use a simple square as our rock.

There are two different methods we can employ to draw the graphics for our game: bitmap images or paths. For the game in this chapter, we will focus on using paths. In Chapter 9, we will explore how to manipulate bitmap images for our game graphics.

Using Paths to Draw the Game’s Main Character

Paths offer us a very simple but powerful way to mimic the vector look of the classic Asteroids game. We could use bitmap images for this purpose, but in this chapter we are going to focus on creating our game in code with no external assets. Let’s take a look at the two frames of animation we will create for our player ship.

The static player ship (frame 1)

The main frame of the player ship will be drawn with paths on a 20×20 grid, as shown in Figure 8-2.

Using the basic HTML file presented in Example 8-1, we can simply swap the drawScreen() function with the code in Example 8-2 to draw the ship.

Example 8-2. Drawing the player ship
function drawScreen() {
// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - Static", 0, 180);

//drawShip
context.strokeStyle = '#ffffff';
context.beginPath();
context.moveTo(10,0);
context.lineTo(19,19);
context.lineTo(10,9);
context.moveTo(9,9);
context.lineTo(0,19);
context.lineTo(9,0);

context.stroke();
context.closePath();
}

We are drawing to the upper-left corner of the screen, starting at 0,0. Figure 8-3 shows what this will look like.

The ship with thrust engaged (frame 2)

Now let’s take a look at the second frame of animation for the player ship, which is shown in Figure 8-4.

The drawScreen() function code to add this extra “thrust” graphic is very simple; see Example 8-3.

Example 8-3. Drawing the player ship with thrust
function drawScreen() {
// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - Thrust", 0, 180);

//drawShip
context.strokeStyle = '#ffffff';
context.beginPath();
context.moveTo(10,0);
context.lineTo(19,19);
context.lineTo(10,9);
context.moveTo(9,9);
context.lineTo(0,19);
context.lineTo(9,0);

//draw thrust
context.moveTo(8,13);
context.lineTo(11,13);
context.moveTo(9,14);
context.lineTo(9,18);
context.moveTo(10,14);
context.lineTo(10,18);

context.stroke();
context.closePath();
}

Animating on the Canvas

The player ship we just created has two frames (static and thrust), but we can display only a single frame at a time. Our game will need to switch out the frame of animation based on the state of the player ship, and it will need to run on a timer so that this animation can occur. Let’s take a quick look at the code necessary to create our game timer.

Game Timer Loop

Games on HTML5 Canvas require the use of the repeated update/render loop to simulate animation. We do this by using the setTimeout() JavaScript function, which will repeatedly call a function of our choosing at millisecond intervals. Each second of game/animation time is made up of 1,000 milliseconds. If we want our game to run at 30 update/render cycles per second, we call this a 30 frames per second (FPS) rate. To run our interval at 30 FPS, we first need to divide 1,000 by 30. The result is the number of milliseconds in each interval:

const FRAME_RATE = 30;
var intervalTime = 1000/FRAME_RATE;

gameLoop()

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

By calling the drawScreen() function repeatedly on each interval time-out, we can simulate animation.

Sometimes we will refer to each of the frame intervals as a frame tick.

The Player Ship State Changes

We simply need to switch between the static and thrust states to simulate the animation. Let’s take a look at the full HTML file to do this. In Example 8-4, we will start to place canvasApp class-level variables in a new section just above the drawScreen() function. This will be the location going forward for all variables needing a global scope inside the canvasApp() object.

Example 8-4. The player ship state changes for thrust animation
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH8EX4: Ship Animation Loop</title>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

canvasApp();
}

function canvasApp(){

var theCanvas = document.getElementById("canvas");
if (!theCanvas || !theCanvas.getContext) {
return;
}

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

if (!context) {
return;
}

//canvasApp level variables
var shipState = 0; //0 = static, 1 = thrust

function drawScreen() {
//update the shipState
shipState++;
if (shipState >1) {
shipState=0;
}

// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - animate", 0, 180);

//drawShip
context.strokeStyle = '#ffffff';
context.beginPath();
context.moveTo(10,0);
context.lineTo(19,19);
context.lineTo(10,9);
context.moveTo(9,9);
context.lineTo(0,19);
context.lineTo(9,0);

if (shipState==1) {
//draw thrust
context.moveTo(8,13);
context.lineTo(11,13);
context.moveTo(9,14);
context.lineTo(9,18);
context.moveTo(10,14);
context.lineTo(10,18);
}

context.stroke();
context.closePath();
}

var FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;

gameLoop();

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

}

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

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

When we run Example 8-4, we will see the player ship in the upper-left corner of the canvas. The static and thrust states will alternate on each frame.

Applying Transformations to Game Graphics

Our game will probably have many individual logical display objects that need to be updated on a single frame tick. We can make use of the Canvas stack (save() and restore() functions) and use the transformation matrix to ensure that the final output affects only the current object we are working on—not the entire canvas.

The Canvas Stack

The Canvas state can be saved to a stack and retrieved. This is important when we are transforming and animating game objects because we want our transformations to affect only the current game object and not the entire canvas. The basic workflow for using the Canvas stack in a game looks like this:

1. Save the current canvas to the stack.

2. Transform and draw the game object.

3. Retrieve the saved canvas from the stack.

As an example, let’s set up a basic rotation for our player ship. We will rotate it by 1 degree on each frame. Because we are currently drawing the player ship in the top-left corner of the canvas, we are going to move it to a new location. We do this because the basic rotation will use the top-left corner of the ship as the registration point: the axis location used for rotation and scale operations. Therefore, if we kept the ship at the 0,0 location and rotated it by its top-left corner, you would not see it half the time because its location would be off the top and left edges of the canvas. Instead, we will place the ship at 50,50.

We will be using the same HTML code as in Example 8-4, changing out only the drawCanvas() function. To simplify this example, we will remove the shipState variable and concentrate on the static state only. We will be adding in three new variables above the drawCanvas() function:

var rotation = 0; - holds the current rotation of the player ship
var x = 50; - holds the x location to start drawing the player ship
var y = 50; - holds the y location to start drawing the player ship

Example 8-5 gives the full code.

Example 8-5. Rotating an image
//canvasApp level variables
var rotation = 0;
var x = 50;
var y = 50;

function drawScreen() {

// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - rotate", 0, 180);

//transformation
var angleInRadians = rotation * Math.PI / 180;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity

//translate the canvas origin to the center of the player
context.translate(x,y);
context.rotate(angleInRadians);

//drawShip
context.strokeStyle = '#ffffff';
context.beginPath();
context.moveTo(10,0);
context.lineTo(19,19);
context.lineTo(10,9);
context.moveTo(9,9);
context.lineTo(0,19);
context.lineTo(9,0);

context.stroke();
context.closePath();

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

//add to rotation
rotation++;
}

As you can see, the player ship rotates clockwise one degree at a time. As we’ve mentioned many times already, we must convert from degrees to radians because the context.rotate() transformations use radians for calculations. In the next section, we’ll take a deeper look at some of the transformations we will use in our Geo Blaster Basic game.

Game Graphic Transformations

As we saw in the previous section, we can easily rotate a game graphic at the top-left corner by using the context.rotate() transformation. However, our game will need to rotate objects at the center rather than the top-left corner. To do this, we must change the transformation point to the center of our game graphic object.

Rotating the Player Ship from the Center

The code to rotate the player ship from its center point is almost exactly like the code used to rotate it at the top-left corner. What we need to modify is the point of the translation. In Example 8-5, we placed the immediate-mode drawing context at the x and y coordinates of our game object (50,50). This had the effect of rotating the object from the top-left corner. Now we must move the translation to the center of our object:

context.translate(x+.5*width,y+.5*height);

The width and height variables represent attributes of our drawn player ship. We will create these attributes in Example 8-6.

This is not the only change we need to make; we also need to draw our ship as though it is the center point. To do this, we will subtract half the width from each x attribute in our path draw sequence, and we will subtract half the height from each y attribute:

context.moveTo(10-.5*width,0-.5*height);
context.lineTo(19-.5*width,19-.5*height);

As you can see, it might get a little confusing trying to draw coordinates in this manner. It is also slightly more processor-intensive than using constants. In that case, we would simply hardcode in the needed values. Remember, the width and height attributes of our ship are both 20. The hardcoded version would look something like this:

context.moveTo(0,−10);  //10-10, 0-10
context.lineTo(9,9); //19-10, 19-10

The method where we use the calculated values (using the width and height variables) is much more flexible, while the hardcoded method is much less processor-intensive. Example 8-6 contains all the code to use either method. We have commented out the calculated version of the code.

Example 8-6. Rotating an image from its center point
//canvasApp level variables
var rotation = 0;
var x = 50;
var y = 50;   var width = 20;
var height = 20;

function drawScreen() {
// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - rotate", 0, 180);

//transformation
var angleInRadians = rotation * Math.PI / 180;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity

//translate the canvas origin to the center of the player
context.translate(x+.5*width,y+.5*height);
context.rotate(angleInRadians);

//drawShip

context.strokeStyle = '#ffffff';
context.beginPath();

//hardcoding in locations
context.moveTo(0,-10);
context.lineTo(9,9);
context.lineTo(0,-1);
context.moveTo(-1,-1);
context.lineTo(-10,9);
context.lineTo(-1,-10);

/*
      //using the width and height to calculate
      context.moveTo(10-.5*width,0-.5*height);
      context.lineTo(19-.5*width,19-.5*height);
      context.lineTo(10-.5*width,9-.5*height);
      context.moveTo(9-.5*width,9-.5*height);
      context.lineTo(0-.5*width,19-.5*height);
      context.lineTo(9-.5*width,0-.5*height);
      */

context.stroke();
context.closePath();

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

//add to rotation
rotation++;
}

When a new player ship in Geo Blaster Basic enters the game screen, we will have it fade from transparent to opaque. Example 8-7 shows how we will create this transformation in our game.

To use the context.globalAlpha attribute of the canvas, we simply set it to a number between 0 and 1 before we draw the game graphics. We will create a new variable in our code called alpha, which will hold the current alpha value for our player ship. We will increase it by .01 until it reaches 1. When we actually create our game, we will stop it at 1 and then start the game level. However, for this demo, we will just repeat it over and over.

Example 8-7. Alpha fading to the player ship
//canvasApp level variables
var x = 50;
var y = 50;
var width = 20;
var height = 20;
var alpha = 0;
context.globalAlpha  = 1;

function drawScreen() {

context.globalAlpha = 1;
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - alpha", 0, 180);
context.globalAlpha = alpha;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity

//translate the canvas origin to the center of the player
context.translate(x+.5*width,y+.5*height);

//drawShip
context.strokeStyle = '#ffffff';
context.beginPath();

//hardcoding in locations
context.moveTo(0,-10);
context.lineTo(9,9);
context.lineTo(0,-1);
context.moveTo(-1,-1);
context.lineTo(-10,9);
context.lineTo(-1,-10);

context.stroke();
context.closePath();

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

//add to rotation
alpha+=.01;
if (alpha > 1) {
alpha=0;
}
}

Game Object Physics and Animation

All of our game objects will move on a two-dimensional plane. We will use basic directional movement vectors to calculate the change in the x and y coordinates for each game object. At its very basic level, we will be updating the delta x (dx) and delta y (dy) of each of our game objects on each frame to simulate movement. These dx and dy values will be based on the angle and direction in which we want the object to move. All of our logical display objects will add their respective dx and dy values to their x and y values on each frame of animation. The player ship will not use strict dx and dy because it needs to be able to float and turn independently. Let’s take a closer look at the player movement now.

How Our Player Ship Will Move

Our player ship will change its angle of center axis rotation when the game player presses the left and right arrow keys. When the game player presses the up arrow key, the player ship will accelerate (thrust) in the angle it is facing. Because there is no friction applied to the ship, it will continue to float in the current accelerated angle until a different angle of acceleration is applied. This happens when the game player rotates to a new angle and presses the up (thrust) key once again.

The difference between facing and moving

Our player ship can rotate the direction it is facing while it is moving in a different direction. For this reason, we cannot simply use classic dx and dy values to represent the movement vector on the x and y axes. We must keep both sets of values for the ship at all times. When the player rotates the ship but does not thrust it, we need to draw the ship in the new rotated angle. All missile projectiles that the ship fires must also move in the direction the ship is facing. On the x-axis, we will name this value facingX; on the y-axis, it’s facingY. movingX and movingY values will handle moving the ship in the direction it was pointed in when the thrust was applied. All four values are needed to thrust the ship in a new direction. Let’s take a look at this next.

Thrusting in the rotated direction

After the ship is rotated to the desired direction, the player can thrust it forward by pressing the up arrow key. This thrust will accelerate the player ship only while the key is pressed. Because we know the rotation of the ship, we can easily calculate the angle of the rotation. We will then add new movingX and movingY values to the ship’s x and y attributes to move it forward.

First, we must change the rotation value from degrees to radians:

var angleInRadians = rotation * Math.PI / 180;

You have seen this before—it’s identical to how we calculated the rotation transformation before it was applied to the player ship.

When we have the angle of the ship’s rotation, we must calculate the facingX and facingY values for this current direction. We do this only when we are going to thrust because it is an expensive calculation, processor-wise. We could calculate these each time the player changes the ship’s rotation, but doing so would add unnecessary processor overhead:

facingX = Math.cos(angleInRadians);
facingY = Math.sin(angleInRadians);

When we have values on the x and y axes that represent the direction the player ship is currently facing, we can calculate the new movingX and movingY values for the player:

movingX = movingX+thrustAcceleration*facingX;
movingY = movingY+thrustAcceleration*facingY;

To apply these new values to the player ship’s current position, we need to add them to its current x and y positions. This does not occur only when the player presses the up key. If it did, the player ship would not float; it would move only when the key was pressed. We must modify the x and y values on each frame with the movingX and movingY values:

x = x+movingX;
y = y+movingY;

Redrawing the player ship to start at angle 0

As you might recall, when we first drew the image for our player ship, we had the pointed end (the top) of the ship pointing up. We did this for ease of drawing, but it’s not really the best direction in which to draw our ship when we intend to apply calculations for rotational thrust. The pointing-up direction is actually the −90 (or 270) degree angle. If we want to leave everything the way it currently is, we will need to modify the angleInRadians calculation to look like this:

var angleInRadians = (Math.PI * (player.rotation −90 ))/ 180;

This is some ugly code, but it works fine if we want our player ship to be pointing up before we apply rotation transformations. A better method is to keep the current angleInRadians calculation but draw the ship pointing in the actual angle 0 direction (to the right). Figure 8-5 shows how we would draw this.

The drawing code for this direction would be modified to look like this:

//facing right
context.moveTo(−10,−10);
context.lineTo(10,0);
context.moveTo(10,1);
context.lineTo(−10,10);
context.lineTo(1,1);
context.moveTo(1,−1);
context.lineTo(−10,−10);

Controlling the Player Ship with the Keyboard

We will add in two keyboard events and an array object to hold the state of each key press. This will allow the player to hold down a key and have it repeat without a pause. Arcade games require this type of key-press response.

The array to hold our key presses

An array will hold the true or false value for each keyCode associated with key events. The keyCode will be the index of the array that will receive the true or false value:

var keyPressList = [];

The key events

We will use separate events for both key down and key up. The key down event will put a true value in the keyPressList array at the index associated with the event’s keyCode. Conversely, the key up event will place a false value in that array index:

document.onkeydown = function(e){

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

document.onkeyup = function(e){

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

Evaluating key presses

Our game will need to include code to look for true (or false) values in the keyPressList array and use those values to apply game logic:

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

movingX = movingX+thrustAcceleration*facingX;
movingY = movingY+thrustAcceleration*facingY;
}

if (keyPressList[37]==true) {
//rotate counterclockwise
rotation-=rotationalVelocity;
}

if (keyPressList[39]==true) {
//rotate clockwise
rotation+=rotationalVelocity;;
}

Let’s add this code to our current set of rotation examples and test it out. We have made some major changes, so Example 8-8 presents the entire HTML file once again.

Example 8-8. Controlling the player ship
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH8EX8: Ship Turn With Keys</title>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

canvasApp();

}

function canvasApp(){

var theCanvas = document.getElementById("canvas");
if (!theCanvas || !theCanvas.getContext) {
return;
}

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

if (!context) {
return;
}

//canvasApp level variables

var rotation = 0;
var x = 50;
var y = 50;
var facingX = 0;
var facingY = 0;
var movingX = 0;
var movingY = 0;
var width = 20;
var height = 20;
var rotationalVelocity = 5; //how many degrees to turn the ship
var thrustAcceleration = .03;
var keyPressList = [];
function drawScreen() {

//check keys

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

movingX = movingX+thrustAcceleration*facingX;
movingY = movingY+thrustAcceleration*facingY;

}

if (keyPressList[37]==true) {
//rotate counterclockwise
rotation -= rotationalVelocity;
}

if (keyPressList[39]==true) {
//rotate clockwise
rotation += rotationalVelocity;;
}   x = x+movingX;
y = y+movingY;

// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Player Ship - key turn", 0, 180);

//transformation
var angleInRadians = rotation * Math.PI / 180;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity

//translate the canvas origin to the center of the player
context.translate(x+.5*width,y+.5*height);
context.rotate(angleInRadians);

//drawShip

context.strokeStyle = '#ffffff';
context.beginPath();

//hardcoding in locations
//facing right
context.moveTo(-10,-10);
context.lineTo(10,0);
context.moveTo(10,1);
context.lineTo(-10,10);
context.lineTo(1,1);
context.moveTo(1,-1);
context.lineTo(-10,-10);

context.stroke();
context.closePath();

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

var FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;
gameLoop();

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

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

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

}

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

When this file is run in a browser, you should be able to press the left and right keys to rotate the ship on its center axis. If you press the up key, the ship will move in the direction it is facing.

Giving the Player Ship a Maximum Velocity

If you play with the code in Example 8-8, you will notice two problems:

1. The ship can go off the sides of the screen and get lost.

2. The ship has no maximum speed.

We’ll resolve the first issue when we start to code the complete game, but for now, let’s look at how to apply a maximum velocity to our current movement code. Suppose we give our player ship a maximum acceleration of two pixels per frame. It’s easy to calculate the current velocity if we are moving in only the four primary directions: up, down, right, left. When we are moving left or right, the movingY value will always be 0. If we are moving up or down, the movingX value will always be 0. The current velocity we are moving on one axis would be easy to compare to the maximum velocity.

But in our game, we are almost always moving in the x and y directions at the same time. To calculate the current velocity and compare it to a maximum velocity, we must use a bit more math.

First, let’s assume that we will add a maximum velocity variable to our game:

var maxVelocity = 2;

Next, we must make sure to calculate and compare the maxVelocity to the current velocity before we calculate the new movingX and movingY values. We will do this with local variables used to store the new values for movingX and movingY before they are applied:

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

The current velocity of our ship is the square root of movingXNew^2 + movingYNew^2:

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

If the currentVelocity is less than the maxVelocity, we set the movingX and movingY values:

if (currentVelocity < maxVelocity) {
movingX = movingXNew;
movingY = movingYNew;
}

A Basic Game Framework

Now that we have gotten our feet wet (so to speak) by taking a peek at some of the graphics, transformations, and basic physics we will use in our game, let’s look at how we will structure a simple framework for all games that we might want to create on HTML5 Canvas. We will begin by creating a simple state machine using constant variables. Next, we will introduce our game timer interval function to this structure, and finally, we will create a simple reusable object that will display the current frame rate our game is running in. Let’s get started.

The Game State Machine

A state machine is a programming construct that allows for our game to be in only a single application state at any one time. We will create a state machine for our game, called application state, which will include seven basic states (we will use constants to refer to these states):

• GAME_STATE_TITLE

• GAME_STATE_NEW_GAME

• GAME_STATE_NEW_LEVEL

• GAME_STATE_PLAYER_START

• GAME_STATE_PLAY_LEVEL

• GAME_STATE_PLAYER_DIE

• GAME_STATE_GAME_OVER

We will create a function object for each state that will contain game logic necessary for the state to function and to change to a new state when appropriate. By doing this, we can use the same structure for each game we create by simply changing out the content of each state function (as we will refer to them).

Let’s take a look at a very basic version of this in action. We will use a function reference variable called currentGameStateFunction, as well as an integer variable called currentGameState that will hold the current application state constant value:

var currentGameState = 0;
var currentGameStateFunction = null;

We will create a function called switchAppState() that will be called only when we want to switch to a new state:

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

case GAME_STATE_TITLE:
currentGameStateFunction = gameStateTitle;
break;

case GAME_STATE_PLAY_LEVEL:
currentGameStateFunctionappStatePlayeLevel;
break;

case GAME_STATE_GAME_OVER:
currentGameStateFunction = gameStateGameOver;
break;

}

}

The gameLoop() function call will start the application by triggering the iteration of the runGame() function by use of the setTimeout() method. We will call the runGame() function repeatedly in this setTimeout() method. runGame() will call the currentGameStateFunction reference variable on each frame tick. This allows us to easily change the function called by runGame() based on changes in the application state:

    gameLoop();

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

function runGame(){
currentGameStateFunction();
}

Let’s look at the complete code. We will create some shell functions for the various application state functions. Before the application starts, we will call the switchGameState() function and pass in the constant value for the new function we want as the currentGameStateFunction:

//*** application start
switchGameState(GAME_STATE_TITLE);

In Example 8-9, we will use the GAME_STATE_TITLE state to draw a simple title screen that will be redrawn on each frame tick.

Example 8-9. The tile screen state
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

canvasApp();

}

function canvasApp(){

var theCanvas = document.getElementById("canvas");
if (!theCanvas || !theCanvas.getContext) {
return;
}

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

if (!context) {
return;
}

//application states

const GAME_STATE_TITLE = 0;
const GAME_STATE_NEW_LEVEL = 1;
const GAME_STATE_GAME_OVER = 2;

var currentGameState = 0;
var currentGameStateFunction = null;

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

case GAME_STATE_TITLE:
currentGameStateFunction = gameStateTitle;
break;

case GAME_STATE_PLAY_LEVEL:
currentGameStateFunctionappStatePlayeLevel;
break;

case GAME_STATE_GAME_OVER:
currentGameStateFunction = gameStateGameOver;
break;

}

}

function gameStateTitle() {
ConsoleLog.log("appStateTitle");
// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("Title Screen", 50, 90);

}

function gameStatePlayLevel() {
ConsoleLog.log("appStateGamePlay");
}

function gameStateGameOver() {
ConsoleLog.log("appStateGameOver");
}

function runGame(){
currentGameStateFunction();
}

//*** application start
switchGameState(GAME_STATE_TITLE);

//**** application loop
var FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;
gameLoop();

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

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

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

}

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

//*** end console log object

</script>

Example 8-9 added in the ConsoleLog object from the previous chapters. We will continue to use this utility to create helpful debug messages in the JavaScript log window of the browser. This was added for browsers that crashed when no console was turned on. However, this is a rare occurrence in most browsers that support Canvas.

We will continue to explore the application state machine and then create one for our game logic states in the upcoming section, “Putting It All Together”.

The Update/Render (Repeat) Cycle

In any of our application states, we might need to employ animation and screen updates. We will handle these updates by separating our code into distinct update() and render() operations. For example, as you might recall, the player ship can move around the game screen, and when the player presses the up arrow key, the ship’s thrust frame of animation will be displayed rather than its static frame. In the previous examples, we contained all the code that updates the properties of the ship, as well as the code that actually draws the ship, in a single function called drawScreen(). Starting with Example 8-10, we will rid ourselves of this simple drawScreen() function and instead employ update() and render() functions separately. We will also separate out the code that checks for the game-specific key presses into a checkKeys() function.

Let’s reexamine the contents of the drawScreen() function from Example 8-8, but this time, we’ll break the function up into separate functions for each set of tasks, as shown in Example 8-10.

Example 8-10. Splitting the update and render cycles
function gameStatePlayLevel() {
checkKeys();
update();
render();
}

function checkKeys() {

//check keys

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

movingX = movingX+thrustAcceleration*facingX;
movingY = movingY+thrustAcceleration*facingY;

}

if (keyPressList[37]==true) {
//rotate counterclockwise
rotation−=rotationalVelocity;
}

if (keyPressList[39]==true) {
//rotate clockwise
rotation+=rotationalVelocity;;
}
}

function update() {
x = x+movingX;
y = y+movingY;
}

function render() {
//draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("render/update", 0, 180);

//transformation
var angleInRadians = rotation * Math.PI / 180;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity

//translate the canvas origin to the center of the player
context.translate(x+.5*width,y+.5*height);
context.rotate(angleInRadians);

//drawShip

context.strokeStyle = '#ffffff';
context.beginPath();

//hardcoding in locations
//facing right
context.moveTo(−10,−10);
context.lineTo(10,0);
context.moveTo(10,1);
context.lineTo(−10,10);
context.lineTo(1,1);
context.moveTo(1,−1);
context.lineTo(−10,−10);

context.stroke();
context.closePath();

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

const FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;
gameLoop();

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

We left out the entire application state machine from Example 8-9 to save space. In Example 8-10, we are simply showing what the gameStatePlayLevel() function might look like.

In the section “Putting It All Together”, we will go into this in greater detail as we start to build out the entire application.

The FrameRateCounter Object Prototype

Arcade games such as Asteroids and Geo Blaster Basic rely on fast processing and screen updates to ensure that all game-object rendering and game-play logic are delivered to the player at a reliable rate. One way to tell whether your game is performing up to par is to employ the use of a frame rate per second (FPS) counter. Below is a simple one that can be reused in any game you create on the canvas:

//*** FrameRateCounter  object prototype
function FrameRateCounter() {

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

FrameRateCounter.prototype.countFrames=function() {
var dateTemp = new Date();
this.frameCtr++;

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

delete dateTemp;
}

Our game will create an instance of this object and call the countFrames() function on each frame tick in our update() function. We will write out the current frame rate in our render() function.

Example 8-11 shows these functions by adding code to Example 8-10. Make sure you add the definition of the FrameRateCounter prototype object to the code in Example 8-10 under the canvasApp() function but before the final <script> tag. Alternatively, you can place it in its own <script\> tags or in a separate .js file and set the URL as the src= value of a <script> tag. For simplicity’s sake, we will keep all our code in a single file.

Example 8-11 contains the definition for our FrameRateCounter object prototype, as well as the code changes to Example 8-10 that are necessary to implement it.

Example 8-11. The FrameRateCounter is added
function update() {
x = x+movingX;
y = y+movingY;
frameRateCounter.countFrames();
}

function render() {
// draw background and text
context.fillStyle = '#000000';
context.fillRect(0, 0, 200, 200);
context.fillStyle = '#ffffff';
context.font = '20px sans-serif';
context.textBaseline = 'top';
context.fillText  ("FPS:" + frameRateCounter.lastFrameCount, 0, 180);

//...Leave everything else from Example 8-10 intact here
}

frameRateCounter = new FrameRateCounter();
const FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;
gameLoop();

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

Putting It All Together

We are now ready to start coding our game. First, we will look at the structure of the game and some of the ideas behind the various algorithms we will employ to create it. After that, we will present the full source code for Geo Blaster Basic.

Geo Blaster Game Structure

The structure of the game application is very similar to the structure we started to build earlier in this chapter. Let’s take a closer look at the state functions and how they will work together.

Game application states

Our game will have seven distinct game application states. We will store these in constants:

const GAME_STATE_TITLE = 0;
const GAME_STATE_NEW_GAME = 1;
const GAME_STATE_NEW_LEVEL = 2;
const GAME_STATE_PLAYER_START = 3;
const GAME_STATE_PLAY_LEVEL = 4;
const GAME_STATE_PLAYER_DIE = 5;
const GAME_STATE_GAME_OVER = 6;

Game application state functions

Each individual state will have an associated function that will be called on each frame tick. Let’s look at the functionality for each:

gameStateTitle()

Displays the title screen text and waits for the space bar to be pressed before the game starts.

gameStateNewGame()

Sets up all the defaults for a new game. All of the arrays for holding display objects are reinitialized—the game level is reset to 0, and the game score is set to 0.

gameStateNewLevel()

Increases the level value by one and then sets the “game knob” values to control the level difficulty. See the upcoming section “Level Knobs” for details.

gameStatePlayerStart()

Fades the player graphic onto the screen from 0 alpha to 1. When this is complete, level play will start.

gameStatePlayLevel()

Controls the play of the game level. It calls the update() and render() functions, as well as the functions for evaluating keyboard input for player ship control.

gameStatePlayerDie()

Starts up an explosion at the location where the player ship was when it was hit by a rock, saucer, or saucer missile. When the explosion is complete (all particles in the explosion have exhausted their individual life values), it sets the move to the GAME_STATE_PLAYER_START state.

gameStateGameOver()

Displays the “Game Over” screen and starts a new game when the space bar is pressed.

Game application functions

Aside from the game application state functions, there are a number of functions we need for the game to run. Each state function will call these functions as needed:

resetPlayer()

Resets the player to the center of the game screen and readies it for game play.

checkForExtraShip()

Checks to see whether the player should be awarded an extra ship. See the section “Awarding the Player Extra Ships” for details about this algorithm.

checkForEndOfLevel()

Checks to see whether all the rocks have been destroyed on a given level and, if so, starts up a new level. See the section “Level and Game End” for details about this algorithm.

fillBackground()

Fills the canvas with the background color on each frame tick.

setTextStyle()

Sets the base text style before text is written to the game screen.

renderScoreBoard()

Is called on each frame tick. It displays the updated score, number of ships remaining, and the current FPS for the game.

checkKeys()

Checks the keyPressList array and then modifies the player ship attributes based on the values found to be true.

update()

Is called from GAME_STATE_PLAY_LEVEL. It in turn calls the update() function for each individual display object array.

Individual display object update() functions

The unique functions listed below update each different type of display object. These functions (with the exception of updatePlayer()) will loop through the respective array of objects associated with its type of display object and update the x and y values with dx and dy values. The updateSaucer() function contains the logic necessary to check whether to create a new saucer and whether any current saucers on the screen should fire a missile at the player.

• updatePlayer()

• updatePlayerMissiles()

• updateRocks()

• updateSaucers()

• updateSaucerMissiles()

• updateParticles()

render()

Is called from GAME_STATE_PLAY_LEVEL. It in turn calls the render() function for each individual display object array.

Individual display object render() functions

Like the update() functions, the unique functions listed below render each different type of display object. Again, with the exception of the renderPlayer() object (because there is only a single player ship), each of these functions will loop through the array of objects associated with its type and draw them to the game screen. As we saw when drawing the player ship earlier in this chapter, we will draw each object by moving and translating the canvas to the point at which we want to draw our logical object. We will then transform our object (if necessary) and paint the paths to the game screen.

• renderPlayer()

• renderPlayerMissiles()

• renderRocks()

• renderSaucers()

• renderSaucerMissiles()

• renderParticles()

checkCollisions()

Loops through the individual game display objects and checks them for collisions. See the section “Applying Collision Detection” for a detailed discussion of this topic.

firePlayerMissile()

Creates a playerMissile object at the center of the player ship and fires it in the direction the player ship is facing.

fireSaucerMissile()

Creates a saucerMissile object at the center of the saucer and fires it in the direction of the player ship.

playerDie()

Creates an explosion for the player by calling createExplode(), as well as changing the game application state to GAME_STATE_PLAYER_DIE.

createExplode()

Accepts in the location for the explosion to start and the number of particles for the explosion.

boundingBoxCollide()

Determines whether the rectangular box that encompasses an object’s width and height is overlapping the bounding box of another object. It takes in two logical display objects as parameters and returns true if they are overlapping and false if they are not. See the section “Applying Collision Detection” for details about this function.

splitRock()

Accepts in the scale and x and y starting points for two new rocks that will be created if a large or medium rock is destroyed.

addToScore()

Accepts in a value to add to the player’s score.

Geo Blaster Global Game Variables

Now let’s look at the entire set of game application scope variables needed for our game.

Variables that control screen flow

These variables will be used when the title and “Game Over” screens first appear. They will be set to true after the screen is drawn. When these variables are true, the screens will look for the space bar to be pressed before moving on to the next application state:

var titleStarted = false;
var gameOverStarted = false;
Game environment variables

These variables set up the necessary defaults for a new game. We will discuss the extraShipAtEach and extraShipsEarned in the section “Awarding the Player Extra Ships”:

var score = 0;
var level = 0;
var extraShipAtEach = 10000;
var extraShipsEarned = 0;
var playerShips = 3;
Playfield variables

These variables set up the maximum and minimum x and y coordinates for the game stage:

var xMin = 0;
var xMax = 400;
var yMin = 0;
var yMax = 400;
Score value variables

These variables set the score value for each of the objects the player can destroy:

var bigRockScore = 50;
var medRockScore = 75;
var smlRockScore = 100;
var saucerScore = 300;
Rock size constants

These variables set up some human-readable values for the three rock sizes, allowing us to simply use the constant instead of a literal value. We can then change the literal value if needed:

const ROCK_SCALE_LARGE = 1;
const ROCK_SCALE_MEDIUM = 2;
const ROCK_SCALE_SMALL = 3;
Logical display objects

These variables set up the single player object and arrays to hold the various other logical display objects for our game. See the upcoming sections “The Player Object” and “Arrays of Logical Display Objects” for further details about each:

var player = {};
var rocks = [];
var saucers = [];
var playerMissiles = [];
var particles = []
var saucerMissiles = [];
Level-specific variables

The level-specific variables handle the difficulty settings when the game level increases. See the section “Level Knobs” for more details about how these are used:

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

The Player Object

The player object contains many of the variables we encountered earlier in this chapter when we discussed animating, rotating, and moving the player ship about the game screen. We have also added in three new variables that you have not seen before:

player.maxVelocity = 5;
player.width = 20;
player.height = 20;
player.halfWidth = 10;
player.halfHeight = 10;
player.rotationalVelocity = 5
player.thrustAcceleration = .05;
player.missileFrameDelay = 5;
player.thrust = false;

The new variables are halfWidth, halfHeight, and missileFrameDelay. halfWidth and halfHeight simply store half the width and half the height values, so these need not be calculated on each frame tick in multiple locations inside the code. The missileFrameDelay variable contains the number of frame ticks the game will count between firing player missiles. This way, the player cannot simply fire a steady stream of ordnance and destroy everything with little difficulty.

The player.thrust variable will be set to true when the player presses the up key.

Geo Blaster Game Algorithms

The game source code covers a lot of ground that we did not touch on earlier in this chapter. Let’s discuss some of those topics now; the rest will be covered in detail in Chapter 9.

Arrays of Logical Display Objects

We have used arrays to hold all our logical display objects, and we have an array for each type of object (rocks, saucers, playerMissiles, saucerMissiles, and particles). Each logical display object is a simple object instance. We have created a separate function to draw and update each of our objects.

The use of an object class prototype similar to FrameRateCounter can be implemented easily for the various display object types. To conserve space, we have not implemented them in this game. However, these objects would allow us to separate the update and draw code from the current common functions and then place that code into the individual object prototypes. We have included a Rock prototype at the end of this chapter as an example. (See Example 8-13.)

You will notice that saucers and rocks are drawn with points in the same manner as the player ship.

Rocks

The rocks will be simple squares that rotate clockwise or counterclockwise. The rock instances will be in the rocks array. When a new level starts, these will all be created in the upper-right corner of the game screen.

Here are the variable attributes of a rock object:

newRock.scale = 1;
newRock.width = 50;
newRock.height = 50;
newRock.halfWidth = 25;
newRock.halfHeight = 25;
newRock.x
newRock.y
newRock.dx
newRock.dy
newRock.scoreValue = bigRockScore;
newRock.rotation = 0;

The rock scale will be set to one of the three rock-scale constants discussed earlier. halfWidth and halfHeight will be set based on the scale, and they will be used in calculations in the same manner as the player object versions. The dx and dy values represent the values to apply to the x and y axes when updating the rock on each frame tick.

Saucers

Unlike Atari’s Asteroids game, which has both small and large saucers, we are going to have only one size in Geo Blaster Basic. It will be stored in the saucers array. On a 28×13 grid (using paths), it looks like Figure 8-6.

The variable attributes of the saucer object are very similar to the attributes of a rock object, although without the rock scale attribute. Also, saucers don’t have a rotation; it is always set at 0. The saucer also contains variables that are updated on each new level to make the game more challenging for the player. Here are those variables, which will be discussed in more detail in the upcoming section “Level Knobs”:

newSaucer.fireRate = levelSaucerFireRate;
newSaucer.fireDelay = levelSaucerFireDelay;
newSaucer.fireDelayCount = 0;
newSaucer.missileSpeed = levelSaucerMissileSpeed;

Missiles

Both the player missiles and saucer missiles will be 2×2-pixel blocks. They will be stored in the playerMissiles and saucerMissiles arrays, respectively.

The objects are very simple. They contain enough attributes to move them across the game screen and to calculate life values:

newPlayerMissile.dx = 5*Math.cos(Math.PI*(player.rotation)/180);
newPlayerMissile.dy = 5*Math.sin(Math.PI*(player.rotation)/180);
newPlayerMissile.x = player.x+player.halfWidth;
newPlayerMissile.y = player.y+player.halfHeight;
newPlayerMissile.life = 60;
newPlayerMissile.lifeCtr = 0;
newPlayerMissile.width = 2;
newPlayerMissile.height = 2;

Explosions and particles

When a rock, saucer, or the player ship is destroyed, that object explodes into a series of particles. The createExplode() function creates this so-called particle explosion. Particles are simply individual logical display objects with their own life, dx, and dy values. Randomly generating these values makes each explosion appear to be unique. Particles will be stored in the particles array.

Like missiles, particle objects are rather simple. They also contain enough information to move them across the screen and to calculate their life span in frame ticks:

newParticle.dx = Math.random()*3;
newParticle.dy = Math.random()*3;
newParticle.life = Math.floor(Math.random()*30+30);
newParticle.lifeCtr = 0;
newParticle.x = x;
newParticle.y = y;

Level Knobs

Even though we never show the level number to the game player, we are adjusting the difficulty every time a screen of rocks is cleared. We do this by increasing the level variable by 1 and then recalculating these values before the level begins. We refer to the variance in level difficulty as knobs, which refers to dials or switches. Here are the variables we will use for these knobs:

level+3

Number of rocks

levelRockMaxSpeedAdjust = level*.25;

Rock max speed

levelSaucerMax = 1+Math.floor(level/10);

Number of simultaneous saucers

levelSaucerOccurrenceRate = 10+3*level;

Percent chance a saucer will appear

levelSaucerSpeed = 1+.5*level;

Saucer speed

levelSaucerFireDelay = 120-10*level;

Delay between saucer missiles

levelSaucerFireRate = 20+3*level;

Percent chance a saucer will fire at the player

levelSaucerMissileSpeed = 1+.2*level;

Speed of saucer missiles

Level and Game End

We need to check for game and level end so we can transition to either a new game or to the next level.

Level end

We will check for level end on each frame tick. The function to do so will look like this:

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

When the rocks array length is 0, we switch the state to GAME_STATE_NEW_LEVEL.

Game end

We do not need to check for the end of the game on each frame tick. We need to check only when the player loses a ship. We do this inside the gameStatePlayerDie() function:

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

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

This is the state function that is called on each frame tick during the GAME_STATE_PLAYER_DIE state. First, it checks to see that there are no longer any particles on the screen. This ensures that the game will not end until the player ship has finished exploding. We also check to make sure that all the player’s missiles have finished their lives. We do this so that we can check for collisions between the playerMissiles and for collisions of rocks against saucers. This way the player might earn an extra ship before playerShips-- is called.

When the particles and missiles have all left the game screen, we subtract 1 from the playerShips variable and then switch to GAME_STATE_GAME_OVER if the playerShips number is less than 1.

Awarding the Player Extra Ships

We want to award the player extra ships at regular intervals based on her score. We do this by setting an amount of points that the game player must achieve to earn a new ship—this also helps us keep a count of the number of ships earned:

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

We call this function on each frame tick. The player earns an extra ship if the score/extraShipAtEach variable (with the decimals stripped off) is greater than the number of ships earned. In our game, we have set the extraShipAtEach value to 10000. When the game starts, extraShipsEarned is 0. When the player’s score is 10000 or more, score/extraShipAtEach will equal 1, which is greater than the extraShipsEarned value of 0. An extra ship is given to the player, and the extraShipsEarned value is increased by 1.

Applying Collision Detection

We will be checking the bounding box around each object when we do our collision detection. A bounding box is the smallest rectangle that will encompass all four corners of a game logic object. We have created a function for this purpose:

function boundingBoxCollide(object1, object2) {

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

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

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

return(true);

};

We can pass any two of our game objects into this function as long as each contains x, y, width, and height attributes. If the two objects are overlapping, the function will return true. If not, it will return false.

The checkCollision() function for Geo Blaster Basic is quite involved. The full code listing is given in Example 8-12. Rather than reprint it here, let’s examine some of the basic concepts.

One thing you will notice is the use of “labels” next to the for loop constructs. Using labels, such as in the following line, can help streamline collision detection:

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

We will need to loop through each of the various object types that must be checked against one another. But we do not want to check an object that was previously destroyed against other objects. To ensure that we do the fewest amount of collision checks necessary, we have implemented a routine that employs label and break statements.

Here is the logic behind the routine:

1. Create a rocks: label, and then start to loop through the rocks array.

2. Create a missiles: label inside the rocks iteration, and loop through the playerMissiles array.

3. Do a bounding box collision detection between the last rock and the last missile. Notice that we loop starting at the end of each array so that we can remove elements (when collisions occur) in the array without affecting array members that have not been checked yet.

4. If a rock and a missile collide, remove them from their respective arrays, and then call break rocks and then break missiles. We must break back to the next element in an array for any object type that is removed.

5. Continue looping through the missiles until they have all been checked against the current rock (unless break rocks was fired off for a rock/missile collision).

6. Check each saucer, each saucer missile, and the player against each of the rocks. The player does not need a label because there is only a single instance of the player. The saucers and saucerMissiles will follow the same logic as missiles. If there is a collision between one and a rock, break back to their respective labels after removing the objects from their respective arrays.

7. After we have checked the rocks against all the other game objects, check the playerMissiles against the saucers, using the same basic logic of loop labels, looping backward through the arrays and breaking back to the labels when objects are removed.

8. Check the saucerMissiles against the player in the same manner.

Over the years, we have found this to be a powerful way to check multiple objects’ arrays against one another. It certainly is not the only way to do so. If you are not comfortable using loop labels, you can employ a method such as the following:

1. Add a Boolean hit attribute to each object, and set it to false when an object is created.

2. Loop through the rocks, and check them against the other game objects. This time the direction (forward or backward) through the loops does not matter.

3. Before calling the boundingBoxCollide() function, be sure that each object’s hit attribute is false. If not, skip the collision check.

4. If the two objects collide, set each object’s hit attribute to true. There is no need to remove objects from the arrays at this time.

5. Loop though playerMissiles and check against the saucers, and then loop through the saucers to check against the player.

6. When all the collision-detection routines are complete, reloop through each object array (backward this time) and remove all the objects with true as a hit attribute.

We have used both methods—and variations—on each. While the second method is a little cleaner, this final loop through all of the objects might add more processor overhead when dealing with a large number of objects. We will leave the implementation of this second method to you as an exercise, in case you want to test it.

The Geo Blaster Basic Full Source

The full source code and assets for Geo Blaster Basic are located at this site.

Figure 8-7 shows a screenshot of the game in action.

Rock Object Prototype

To conserve space, we did not create separate object prototypes for the various display objects in this game. However, Example 8-12 is a Rock prototype object that can be used in a game such as Geo Blaster Basic.

Example 8-12. The Rock.js prototype
//*** Rock Object Prototype

function Rock(scale, type) {

//scale
//1 = large
//2 = medium
//3 = small
//these will be used as the divisor for the new size
//50/1 = 50
//50/2 = 25
//50/3 = 16

this.scale = scale;
if (this.scale <1 || this.scale >3){
this.scale=1;
}
this.type = type;
this.dx = 0;
this.dy = 0;
this.x = 0;
this.y = 0;
this.rotation = 0;
this.rotationInc = 0;
this.scoreValue = 0;

//ConsoleLog.log("create rock. Scale=" + this.scale);
switch(this.scale){

case 1:
this.width = 50;
this.height = 50;
break;
case 2:
this.width = 25;
this.height = 25;
break;
case 3:
this.width = 16;
this.height = 16;
break;
}

}

Rock.prototype.update = function(xmin,xmax,ymin,ymax) {
this.x += this.dx;
this.y += this.dy;
this.rotation += this.rotationInc;
if (this.x > xmax) {
this.x = xmin-this.width;
}else if (this.x<xmin-this.width){
this.x = xmax;
}

if (this.y > ymax) {
this.y = ymin-this.width;
}else if (this.y<ymin-this.width){
this.y = ymax;
}
}

Rock.prototype.draw = function(context) {

var angleInRadians = this.rotation * Math.PI / 180;
var halfWidth = Math.floor(this.width*.5); //used to find center of object
var halfHeight = Math.floor(this.height*.5)// used to find center of object
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity
// translate the canvas origin to
// the center of the player
context.translate(this.x+halfWidth,this.y+halfHeight);
context.rotate(angleInRadians);
context.strokeStyle = '#ffffff';

context.beginPath();

// draw everything offset by 1/2. Zero Relative 1/2 is if .5*width −1.
// Same for height

context.moveTo(-(halfWidth-1),-(halfHeight-1));
context.lineTo((halfWidth-1),-(halfHeight-1));
context.lineTo((halfWidth-1),(halfHeight-1));
context.lineTo(-(halfWidth-1),(halfHeight-1));
context.lineTo(-(halfWidth-1),-(halfHeight-1));

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

}

//*** end Rock Class

Simple A* Path Finding on a Tile Grid

Next we are going to take a look at a subject that applies to many games, Path Finding. We are going to revisit the tile-based grid drawing from Chapter 4 and apply JavaScript path finding algorithms to the game screen. This type of logic can be used in many types of games, from the simplest Pac-Man style contests to the most complicated 3D shooters. We are not going to develop our own path finding algorithms in this chapter; instead, we will make use of a pre-existing A-Star (or A* as we will continue to call it over the next few sections) and use the canvas to display the results.

What Is A*?

A* is a grid-based path finding algorithm used to find the shortest “node” path from point A to point B. A* is best implemented on a grid-based game screen where we consider each tile of the grid to be a “node.” For example, Figure 8-8 shows a simple grid made up of tiles from a tile sheet.

In this simple five-column and five-row tile map, we have only two types of tiles. The gray tiles are “movable” tiles, meaning that game characters can occupy those tiles, while the blue tiles are “walls.” Wall tiles are tiles that no game character can occupy. As you can see, there are only three movable tiles on the Figure 8-8 tile map.

A* can be used to find the shortest path between two points on a map, as illustrated in Figure 8-8. As you can see, there is only one straight line that an object can move in on this simple map. Using 0,0 as the index of the first tile, the column that extends from row 0, column 1 to row 2, column 1 is this straight vertical line. This is of no use to us in practice, because you would not need any type of path finding algorithm to figure out that a game character can move on only those three tiles, but we are going to start simple and get more complicated as we proceed. A* is a very useful tool, and although we are not going to code our own library here, we will go over the basic pseudocode for the algorithm.

David M. Bourg and Glenn Seeman, in AI for Game Developers (O’Reilly), describe A* this way:

A* uses nodes to create a simplified search area to find the shortest path between any two points.

They also provide the following simplified A* pseudocode:

//*** A* Pseudo code from AI For Game Developers
//*** David M. Bourg
//*** Glenn Seeman
//*** O'Reilly (R)

add the starting node to the open node list
while the open list is not empty
current node=node from open list with lowest cost
if current node == goal then
path complete
else
move current node to the closed list
examine each node adjacent to the current node
for each adjacent node
if node is not on the open list
and node is not on the closed list
and node is not an obstacle
then
move node to open list and calculate cost of entire path
}

Nodes can contain obstacles (like our blue walls) or other terrain, such as water or grass. For our purposes, the blue tiles can never be passed, but we will add in a grass tile in later examples that, while not easily passable, is not impassable like the blue tiles. We are not going to use the available chapter space to create our own version of A* in JavaScript. Rather, we are going to use a version created by Brian Grinstead.

Brian’s A* algorithm in JavaScript can be found at this site.

We will be using both the graph.js and the astar.js files that Brian created and has supplied for download at his site. The one caveat to using an algorithm that we did not create is that we will need to make some minor modifications to our display code. Brian’s code expects a tile map made up of columns of rows, while our tile map is made up of rows of columns. For example purposes, you can treat one of our tile maps from Chapter 4 as being turned on its side if it’s to be used with Brian’s code. (That is, we simply swap the x and y components.) However, it is easy to compensate for this. Let’s get into the first example, and then we can describe what we mean and how the JavaScript and Canvas code is affected.

You will need to have downloaded the example files for this chapter from the book’s website to use the use the first example. We have provided a new, very simple tile sheet called tiles.png, as well as the astar.js and graph.js files from Brian Grinstead’s algorithm library.

Figure 8-9 shows the tiles.png tile sheet that we will use as the source graphic material for the A* examples throughout the rest of this section.

Here is the full code listing for Example 8-13. We will be modifying this code in the following sections to add to the size of the map and to demonstrate further A* functionality. This simple example shows the basics of using the algorithm with our tile sheet and tile map and shows how to display the results on the Canvas.

Example 8-13. A* Example 1
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 14: A* Example 1</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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');
}
//set up tile map
var mapRows=5;
var mapCols=5;
var tileMap=[
[0,1,0,0,0]
,[0,1,0,0,0]
,[0,1,0,0,0]
,[0,0,0,0,0]
,[0,0,0,0,0]
];
//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:0,y:1}; // use values of map turned on side
var endNode={x:2,y:1};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, false);

//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

function eventSheetLoaded() {
drawScreen();
}
function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){
var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;
context.drawImage(tileSheet,sourceX,sourceY,32,32,colCtr*32,
rowCtr*32,32,32);
}
}
//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16,(startNode.x*32)+16,10,0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}
}
}
</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="500" height="500">
Your browser does not support the HTML5 Canvas.
</canvas>
</div>
</body>
</html>

Example 8-13 presents some new concepts and some review from Chapter 4. Let’s take a look at the most important parts of the code before we take a look at what it does in action.

In the example code, we first set up our tile map. This describes the look of the game screen and which tiles to display:

   var mapRows=5;
var mapCols=5;
var tileMap=[
[0,1,0,0,0]
,[0,1,0,0,0]
,[0,1,0,0,0]
,[0,0,0,0,0]
,[0,0,0,0,0]
];

Our tile map will be made up of five rows and five columns. The tile map is laid out in a two-dimensional array of rows and columns that mimic how it will look on the screen. Notice that the second column is made up of the number 1 in the first three rows. When displayed on the canvas (without including the path finding code from Example 8-13), it will look like Figure 8-10. This amounts to simply swapping the x and y values we use for displaying the map when we use and evaluate the data that comes back from the astar.js functions.

The goal of Example 8-13 is to use this very simple tile map with only three movable tiles to show an example of how the astar.as and graph.as functions work to find a path of nodes.

The first task is to create a new Graph object from the prototype code in the graph.as file. We do this by passing a two-dimensional array into to new Graph() constructor function. As mentioned earlier, the problem is that the Graph prototype is looking for columns of rows rather than rows of columns. Therefore, when we create the startNode and endNode objects that are used by astar.js, we need to flip our idea of the tile map on its side and pass in the values as if the tile map was set up in this manner. (Again, simply swapping the x and y values will do the trick.)

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:0,y:1}; // use values of map turned on side
var endNode={x:2,y:1};

Figure 8-11 shows this flipping concept in an easy-to-understand fashion. The x and y values for our tile map are simply swapped for use with astar.js.

Figure 8-11 shows the difference between the two-dimensional array structure for the tile map we have created and the two-dimensional structure that the graphs.js expects its graph map to be in. When we create the startNode object, we use 0 for the x value and 1 for the y value because these map to the graph.js expected values. The actual values on our tile map are 0 for the row (y) and 1 for the column (x). We needed to use the same logic to transpose the end node from row 2, column 1 (or, y=2 and x=1) into x=2, y=1.

To find an actual path, the start and end nodes are passed into the astar.search function:

var result = astar.search(graph.nodes, start, end, false);

The false value passed into the function as the final parameter tells the function to not consider diagonal nodes in the search. We will use this option in an example further along in this section. The result that is returned is an array of nodes that is the shortest path through the movable tiles.

When we do the actual Canvas drawing, we use the exact same code as we used in Chapter 4 for drawing the tile map from the tile sheet. This uses the rows of columns. The tile map is first drawn in its entirety by looping through the rows and then each element in each row (or the column) and drawing that tile to the screen.

To show the path, we draw a green circle at the starting node and a red circle at the end node. In between, we draw a black circle on each path node. The interesting thing to note here is that when we loop through the result array of objects for the nodes, we use the y value returned as the x coordinate (or row) and the x returned as the y coordinate (or column):

context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,(Math.PI/180)*360,false);

The final output from the example will return the simplest path that can be drawn between two points. Figure 8-12 shows this final output.

A* Applied to a Larger Tile Map

To demonstrate A* path finding in a more real-world way, we must first create a new tile map. For our next example, we are going to create a more maze-like structure that still uses just the first two tile types.

The following code example shows the changes to the Example 8-13 tile map for Example 8-14. We will also be changing the startNode and endNode for Example 8-14. We have not provided the full code listing for this example, just the changes needed to turn Example 8-13 into Example 8-14:

 //Example 8-14 changes to Example 8-13 to make a larger tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
,[0,1,1,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,1,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,1,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,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,1,1,1,1,1,1,1,1,1,1,0]
,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
];

If we create a tile map like the above, it will result in a map that looks like Figure 8-13, which shows the output from the new, larger tile map that will be generated from this data.

Along with changing the map data for Example 8-14, we will also be adding new start and end nodes to reflect the larger map size. Let’s choose a start node of row 4, column 1, and end node of row 13, column 10. This would require us to make changes to the new startNode and endNode variables. The changes for Example 8-14 are simple, but they will create this larger map and provide a better demonstration of the capabilities of the graph.as and astar.js functions:

In Example 8-14 we will demonstrate what happens when we increase the size of the Example 8-13 tile map and run the A* functions. The following code shows the changes to the startNode and endNode for this larger tile map.

//Example 8-14 startNode and endNode
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

If you make all of the changes to Example 8-13 listed previously, the result will be Example 8-14. The result of Example 8-14 is shown in Figure 8-14.

Example 8-14. Larger A* example
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 14 - Larger A* Example</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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');
}

//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
,[0,1,1,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,1,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,1,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,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,1,1,1,1,1,1,1,1,1,1,0]
,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
];

console.log("tileMap.length=" , tileMap.length);

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, false);

//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

function eventSheetLoaded() {
drawScreen()
}

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){

var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;

context.drawImage(tileSheet, sourceX, sourceY,32,32,
colCtr*32,rowCtr*32,32,32);

}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

}

}

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

A* Taking Diagonal Moves into Account

For Example 8-15, we will be making changes to Example 8-14 to add in diagonal movement to the A* path.

So far, each of our examples has used the astar.js functions, which ignore diagonal movements between nodes. We can easily add this capability. Example 8-15 does this with one simple change. We will be changing the false in the A* search function to true:

//For example 8-15 we will add true to the end of the search function

var result = astar.search(graph.nodes, start, end, true);

By simply changing false to true at the end of the call to the astar.search(), we can change the result node path dramatically. Figure 8-15 shows the path difference.

Each node on the movable tiles has the same weight (1). When A* calculates the shortest node path, it does so by taking these weights into account. Next we will add in some nodes with a higher weight value.

Example 8-15. Larger A* example with diagonals
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 15 - Larger A* Example with Diagonals</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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');
}

//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
,[0,1,1,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,1,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,1,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,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,1,1,1,1,1,1,1,1,1,1,0]
,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
];

console.log("tileMap.length=" , tileMap.length);

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, true);

//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

function eventSheetLoaded() {
drawScreen()
}

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){

var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;

context.drawImage(tileSheet, sourceX, sourceY,32,32,colCtr*32,
rowCtr*32,32,32);

}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

}

}

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

A* with Node Weights

For Example 8-16, we will be adding weighs to our nodes. We’ll do this by simply adding in some grass tiles to the tile map we have been using in the previous examples. By doing this, we can change the A* search result in a path avoiding the grass tiles has a lower total node value sum than one that travels over the grass tiles.

We can add to the weight of each open node by simply giving it a number higher than 1. We have created our tile sheet to make this very simple. The third tile (or tile index 2) is a grass tile. With astar.as, as long as a tile has a node value greater than 0, it is considered a movable tile. If a path can be made through the maze and the total value of the path, taking the node values into account, is the lowest, the path will cross these nodes with higher values. To demonstrate this, we will now add some grass tiles to the tile map. The changes for Example 8-16 are below. Notice that we are also removing the diagonal movement from Example 8-15, but it is not mandatory that you do so. We will look at that in Example 8-17:

//Example 8-16 changes to example 8-15 tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[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,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,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]
];

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, false);

This will result in Figure 8-16, showing the path running through a grass tile because it does not add enough to the total path cost to force a new direction change.

Example 8-16. Larger A* example with grass tiles
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 16 - Larger A* Example with Grass Tiles</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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');
}

//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[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,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,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]
];

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, false);

//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

function eventSheetLoaded() {
drawScreen()
}

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){

var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;

context.drawImage(tileSheet, sourceX, sourceY,32,32,
colCtr*32,rowCtr*32,32,32);

}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

}

}

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

In Example 8-16 (shown in Figure 8-16), you can see that even though the grass tile increased the total node path value by 1, it was still the shortest path through the maze to the end goal node. This resulted in a path that ran through the grass node. In the next example, Example 8-17, we will add back in the diagonal movement to the node path. By doing so, the tank will avoid the grass node because the A* function will be able to find a path that has a lower total combined node weight than one that travels over the grass tiles.

A* with Node Weights and Diagonals

In Example 8-17 we will add back in the diagonal node capability to see whether this will allow for a path that will bypass the grass tiles. A diagonal will add 1.41 (the square root of 2) to the total node path weight, while a grass tile will add 2. For that reason, adding in the use of diagonals will cost less than moving over grass.

//Example 8-17 changes to Example 8-16 to add node weights
var result = astar.search(graph.nodes, start, end, true);

After simply adding true back into astar.search(), Figure 8-17 shows that the grass tiles can be avoided because even though a diagonal adds 1.41 to the path cost, that amount is still less than grass tile movement.

Wow...just adding in diagonal path node capability changed the node structure of the result path dramatically and allowed us to avoid a path that moves over grass tiles. As a final example, and because this book really is about making cool things happen on the canvas, we are going add the green tank from the tile sheet to the demo and have it follow the A* created path.

Example 8-17. Larger A* Example with grass tiles and diagonal movement
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 17 - A* with Grass Tiles and Diagonal Moves</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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');
}

//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[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,2,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,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,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]
];

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, true);

//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

function eventSheetLoaded() {
drawScreen()
}

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){

var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;

context.drawImage(tileSheet, sourceX, sourceY,32,32,
colCtr*32,rowCtr*32,32,32);

}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

}

}

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

In the final next A* example, Example 8-18, we’ll get to the fun part of Canvas and A*. We are going to actually animate and move the tank through the node path.

The full code listing for Example 8-18 follows. We’ll explain the more interesting parts after you have taken a look and have had a chance to see the example in action. The movement code combines the animation and transformation code from Chapter 4 with the A* node path result to create a really cool animated implementation of A* path finding.

Example 8-18. Full code listing of A* with tank animation
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 18 - Larger A* With Tank Animation</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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 currentNodeIndex=0;
var nextNode;
var currentNode;
var rowDelta=0;
var colDelta=0;
var tankX=0;
var tankY=0;
var angleInRadians=0;
var tankStarted=false;
var tankMoving=false;
var finishedPath=false;
//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[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,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,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]
];

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, false);
console.log("result", result);

//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

const FRAME_RATE=40;
var intervalTime=1000/FRAME_RATE;
function eventSheetLoaded() {
gameLoop();
}

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

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){
var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;
context.drawImage(tileSheet,sourceX,
sourceY,32,32,colCtr*32,rowCtr*32,32,32);
}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

if (!finishedPath) {
if (!tankStarted) {
currentNode=startNode;
tankStarted=true;
nextNode=result[0];
tankX=currentNode.x*32;
tankY=currentNode.y*32
}

if (tankX==nextNode.x*32 && tankY==nextNode.y*32) {
//node change
currentNodeIndex++;
if (currentNodeIndex == result.length) {
finishedPath=true;
}
currentNode=nextNode;
nextNode=result[currentNodeIndex]
tankMoving=false;
}

if (!finishedPath) {
if (nextNode.x > currentNode.x) {
colDelta=1;
}else if (nextNode.x < currentNode.x) {
colDelta=-1
}else{
colDelta=0
}

if (nextNode.y > currentNode.y) {
rowDelta=1;
}else if (nextNode.y < currentNode.y) {
rowDelta=-1
}else{
rowDelta=0
}
angleInRadians=Math.atan2(colDelta,rowDelta);
tankMoving=true;

}
tankX+=colDelta;
tankY+=rowDelta;
}

var tankSourceX=Math.floor(3 % 5) *32;
var tankSourceY=Math.floor(3 / 5) *32;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity
context.translate((tankY)+16,(tankX)+16);
context.rotate(angleInRadians);
context.drawImage(tileSheet, tankSourceX, tankSourceY,32,32,-16,-16,32,32);
context.restore();
}
}

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

Moving a Game Character Along the A* Path

Let’s dig deeper into the logic behind Example 8-18. To add a game character to the path, we will need to use the x and y coordinates returned in the result array from the astar.search() function. This function does not return the starting point of our path, so we must use the startNode object values for the first node. After that, we can use the nodes from the path to have our tank follow. There is a lot of new code for this example, so let’s take it in small pieces, starting with the variable we will need.

Game variables for tank movement and node changes

Here are the variables we’ll use for tank movement and node changes:

var currentNodeIndex=0;
var nextNode;
var currentNode;
var rowDelta=0;
var colDelta=0;
var tankX=0;
var tankY=0;
var angleInRadians=0;
var tankStarted=false;
var tankMoving=false;
var finishedPath=false;

Following are brief descriptions of what these variables do:

currentNodeIndex

This will hold the integer of the current node on the path. Because the result array does not contain the entire path, we will need to calculate the location to draw the tank on the starting node in a slightly different manner than when the tank is moving along the A* node path.

nextNode

Contains the object values for the next node that the tank is moving to.

currentNode

Contains the object values for the node the tank is moving from.

rowDelta

Calculated each time the tank changes nodes. This represents the pixel change on the y-axis for the moving tank.

colDelta

Calculated each time the tank changes nodes. This represents the pixel change on the x-axis for the moving tank.

tankX

The current Canvas screen x-axis coordinates used to draw the tank on the screen.

tankY

The current Canvas screen y-axis coordinate used to draw the tank on the screen.

angleInRadians

Calculated each time the tank changes nodes. This is the angle that we must rotate the tank so that it is pointing in the right direction as it moves along the node path.

tankStarted

This remains false until the tank has moved the first time from the starting node. After the tank has moved from the starting node, we can use the result array node values.

tankMoving

When this is false, the tank has made it to the center of the next node (or the tank has not yet moved from the starting node. The code will calculate new rowDelta, colDelta, and angleInRadians values at this time.

finishedPath

This is set to true after the tank has moved completely into the final node on the path.

In the drawScreen() function, we are going to add code to test whether or not the tank has started down the path. First we are going to enclose everything but the actual tank drawing code inside the following if conditional:

if (!finishedPath) {
//Logic for updating he tank node and position
}

Logic for updating the tank node and position

Next we will update the node the tank is positioned on. First we must check to see whether the tank has started moving at all, If not, we will use the result[0] value returned by the A* function.

if (!tankStarted) {
currentNode=startNode;
tankStarted=true;
nextNode=result[0];
tankX=currentNode.x*32;
tankY=currentNode.y*32
}

If the tank is in the first startNode, tankStarted will be false. In this case, we must set currentNode to the startNode value. nextNode will be the first node in the result array node path. The tankX and tankY coordinates are the path x and y values (respectively) multiplied by the tile size (32).

Now that the tank is actually moving, we check to determine when it has made it to the center of the next node. We did this the simplest manner possible because each step that the tank takes in any direction will be the value 1.

If the tank were to move at a speed other than 1, it would be better to calculate the vector between the center of each of the nodes and count the steps in between the nodes. We would then move the tank that number of steps before it stopped to change nodes. Examples of this type of movement are provided in Chapter 5.

if (tankX==nextNode.x*32 && tankY==nextNode.y*32) {
//node change
currentNodeIndex++;
if (currentNodeIndex == result.length) {
finishedPath=true;
}
currentNode=nextNode;
nextNode=result[currentNodeIndex]
tankMoving=false;
}

Next we check to see whether the tank has moved to the center of the next node. We do this by comparing the tank x and y values with the node x and y values. First we add 1 to currentNodeIndex. Using that new value, we either stop the tank from moving by setting finishedPath to true, or we set currentNode to be nextNode, set nextNode to the new node in the result path using currentNodeIndex, and we set tankMoving to false.

When the tank is not moving, we know that we must be at the center of the current node. This requires us to calculate how and where to move the tank to the next node and what angle it should be rotated in.

if (!finishedPath) {
if (nextNode.x > currentNode.x) {
colDelta=1;
}else if (nextNode.x < currentNode.x) {
colDelta=-1
}else{

colDelta=0
}
if (nextNode.y > currentNode.y) {
rowDelta=1;
}else if (nextNode.y < currentNode.y) {
rowDelta=-1
}else{
rowDelta=0
}
angleInRadians=Math.atan2(colDelta,rowDelta);
tankMoving=true;
}

If the x value of nextNode is greater than the x value of currentNode, the tank will need to move to the right. If the opposite is true, the tank will move to the left. If the x values are equal, there will be no movement on the x-axis.

If the y value of nextNode is greater than the y value of currentNode, the tank will need to move down. If the opposite is true, the tank will move up. If the y values are equal, there will be no movement on the y-axis.

After we have calculated the rowDelta and colDelta values, we can use those to find the angle to rotate the tank. This is accomplished by passing them into the Mathy.atan2 function. Notice that we swap the normal y,x (row, col) in the atan2 function with x,y (col,row) to match the screen coordinates for our tile map rather than the astar.search() returned coordinates. This goes back to the tile map being laid out in rows of columns rather than columns of rows (the way the graph.as uses them).

Finally, if the tank has not finished its path, we need to add the colDelta value to the tankX position and the rowDelta value to the tankY position:

tankX+=colDelta;
tankY+=rowDelta;

Drawing the tank on the screen

This is a review of the discussion in Chapter 4 about using the Canvas transformations to rotate the tank to the correct angle based on the angleInRadians value we have stored on each node change:

var tankSourceX=Math.floor(3 % 5) *32;
var tankSourceY=Math.floor(3 / 5) *32;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity
context.translate((tankY)+16,(tankX)+16);
context.rotate(angleInRadians);
context.drawImage(tileSheet, tankSourceX, tankSourceY,32,32,-16,-16,32,32);
context.restore();

First, we find the tile location of the tile sheet (top-left corner) of the tank tile and place those values into the tankSourceX and tankSourceY variables. Next we save the current canvas to the stack and reset the transformation matrix to the reset identity. We then translate the entire canvas to the center of the current node (tile) and rotate it using the angleInRadians value. The image is then drawn to the Canvas as if the drawing pen is sitting at 0,0. To draw the tank in the center, we must offset −16 on each.

Figure 8-18 shows the tank following the path created in Example 8-18. If we change var result = astar.search(graph.nodes, start, end, false) to var result = astar.search(graph.nodes, start, end, true), we will get the result shown in Figure 8-19. The path will actually take the tank diagonally through walls, so if you plan to use the true (use diagonal path node) parameter, you will want to take this into consideration when you are creating your tile maps. An extra block at that intersection would prevent the tank from moving through that path, as can be seen in Figure 8-20.

Tanks That Pass Through Walls?

In Example 8-19, we are going to demonstrate how a poorly designed tile map can allow for a node path to seemingly go through solid walls when diagonal movement is allow. Figure 8-19 shows this occurring. Here is the full code listing for Example 8-19.

Example 8-19. Maze design with tank going through walls

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 19 - A* With Tank Animation with diagoinal moves</title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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 currentNodeIndex=0;
var nextNode;
var currentNode;
var rowDelta=0;
var colDelta=0;
var tankX=0;
var tankY=0;
var angleInRadians=0;
var tankStarted=false;
var tankMoving=false;
var finishedPath=false;
//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[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,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,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]
];

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, true);
console.log("result", result);
//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

const FRAME_RATE=40;
var intervalTime=1000/FRAME_RATE;

function eventSheetLoaded() {
gameLoop();
}

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

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){

var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;
context.drawImage(tileSheet, sourceX, sourceY,32,32,
colCtr*32,rowCtr*32,32,32);
}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

if (!finishedPath) {
if (!tankStarted) {
currentNode=startNode;
tankStarted=true;
nextNode=result[0];
tankX=currentNode.x*32;
tankY=currentNode.y*32
}

if (tankX==nextNode.x*32 &&  tankY==nextNode.y*32) {
//node change
currentNodeIndex++;
if (currentNodeIndex == result.length) {
finishedPath=true;
}
currentNode=nextNode;
nextNode=result[currentNodeIndex]
tankMoving=false;
}

if (!finishedPath) {

if (nextNode.x > currentNode.x) {
colDelta=1;
}else if (nextNode.x < currentNode.x) {
colDelta=-1
}else{
colDelta=0
}

if (nextNode.y > currentNode.y) {
rowDelta=1;
}else if (nextNode.y < currentNode.y) {
rowDelta=-1
}else{
rowDelta=0
}

angleInRadians=Math.atan2(colDelta,rowDelta);
tankMoving=true;
}

tankX+=colDelta;
tankY+=rowDelta;
}

var tankSourceX=Math.floor(3 % 5) *32;
var tankSourceY=Math.floor(3 / 5) *32;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity
context.translate((tankY)+16,(tankX)+16);
context.rotate(angleInRadians);
context.drawImage(tileSheet, tankSourceX, tankSourceY,
32,32,-16,-16,32,32);
context.restore();

}

}

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

Figure 8-19 demonstrates Example 8-19. The poorly designed tile map with diagonal node paths turned on results in a path where the tank can move through the walls. In the next example, we will fix this problem with a new tile map.

In Example 8-20, we will modify the maze to add an extra block at the intersection where the tank passed through the walls with a diagonal path. Here is the new tile map for Example 8-20:

 var mapCols=15;
var tileMap=[
[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]
];

Figure 8-20 shows how the new tile map will affect the node path with diagonal movement applied. The new tiles will block the diagonal path through the seemingly impassable walls and create real impassable diagonal walls.

Example 8-20. New maze design with impassable walls

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 8 Example 20 - A* With Tank Animation with new maze to prevent
passable walls </title>
<script src="modernizr.js"></script>
<script type='text/javascript' src='graph.js'></script>
<script type='text/javascript' src='astar.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 currentNodeIndex=0;
var nextNode;
var currentNode;
var rowDelta=0;
var colDelta=0;
var tankX=0;
var tankY=0;
var angleInRadians=0;
var tankStarted=false;
var tankMoving=false;
var finishedPath=false;
//set up tile map
var mapRows=15;
var mapCols=15;
var tileMap=[
[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]
];

//set up a* graph
var graph = new Graph(tileMap);
var startNode={x:4,y:1}; // use values of map turned on side
var endNode={x:13,y:10};

//create node list
var start = graph.nodes[startNode.x][startNode.y];
var end = graph.nodes[endNode.x][endNode.y];
var result = astar.search(graph.nodes, start, end, true);
console.log("result", result);
//load in tile sheet image
var tileSheet=new Image();
tileSheet.addEventListener('load', eventSheetLoaded , false);
tileSheet.src="tiles.png";

const FRAME_RATE=40;
var intervalTime=1000/FRAME_RATE;

function eventSheetLoaded() {
gameLoop();
}

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

function drawScreen() {
for (var rowCtr=0;rowCtr<mapRows;rowCtr++) {
for (var colCtr=0;colCtr<mapCols;colCtr++){

var tileId=tileMap[rowCtr][colCtr];
var sourceX=Math.floor(tileId % 5) *32;
var sourceY=Math.floor(tileId / 5) *32;
context.drawImage(tileSheet, sourceX, sourceY,32,32,
colCtr*32,rowCtr*32,32,32);
}
}

//draw green circle at start node
context.beginPath();
context.strokeStyle="green";
context.lineWidth=5;
context.arc((startNode.y*32)+16, (startNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw red circle at end node
context.beginPath();
context.strokeStyle="red";
context.lineWidth=5;
context.arc((endNode.y*32)+16, (endNode.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();

//draw black circles on path
for (var ctr=0;ctr<result.length-1;ctr++) {
var node=result[ctr];
context.beginPath();
context.strokeStyle="black";
context.lineWidth=5;
context.arc((node.y*32)+16, (node.x*32)+16, 10, 0,
(Math.PI/180)*360,false);
context.stroke();
context.closePath();
}

if (!finishedPath) {
if (!tankStarted) {
currentNode=startNode;
tankStarted=true;
nextNode=result[0];
tankX=currentNode.x*32;
tankY=currentNode.y*32
}

if (tankX==nextNode.x*32 &&  tankY==nextNode.y*32) {
//node change
currentNodeIndex++;
if (currentNodeIndex == result.length) {
finishedPath=true;
}
currentNode=nextNode;
nextNode=result[currentNodeIndex]
tankMoving=false;
}

if (!finishedPath) {

if (nextNode.x > currentNode.x) {
colDelta=1;
}else if (nextNode.x < currentNode.x) {
colDelta=-1
}else{
colDelta=0
}

if (nextNode.y > currentNode.y) {
rowDelta=1;
}else if (nextNode.y < currentNode.y) {
rowDelta=-1
}else{
rowDelta=0
}

angleInRadians=Math.atan2(colDelta,rowDelta);
tankMoving=true;
}

tankX+=colDelta;
tankY+=rowDelta;
}

var tankSourceX=Math.floor(3 % 5) *32;
var tankSourceY=Math.floor(3 / 5) *32;
context.save(); //save current state in stack
context.setTransform(1,0,0,1,0,0); // reset to identity
context.translate((tankY)+16,(tankX)+16);
context.rotate(angleInRadians);
context.drawImage(tileSheet, tankSourceX, tankSourceY,
32,32,-16,-16,32,32);
context.restore();

}
}

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

We have now covered some basic, intermediate, and more advanced topics on A* path finding. These are easy to apply to any tile-based application where you need to find the shortest path between two points.

What’s Next?

We covered quite a bit in this chapter. HTML5 Canvas might lack some of the more refined features common to web game development platforms such as Flash, but it contains powerful tools for manipulating the screen in immediate mode. These features allow us to create a game application with many individual logical display objects—even though each canvas can support only a single physical display object (the canvas itself). We also covered a more advanced topic of tile-based path finding on a 2D map using the A* algorithm.

In Chapter 9, we will explore some more advanced game topics, such as replacing paths with bitmap images, creating object pools, and adding a sound manager. We’ll extend the game we built in this chapter and create a new turn-based strategy game, and we’ll cover scrolling a tile-based game screen.