Cover by Jeff Fulton, Steve Fulton

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

O'Reilly logo

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

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required