Chapter 8. Canvas Game Essentials

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.

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 the 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 = '#ffaaaa';
         context.fillRect(0, 0, 200, 200);
         context.fillStyle = '#000000';
         context.font = '20px _sans';
         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.

The basic HTML file for
Figure 8-1. The basic HTML file for Chapter 8

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 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; once 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.

The player ship
Figure 8-2. The player ship

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';
   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 player ship on the canvas
Figure 8-3. The player ship on the canvas

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 player ship with thrust engaged
Figure 8-4. The player ship with thrust engaged

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';
   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 only display 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 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 setInterval() 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;
setInterval(drawScreen, intervalTime );

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

Note

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

   const FRAME_RATE = 40;
   var intervalTime = 1000/FRAME_RATE;
   setInterval(drawScreen, 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 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. Since 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. So, 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';
      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);

Note

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 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';
      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++;

   }

Alpha Fading the Player Ship

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.

Using the context.globalAlpha attribute

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';
      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 to 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 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

Once 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. Since 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.

Once we have the angle of the ship’s rotation, we must calculate the facingX and facingY values for this current direction. We only do this 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);

Once 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 only move 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 may recall, when we first drew the image for our player ship, we had the point 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 player ship drawn at the 0 degree rotation
Figure 8-5. The player ship drawn at the 0 degree rotation

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 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){
   //document.body.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+thurstAcceleration*facingX;
   movingY = movingY+thurstAcceleration*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';
   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
   }

   const FRAME_RATE = 40;
   var intervalTime = 1000/FRAME_RATE;
   setInterval(drawScreen, 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>

Once 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 2 pixels per frame. It’s easy to calculate the current velocity if we are only moving in 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) + (movingXNew*movingXNew));

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 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 (currentState) {

      case GAME_STATE_TITLE:
         currentGameStateFunction = gameStateTitle;
         break;

      case GAME_STATE_PLAY_LEVEL:
         currentGameStateFunctionappStatePlayeLevel;
         break;

      case GAME_STATE_GAME_OVER:
         currentGameStateFunction = gameStateGameOver;
         break;

   }

}

We will call the runGame() function repeatedly in the setInterval() 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:

setInterval(runGame, 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';
      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
   const FRAME_RATE = 40;
   var intervalTime = 1000/FRAME_RATE;
   setInterval(runGame, intervalTime );

}

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

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

}

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

//*** end console log object

</script>

Note

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.

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 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';
   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;
setInterval(appStateGamePlay, 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 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';
   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;
setInterval(runGame, 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. Once 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. Once 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 on 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 on 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 on 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 once 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 and Arrays of Logical Display Objects for further details on 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 on 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.

Note

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 only going to have 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 saucer design
Figure 8-6. The saucer design

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 :

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

Once 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 only need to check 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 we can check for collisions between the playerMissiles, and for rocks against saucers. This way the player might earn an extra ship before playerShips-- is called.

Once 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 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. Once 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 once 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 wish to test it.

The Geo Blaster Basic Full Source

Example 8-12 shows the entire set of code for our game. You can download this and the entire set of example files from the book’s website.

Example 8-12. The Geo Blaster Basic full source listing
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Geo Blaster Basic Game</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;
   }

   //application states
   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;
   var currentGameState = 0;
   var currentGameStateFunction = null;

   //title screen
   var titleStarted = false;

   //gameover screen
   var gameOverStarted = false;

   //objects for game play

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

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

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

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

   //create game objects and arrays
   var player = {};
   var rocks = [];
   var saucers = [];
   var playerMissiles = [];
   var particles = []
   var saucerMissiles = [];

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

   //keyPresses
   var keyPressList = [];

   function runGame(){
      currentGameStateFunction();
   }

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

         case GAME_STATE_TITLE:
             currentGameStateFunction = gameStateTitle;
             break;
         case GAME_STATE_NEW_GAME:
             currentGameStateFunction = gameStateNewGame;
             break;
         case GAME_STATE_NEW_LEVEL:
             currentGameStateFunction = gameStateNewLevel;
             break;
         case GAME_STATE_PLAYER_START:
             currentGameStateFunction = gameStatePlayerStart;
             break;
         case GAME_STATE_PLAY_LEVEL:
             currentGameStateFunction = gameStatePlayLevel;
             break;
         case GAME_STATE_PLAYER_DIE:
             currentGameStateFunction = gameStatePlayerDie;
             break;

         case GAME_STATE_GAME_OVER:
             currentGameStateFunction = gameStateGameOver;
             break;

      }

   }

   function gameStateTitle() {
      if (titleStarted !=true){
         fillBackground();
         setTextStyle();
         context.fillText  ("Geo Blaster Basic", 130, 70);
         context.fillText  ("Press Space To Play", 120, 140);

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

         }
      }
   }

   function gameStateNewGame(){
      ConsoleLog.log("gameStateNewGame")
      //set up new game
      level = 0;
      score = 0;
      playerShips = 3;
      player.maxVelocity = 5;
      player.width = 20;
      player.height = 20;
      player.halfWidth = 10;
      player.halfHeight = 10;
      player.rotationalVelocity = 5; //how many degrees to turn the ship
      player.thrustAcceleration = .05;
      player.missileFrameDelay = 5;
      player.thrust = false;

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

   }

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

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

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

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

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

         //start all new rocks in upper left for ship safety
         newRock.x = Math.floor(Math.random()*50);

         //ConsoleLog.log("newRock.x=" + newRock.x);
         newRock.y = Math.floor(Math.random()*50);

         //ConsoleLog.log("newRock.y=" + newRock.y);
         newRock.dx = (Math.random()*2)+levelRockMaxSpeedAdjust;
         if (Math.random()<.5){
            newRock.dx*=-1;
         }

         newRock.dy = (Math.random()*2)+levelRockMaxSpeedAdjust;
         if (Math.random()<.5){
            newRock.dy*=-1;
         }

         //rotation speed and direction
         newRock.rotationInc = (Math.random()*5)+1;

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

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

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

   }

   function gameStatePlayerStart(){
      fillBackground();
      renderScoreBoard();
      if (player.alpha < 1){
         player.alpha += .02;
         context.globalAlpha = player.alpha;
      }else{
         switchGameState(GAME_STATE_PLAY_LEVEL);
      }

      renderPlayerShip(player.x, player.y,270,1);
      context.globalAlpha = 1;
      updateRocks();
      renderRocks();
   }

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

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

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

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

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

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

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

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

         }
      }
   }

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

   }

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

   function renderScoreBoard() {

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

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

   }

   function checkKeys() {
      //check keys

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

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

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

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


      }else{
         player.thrust = false;
      }

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

      }

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

      if (keyPressList[32]==true) {
         //ConsoleLog.log("player.missileFrameCount=" + player.missileFrameCount);
         //ConsoleLog.log("player.missileFrameDelay=" + player.missileFrameDelay);         if (player.missileFrameCount>player.missileFrameDelay){
            firePlayerMissile();
            player.missileFrameCount = 0;

         }
      }
   }

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

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

   function updatePlayer() {
      player.missileFrameCount++;

      player.x += player.movingX;
      player.y += player.movingY;

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

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

   function updatePlayerMissiles() {
      var tempPlayerMissile= {};
      var playerMissileLength = playerMissiles.length-1;
      //ConsoleLog.log("update playerMissileLength=" + playerMissileLength);

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

         //ConsoleLog.log("update player missile" + playerMissileCtr)
         tempPlayerMissile = playerMissiles[playerMissileCtr];
         tempPlayerMissile.x += tempPlayerMissile.dx;
         tempPlayerMissile.y += tempPlayerMissile.dy;
         if (tempPlayerMissile.x > xMax) {
            tempPlayerMissile.x =- tempPlayerMissile.width;
         }else if (tempPlayerMissile.x<-tempPlayerMissile.width){
            tempPlayerMissile.x = xMax;
         }

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

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

   function updateRocks(){

      var tempRock = {};
      var rocksLength = rocks.length−1;
      //ConsoleLog.log("update rocks length=" + rocksLength);
      for (var rockCtr=rocksLength;rockCtr>=0;rockCtr--){
         tempRock = rocks[rockCtr]
         tempRock.x += tempRock.dx;
         tempRock.y += tempRock.dy;
         tempRock.rotation += tempRock.rotationInc;
         //ConsoleLog.log("rock rotationInc="+ tempRock.rotationInc)
         //ConsoleLog.log("rock rotation="+ tempRock.rotation)
         if (tempRock.x > xMax) {
            tempRock.x = xMin-tempRock.width;
         }else if (tempRock.x<xMin-tempRock.width){
            tempRock.x = xMax;
         }

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

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

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

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

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

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

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

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

            saucers.push(newSaucer);
         }

      }

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

         //should saucer fire
         tempSaucer.fireDelayCount++;

         if (Math.floor(Math.random()*100) <=tempSaucer.fireRate
             && tempSaucer.fireDelayCount>tempSaucer.fireDelay ){

            fireSaucerMissile(tempSaucer)
            tempSaucer.fireDelayCount= 0;
         }

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

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

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

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

      }
   }

   function updateSaucerMissiles() {
      var tempSaucerMissile = {};
      var saucerMissileLength = saucerMissiles.length-1;

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

         //ConsoleLog.log("update player missile" + playerMissileCtr)
         tempSaucerMissile = saucerMissiles[saucerMissileCtr];
         tempSaucerMissile.x += tempSaucerMissile.dx;
         tempSaucerMissile.y += tempSaucerMissile.dy;
         if (tempSaucerMissile.x > xMax) {
            tempSaucerMissile.x=-tempSaucerMissile.width;
         }else if (tempSaucerMissile.x<-tempSaucerMissile.width){
            tempSaucerMissile.x = xMax;
         }

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

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

   function updateParticles() {
      var tempParticle = {};
      var particleLength = particles.length-1;
      //ConsoleLog.log("particle=" + particleLength)
      for (var particleCtr=particleLength;particleCtr>=0;particleCtr--){
         var remove = false;
         tempParticle = particles[particleCtr];
         tempParticle.x += tempParticle.dx;
         tempParticle.y += tempParticle.dy;

         tempParticle.lifeCtr++;
         //ConsoleLog.log("particle.lifeCtr=" + tempParticle.lifeCtr);

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

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

            remove = true;

         }
         //}
         //catch(err) {
         //   ConsoleLog.log ("error in particle");
         //   ConsoleLog.log("particle:" + particleCtr);

         //}

         if (remove) {
            particles.splice(particleCtr,1)
            tempParticle = null;
         }

      }
   }

   function renderPlayerShip(x,y,rotation, scale) {
      //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+player.halfWidth,y+player.halfHeight);
      context.rotate(angleInRadians);
      context.scale(scale,scale);

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

      if (player.thrust==true && scale==1) {
      //check for scale==1 for ship indicator does not display with thrust
         context.moveTo(-4,-2);
         context.lineTo(-4,1);
         context.moveTo(-5,-1);
         context.lineTo(-10,-1);
         context.moveTo(-5,0);
         context.lineTo(-10,0);
      }
      context.stroke();
      context.closePath();

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

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

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

         //ConsoleLog.log("draw player missile " + playerMissileCtr)
         tempPlayerMissile = playerMissiles[playerMissileCtr];
         context.save(); //save current state in stack
         context.setTransform(1,0,0,1,0,0); // reset to identity

         //translate the canvas origin to the center of the player
         context.translate(tempPlayerMissile.x+1,tempPlayerMissile.y+1);
         context.strokeStyle = '#ffffff';

         context.beginPath();

         //draw everything offset by 1/2. Zero Relative 1/2 is 15
         context.moveTo(-1,-1);
         context.lineTo(1,-1);
         context.lineTo(1,1);
         context.lineTo(-1,1);
         context.lineTo(-1,-1);
         context.stroke();
         context.closePath();
         context.restore(); //pop old state on to screen
      }
   }

   function renderRocks() {
      var tempRock = {};
      var rocksLength = rocks.length-1;
      for (var rockCtr=rocksLength;rockCtr>=0;rockCtr--){

         tempRock = rocks[rockCtr];
         var angleInRadians  = tempRock.rotation * Math.PI / 180;
         //ConsoleLog.log("render rock rotation"+(tempRock.rotation));
         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(tempRock.x+tempRock.halfWidth,
                           tempRock.y+tempRock.halfHeight);

         //ConsoleLog.log("render rock x"+(tempRock.x+tempRock.halfWidth));
         //ConsoleLog.log("render rock y"+(tempRock.y+tempRock.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(-(tempRock.halfWidth-1),-(tempRock.halfHeight-1));
         context.lineTo((tempRock.halfWidth-1),-(tempRock.halfHeight-1));
         context.lineTo((tempRock.halfWidth-1),(tempRock.halfHeight-1));
         context.lineTo(-(tempRock.halfWidth-1),(tempRock.halfHeight-1));
         context.lineTo(-(tempRock.halfWidth-1),-(tempRock.halfHeight-1));

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

      }
   }

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

         context.save(); //save current state in stack
         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.translate(tempSaucer.x,tempSaucer.y);
         context.strokeStyle = '#ffffff';

         context.beginPath();

         //did not move to middle because it is drawn in exact space

         context.moveTo(4,0);
         context.lineTo(9,0);
         context.lineTo(12,3);
         context.lineTo(13,3);
         context.moveTo(13,4);
         context.lineTo(10,7);
         context.lineTo(3,7);
         context.lineTo(1,5);
         context.lineTo(12,5);
         context.moveTo(0,4);
         context.lineTo(0,3);
         context.lineTo(13,3);
         context.moveTo(5,1);
         context.lineTo(5,2);
         context.moveTo(8,1);
         context.lineTo(8,2);
         context.moveTo(2,2);
         context.lineTo(4,0);

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

   function renderSaucerMissiles() {
      var tempSaucerMissile = {};
      var saucerMissileLength = saucerMissiles.length-1;
      //ConsoleLog.log("saucerMissiles= " + saucerMissiles.length)

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

         //ConsoleLog.log("draw player missile " + playerMissileCtr)
         tempSaucerMissile = saucerMissiles[saucerMissileCtr];
         context.save(); //save current state in stack
         context.setTransform(1,0,0,1,0,0); // reset to identity

         //translate the canvas origin to the center of the player
         context.translate(tempSaucerMissile.x+1,tempSaucerMissile.y+1);
         context.strokeStyle = '#ffffff';

         context.beginPath();

         //draw everything offset by 1/2. Zero Relative 1/2 is 15
         context.moveTo(-1,-1);
         context.lineTo(1,-1);
         context.lineTo(1,1);
         context.lineTo(-1,1);
         context.lineTo(-1,-1);
         context.stroke();
         context.closePath();
         context.restore(); //pop old state on to screen

      }
   }

   function renderParticles() {

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

         //translate the canvas origin to the center of the player
         context.translate(tempParticle.x,tempParticle.y);
         context.strokeStyle = '#ffffff';

         context.beginPath();

         //draw everything offset by 1/2. Zero Relative 1/2 is 15
         context.moveTo(0,0);
         context.lineTo(1,1);
         context.stroke();
         context.closePath();
         context.restore(); //pop old state on to screen

      }

   }

   function checkCollisions() {

      //loop through rocks then missiles. There will always be rocks and a ship,
      //but there will not always be missiles.

      var tempRock = {};
      var rocksLength = rocks.length-1;
      var tempPlayerMissile = {};
      var playerMissileLength = playerMissiles.length-1;
      var saucerLength = saucers.length-1;
      var tempSaucer = {};
      var saucerMissileLength = saucerMissiles.length-1;

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

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

            tempPlayerMissile = playerMissiles[playerMissileCtr];

            if (boundingBoxCollide(tempRock,tempPlayerMissile)){
               //ConsoleLog.log("hit rock");

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

               if (tempRock.scale<3) {
                  splitRock(tempRock.scale+1, tempRock.x, tempRock.y);
               }
               addToScore(tempRock.scoreValue);
               playerMissiles.splice(playerMissileCtr,1);
               tempPlayerMissile = null;

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


               break rocks;
               break missiles;
            }
         }

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

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

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

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

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

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

               break rocks;
               break saucers;
            }
         }
         //saucer missiles against rocks
         //this is done here so we don't have to loop
         //through rocks again as it would probably
         //be the biggest array

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

            tempSaucerMissile = saucerMissiles[saucerMissileCtr];

            if (boundingBoxCollide(tempRock,tempSaucerMissile)){
               //ConsoleLog.log("hit rock");

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

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

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

               break rocks;
               break saucerMissiles;
            }
         }

         //check player aginst rocks

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

            playerDie();
         }
      }

      //now check player against saucers and then saucers against player missiles
      //and finally player against saucer missiles

      playerMissileLength = playerMissiles.length-1;
      saucerLength = saucers.length-1;
      saucers:for (var saucerCtr=saucerLength;saucerCtr>=0;saucerCtr--){
         tempSaucer = saucers[saucerCtr];

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

            tempPlayerMissile = playerMissiles[playerMissileCtr];

            if (boundingBoxCollide(tempSaucer,tempPlayerMissile)){
            //ConsoleLog.log("hit rock");

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

               addToScore(tempSaucer.scoreValue);

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

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

               break saucers;
               break missiles;
            }
         }

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

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

            playerDie();
         }
      }

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

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

         tempSaucerMissile = saucerMissiles[saucerMissileCtr];

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

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

            break saucerMissiles;
         }
      }
   }

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

   function fireSaucerMissile(saucer) {
      var newSaucerMissile = {};
      newSaucerMissile.x = saucer.x+.5*saucer.width;
      newSaucerMissile.y = saucer.y+.5*saucer.height;

      newSaucerMissile.width = 2;
      newSaucerMissile.height = 2;
      newSaucerMissile.speed = saucer.missileSpeed;

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

   function playerDie() {
      ConsoleLog.log("player die");
      createExplode(player.x+player.halfWidth, player.y+player.halfWidth,50);
      switchGameState(GAME_STATE_PLAYER_DIE);

   }

   function createExplode(x,y,num) {
      //create 10 particles
      for (var partCtr=0;partCtr<num;partCtr++){
         var newParticle = new Object();
         newParticle.dx = Math.random()*3;
            if (Math.random()<.5){
               newParticle.dx*=-1;
            }
         newParticle.dy = Math.random()*3;
         if (Math.random()<.5){
            newParticle.dy*=-1;
         }

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

   }

   function boundingBoxCollide(object1, object2) {

      var left1 = object1.x;
      var left2 = object2.x;
      var right1 = object1.x + object1.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);

   };

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

         if (scale==2){
            newRock.scoreValue = medRockScore;
            newRock.width = 25;
            newRock.height = 25;
            newRock.halfWidth = 12.5;
            newRock.halfHeight = 12.5;

         }else {
            newRock.scoreValue = smlRockScore;
            newRock.width = 16;
            newRock.height = 16;
            newRock.halfWidth = 8;
            newRock.halfHeight = 8;
         }

         newRock.scale = scale;
         newRock.x = x;
         newRock.y = y;
         newRock.dx = Math.random()*3;
         if (Math.random()<.5){
            newRock.dx*=-1;
         }
         newRock.dy = Math.random()*3;
         if (Math.random()<.5){
            newRock.dy*=-1;
         }
         newRock.rotationInc = (Math.random()*5)+1;
         if (Math.random()<.5){
            newRock.rotationInc*=-1;
         }
         newRock.rotation = 0;
         ConsoleLog.log("new rock scale"+(newRock.scale));
         rocks.push(newRock);

      }

   }

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

   document.onkeydown = function(e){

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

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

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

}

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

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

}

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

//*** end console log object

//*** 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;
}
</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="400" height="400">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

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

Geo Blaster Basic in action
Figure 8-7. Geo Blaster Basic 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-13 is a Rock prototype object that can be used in a game such as Geo Blaster Basic.

Example 8-13. 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

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

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.

Get HTML5 Canvas now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.