Share via


HTML5 game skeleton code for Boulder Bop

When thinking about how to structure an HTML5 game, it can help to look at an example. Let's look at the basic structure of Boulder Bop to get some ideas.

To get started, review the Boulder Bop skeletal code:

  • Boulder Bop Skeleton

    When you view the code, click the game's Start and Sound buttons with the console window open (see Introduction to F12 Developer Tools). We've added a series of useful console.log statements to make it easier for you to see how the skeletal code works.

Note  In order to simplify the code, Boulder Bop uses setInterval instead of requestAnimationFrame. Because requestAnimationFrame offers important benefits, we'll look specifically at how to convert the game from setInterval to requestAnimationFrame in the Replacing setInterveral with requestAnimationFrame in a HTML5 game section.

 

To start, we'll begin with Boulder Bop Skeleton's markup.

Markup

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="X-UA-Compatible" content="IE=10" />
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
  <title>Boulder Bop Skeleton</title>
  <style>
    html {
      height: 100%; /* Required for liquid/fluid layout. */
      margin: 0;
      padding: 0;     
    }
    
    body {
      height: 100%; /* Required for liquid/fluid layout. */
      margin: 0;
      padding: 0;
      background-color: #056;
    }    

    svg {
      margin: 0;
      padding: 0;    
    }
    
    .metroButton {
      fill: #09F;
      cursor: pointer;
    }
    
    .metroButton:hover {
      fill: #666;
    }    
    
    .metroText {
      fill: white;
      font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
    }          
  </style>
</head>

<body>
  <svg width="100%" height="100%" viewbox="0 0 1200 800">
    <defs>
      <g id="buttonGraphic">
        <rect x="0" y="0" width="105" height="40" /> <!-- Note that this could be replaced with a much more complicated and colorful graphic. -->
      </g> 
      
      <lineargradient id="skyGradient" x1="0" y1="0" x2="0" y2="100%">
        <stop offset="0%" style="stop-color: darkblue;" />      
        <stop offset="73%" style="stop-color: #00BFFF;" />
        <stop offset="87%" style="stop-color: #87CEFA" />        
        <stop offset="100%" style="stop-color: #999" />                
      </lineargradient>        
    </defs>    
        
    <g transform="translate(0, 10)"> <!-- Define the y-coordinate for the row of buttons and status indicators so that they can be moved vertically as a group. -->
      <g id="startButton" class="metroButton" transform="translate(10)">
        <use xlink:href="#buttonGraphic" /> <!-- "xlink:" is required for WebKit browsers. -->
        <text class="metroText" x="22" y="31" font-size="30">Start</text>
      </g>
      
      <g id="soundButton" class="metroButton" transform="translate(130)">
        <use xlink:href="#buttonGraphic" />
        <text class="metroText" x="10" y="31" font-size="30">Sound</text>
      </g>
      
      <a id="infoButton" class="metroButton" xlink:href="boulderBopInfo.html" transform="translate(250)">
        <use xlink:href="#buttonGraphic" />
        <text class="metroText" x="28" y="31" font-size="30">Info</text>
      </a>

      <g id="level" transform="translate(855)">
        <text class="metroText" x="30" y="32" font-size="30">Level: 1</text>
      </g>       
      
      <g id="score" transform="translate(1028)">
        <text class="metroText" x="9" y="32" font-size="30">Score: 0</text>
      </g>         
    </g>  
    <clipPath id="playingFieldClipPath">
      <rect x="0" y="0" width="1180" height="725" />
    </clipPath>                 
    <g id="playingField" transform="translate(10, 60)" clip-path="url(#playingFieldClipPath)">  <!-- Game objects are always on top of the background. -->
      <rect id="gameBackground" x="0" y="0" width="1180" height="725" fill="url(#skyGradient)" stroke="white" stroke-width="4" />      
      <g id="activeGameObjectsContainer"><!-- Active game objects injected via JavaScript here. --></g>      
      <rect id="gameClickPlane" width="1180" height="725" opacity="0"></rect> <!-- Always on top so it can capture all click events. -->
      <g id="staticGameObjectsContainer"><!-- Static game objects (like bunkers) injected via JavaScript here. --></g> <!-- Note that it makes no sense to click a bunker and bunkers must always be on top. -->    
    </g>
  </svg>
  <script>
    var FPS = 60; // The frames per second for the animations. Anything below 60 starts to look jerky. Adjusting this value does not effect the action-speed of the game.
      .
      .
      .
  </script>
</body>

</html>

The first item of note is this CSS:

html {
  height: 100%; /* Required for liquid/fluid layout. */
  margin: 0;
  padding: 0;     
}
    
body {
  height: 100%; /* Required for liquid/fluid layout. */
  margin: 0;
  padding: 0;
  background-color: #056;
}

In particular, the two height: 100% values along with <svg width="100%" height="100%" ...> are the key to SVG liquid layout.

Note  Liquid layout is useful because the page can adjust itself to accommodate whatever form factor (such as phone) is being used to view the page. This includes the four possible view states of a HTML5 Windows Store app.

 

In this next example, we first define a clipping path so that game objects that "escape" from the game's "playing field" aren't visible.

<clipPath id="playingFieldClipPath">
  <rect x="0" y="0" width="1180" height="725" />
</clipPath>                 
<g id="playingField" transform="translate(10, 60)" clip-path="url(#playingFieldClipPath)">  <!-- Game objects are always on top of the background. -->
  <rect id="gameBackground" x="0" y="0" width="1180" height="725" fill="url(#skyGradient)" stroke="white" stroke-width="4" />      
  <g id="activeGameObjectsContainer"><!-- Active game objects injected via JavaScript here. --></g>      
  <rect id="gameClickPlane" width="1180" height="725" opacity="0"></rect> <!-- Always on top so it can capture all click events. -->
  <g id="staticGameObjectsContainer"><!-- Static game objects (like bunkers) injected via JavaScript here. --></g> <!-- Note that it makes no sense to click a bunker and bunkers must always be on top. -->    
</g>

Then, we define the playingField ``<g> element, set its origin at (10, 60) (relative to the SVG viewport), and apply the clipping path:

<g id="playingField" transform="translate(10, 60)" clip-path="url(#playingFieldClipPath)">

Recall that the last SVG element in markup is the top-most SVG graphic drawn on the screen. Thus, the first playingField child defines the background - a rectangular region using the skyGradient linear gradient:

<rect id="gameBackground" x="0" y="0" width="1180" height="725" fill="url(#skyGradient)" stroke="white" stroke-width="4" />

Now all of the game objects (created in the future) will appear on top of this background.

The next child (activeGameObjectsContainer) and last child(staticGameObjectsContainer) determine the order (in the z-index sense of the word) that game objects are attached to the DOM:

<g id="activeGameObjectsContainer"><!-- Active game objects injected via JavaScript here. --></g>      
<rect id="gameClickPlane" width="1180" height="725" opacity="0"></rect> <!-- Always on top so it can capture all click events. -->
<g id="staticGameObjectsContainer"><!-- Static game objects (like bunkers) injected via JavaScript here. --></g> <!-- Note that it makes no sense to click a bunker and bunkers must always be on top. -->

Because activeGameObjectsContainer appears first, all its children are drawn below the contents of staticGameObjectsContainer. For example, if a bunker image is a child of staticGameObjectsContainer and a missile object is a child of activeGameObjectsContainer, when the bunker fires a missile (towards the sky), the missile is initially drawn below the bunker image and only becomes visible as it exits the bunker image.

With the middle gameClickPlane rectangle, we can capture all playing field click events except for those that occur on top of static game objects. For example, clicking a bunker image should not fire a missile because that could cause the missile to explode within the bunker. To reiterate, the purpose of the gameClickPlane "layer" is to convert the screen coordinates from a tap or mouse click to playing field coordinates. This conversion allows a missile to be sent from the closest bunker to that point.

Next, let's look at the JavaScript for the Boulder Bop Skeleton.

JavaScript

Ignoring the JavaScript FPS "constant", all would-be global variables are stored in one single global location:

var globals = {};

The two properties of this variable are globals.helpers and globals.game. The event handlers are registered by a self-executing, anonymous function ((function() {...})();).

When writing the code, an attempt was made to isolate all game related functionality to globals.game. All other "helper" code (less event handler registration) is contained in globals.helpers. For example, determining the distance between two SVG objects is not game specific per se. So, this code (euclideanDistance) is contained within the globals.helpers group as opposed to the globals.game group.

Of these three items (globals.helpers, globals.game, and the event registration), let's look at the simplest first.

Event registration

To avoid contaminating the global namespace unnecessarily, event registration is handled by a self-executing anonymous function:

(function() {
  document.getElementById('startButton').addEventListener('click', handleStartButton, false); // Single touch events are transformed into click events in IE10 and above.
  document.getElementById('soundButton').addEventListener('click', handleSoundButton, false);
  document.getElementById('infoButton').addEventListener('click', handleInfoButton, false);
  document.getElementById('gameClickPlane').addEventListener('click', handleGameClickPlane, false);
  document.getElementsByTagName('svg')[0].addEventListener('dragstart', function(evt) { evt.preventDefault(); }, false); // Don't let the user drag the screen in that it's very easy to mistakenly drag the screen when playing the game at speed.

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  function handleStartButton() {
    console.log("handleStartButton() called");
    var game = globals.game;

    if (game.getState().over) {
      game.init(); // Note that this sets spec.paused to true.
    }

    if (game.getState().paused) {
      game.run();
      document.querySelector('#startButton text').style.textDecoration = "line-through";
    }
    else {
      game.pause();
      document.querySelector('#startButton text').style.textDecoration = "none";
    }
  } // handleStartButton

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/
      
  function handleSoundButton() {
    console.log("handleSoundButton() called");
    var game = globals.game;

    if (game.getState().sound) {
      game.setSound('mute'); // Mute any currently playing sounds.
      game.setState('sound', false); // Do not create any audio objects moving forward.
      document.querySelector('#soundButton text').style.textDecoration = "line-through";
    }
    else {
      game.setState('sound', true); // Create audio objects moving forward.
      document.querySelector('#soundButton text').style.textDecoration = "none";
    }
  } // handleSoundButton

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/
      
  function handleInfoButton() {
    console.log("handleInfoButton() called");
    if (!globals.game.getState().paused) { // If the game is not paused, then it's running - so pause it.
      handleStartButton(); // Pause the game whilst the user looks at the info on the info webpage.
    }
  } // handleInfoButton

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  function handleGameClickPlane(evt) {
    console.log("handleGameClickPlane() fired, (" + evt.pageX + ", " + evt.pageY + ")");
  } // handleGameClickPlane
})(); // Execute this anonymous function now, thereby wiring up all event handlers.

The five event handlers are as follows:

document.getElementById('startButton').addEventListener('click', handleStartButton, false); // Single touch events are transformed into click events in IE10 and above.
document.getElementById('soundButton').addEventListener('click', handleSoundButton, false);
document.getElementById('infoButton').addEventListener('click', handleInfoButton, false);
document.getElementById('gameClickPlane').addEventListener('click', handleGameClickPlane, false);
document.getElementsByTagName('svg')[0].addEventListener('dragstart', function(evt) { evt.preventDefault(); }, false); // Don't let the user drag the screen in that it's very easy to mistakenly drag the screen when playing the game at speed.

Let's look at each handler individually:

function handleStartButton() {
  var game = globals.game;

  if (game.getState().over) {
    game.init(); // Note that this sets spec.paused to true.
  }
        
  if (game.getState().paused) {
    game.run();
    document.querySelector('#startButton text').style.textDecoration = "line-through";
  }
  else {
    game.pause();
    document.querySelector('#startButton text').style.textDecoration = "none";
  }
} // handleStartButton

When the page initially loads, the game is not running (meaning, game.getState().paused is true). The user must click the Start button to start the game, invoking game.run(). The Start button also doubles as a pause button - a paused game is indicated by placing a line through the "Start" text.

We next move onto handleSoundButton:

function handleSoundButton() {
  console.log("handleSoundButton() called");
  var game = globals.game;

  if (game.getState().sound) {
    game.setSound('mute'); // Mute any currently playing sounds.
    game.setState('sound', false); // Do not create any audio objects moving forward.
    document.querySelector('#soundButton text').style.textDecoration = "line-through";
  }
  else {
    game.setState('sound', true); // Create audio objects moving forward.
    document.querySelector('#soundButton text').style.textDecoration = "none";
  }
} // handleSoundButton

As can be seen, the handleSoundButton function is similar to the handleStartButton function and can be understood via handleSoundButton's comments.

Here's the handleInfoButton event handler:

function handleInfoButton() {
  console.log("handleInfoButton() called");
  if (!globals.game.getState().paused) { // If the game is not paused, then it's running - so pause it.
    handleStartButton(); // Pause the game whilst the user looks at the info on the info webpage.
  }
} // handleInfoButton

If the game is running, the handleInfoButton function pauses the game before showing the user the info page by calling handleStartButton (which pauses a running game). The game's info page is displayed to the user via a standard anchor tag: <a id="infoButton" class="metroButton" xlink:href="boulderBopInfo.html" transform="translate(250)">...</a> (as of this writing, xlink is required for other browsers).

Next, we discuss the handleGameClickPlane event handler:

  function handleGameClickPlane(evt) {
    console.log("handleGameClickPlane() fired, (" + evt.pageX + ", " + evt.pageY + ")");
  } // handleGameClickPlane
})(); // Execute this anonymous function now, thereby wiring up all event handlers.

At this stage, the handleGameClickPlane event handler just shows (to the console window) where the user clicked (in screen coordinates).

Here's the last event handler:

document.getElementsByTagName('svg')[0].addEventListener('dragstart', function(evt) {
  evt.preventDefault();
}, false);

During rapid game play (i.e., quick clicking or tapping), it's easy to inadvertently induce a click and drag (swipe) motion across the screen. Because this can be detrimental to game play, we prevent this default browser behavior using preventDefault().

Next, we discuss skeletal game specific code.

Globals.game

Because Boulder Bop's game objects (in JavaScript) share some common characteristics, using some form of inheritance might simplify and/or speed development. Boulder Bop uses the functional inheritance pattern. Look at this code example to get an idea of how this pattern works:

var globals = {};

globals.game = (function() {
  var that = {};

  var PrimaryObject = function(spec) { // Private method.
    var that = {};

    /*
      Define the methods and properties for the primary object, taking the 
      values of the spec parameter into account.
    */

    return that;
  } // PrimaryObject

  var SecondaryObject = function() { // Public method.
    // Specify the characteristics of a secondary object:
    var spec = {type: "second", state: "active"}; 
    
    // Call the PrimaryObject constructor:
    var that = PrimaryObject(spec);

    /*
      Modify the "that" variable's methods and properties as inherited 
      from the PrimaryObject constructor.
    */

    return that;
  } // SecondaryObject

  that.SecondaryObject = SecondaryObject; // Make this method public.

  return that; // This is the "that" that ends up in globals.game.
})();

Here, the PrimaryObject constructor typically defines the properties and methods that are to be inherited by objects that call it. For example, the SecondaryObject constructor calls the PrimaryObject constructor in order to inherit all of its properties and methods:

// Specify the characteristics of a secondary object:
var spec = {type: "second", state: "active"}; 
    
// Call the PrimaryObject constructor:
var that = PrimaryObject(spec);

Additionally, the functional inheritance pattern allows for public and private methods. For example, the SecondaryObject constructor can be called from globals.game because of the line that.SecondaryObject = SecondaryObject. The PrimaryObject constructor, however, cannot be called from globals.game precisely because it lacks such a line.

Now, we move on to the details of the code contained in globals.game:

globals.game = (function (spec) { // The spec parameter is used as a local static variable in a number of the following methods.
  var _updateID = null;
  var _objects = []; // Will contain all of the game's objects.
  var _gameClickPlaneWidth = document.getElementById('gameClickPlane').width.baseVal.value; // In SVG user units.
  var _gameClickPlaneHeight = document.getElementById('gameClickPlane').height.baseVal.value; // In SVG user units.
  var _scoreBox = document.getElementById('score').firstElementChild; // A very small performance optimization (i.e., do this once).
  var _levelBox = document.getElementById('level').firstElementChild; // A very small performance optimization (i.e., do this once).      
  var that = {}; // The object returned by the Game "constructor".

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var GameObject = function (spec) { // Private constructor. The base game object constructor - all game objects stem from this constructor.
    var that = {};

    that.active = spec.active;
    if (typeof (that.active) == "undefined" || that.active == null) {
      that.active = true;
    }

    that.type = spec.type || 'unknown';
    that.core = spec.core || null;

    that.x = spec.x; // The initial position of the game object.
    that.y = spec.y;

    if (getState().sound) { // True if the user wants sound to be played. False otherwise.
      that.audio = new Audio(spec.soundPath); // spec.soundPath is a path to an MP3 file.
      if (that.audio) { // If a valid spec.soundPath path was supplied, play the sound effect for the game object (otherwise don't).
        that.audio.preload = "auto"; // Attempt to reduce audio latency.
        that.audio.play();
      } // if
    } // if

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var setPosition = function (x, y) { // Public method.
      that.x = x || 0;
      that.y = y || 0;
    }; // Game.GameObject.setPosition()

    that.setPosition = setPosition;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var getPosition = function () { // Public.
      return globals.helpers.createPoint(that.x, that.y); // In the form of an SVG point object, returns the position of this game object's position.
    }; // Game.GameObject.getPosition()

    that.getPosition = getPosition; // Make the method public.

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var move = function () { // Public. The default move() method is a NOP.
    }; // Game.GameObject.move()

    that.move = move;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var collisionDetection = function () { // Public. The default collisionDetection() method is a NOP in that some of the objects created using GameObject() do not move.
    }; // Game.GameObject.collisionDetection()

    that.collisionDetection = collisionDetection;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var exited = function () { // Public.
      return (!that.active); // It's definitely true that a non-active game object has logically exited the game.
    }; // Game.GameObject.exited()

    that.exited = exited;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var remove = function () { // Public.
      var arrayIndex = _objects.indexOf(that); // Return the array index associated with this game object.
      var core = that.core;

      if (arrayIndex == -1) {
        console.log('Error in GameObject.remove(), "that" = ' + that + ' object not found in _objects[], arrayIndex = ' + arrayIndex); // Defensive programming good!
        return;
      }

      var removeChild = core.parentNode.removeChild(core); // Remove the game object from the DOM, and thus from the screen.
      if (!removeChild) {
        console.log('Error in GameObject.remove(), "that" = ' + that + ' object was not removed from DOM'); // Defensive programming good!
        return;
      } // if

      _objects.splice(arrayIndex, 1); // Remove the game object from the array of game objects.                 
    }; // Game.GameObject.remove()

    that.remove = remove;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    var markActive = function (Boolean) { // Public.
      that.active = Boolean; // In honor of George Boole, "Boolean" is capitalized (and also because "boolean" is a reserved word).
    }; // Game.GameObject.markActive()

    that.markActive = markActive;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    return that; // Return the result of calling the GameObject constructor.
  } // Game.GameObject()      

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var init = function () { // Public.
    console.log("init() called");
    setState("over", false);
    setState("paused", true);
    setState("score", 0);
    setState("level", 1);
  }; // Game.init()

  that.init = init;

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var pause = function () { // Public.
    console.log("pause() called");
    setSound('pause'); // Pause any playing sound effects.        
    window.clearInterval(_updateID);
    spec.paused = true; // This is the spec parameter that is passed into the "Game constructor".
  }; // Game.pause()

  that.pause = pause;

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var update = function () { // Private. Update the position of the game objects, etc. 
    console.log("update() called");
    for (var i = 0; i < _objects.length; i++) { // Must use _objects.length in that the length of objects can dynamically change.
      _objects[i].move(); // Move the game object in a way that only this type of game object knows how to do.
      _objects[i].collisionDetection();
      if (_objects[i].exited()) {
        _objects[i].remove(); // This must be the last thing called in the FOR loop.
      } // if
    } // for                   
  }; // Game.update()

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var run = function () { // Public. Invoked when the Start button is clicked.
    console.log("run() called");
    setSound('play'); // Play any previously paused sound effects.        

    var msPerFrame = 1000 / spec.fps; // (1000 ms / 1 s ) / (spec.fps frames / 1 s) = the number of milliseconds per frame.
    _updateID = window.setInterval(update, msPerFrame); // Using setInterval (as opposed to requestAnimationFrame) to ensure precise frames per second rate for the game.

    spec.paused = false; // This is the spec parameter that is passed into the "Game constructor".
  }; // Game.run()

  that.run = run; // Make run() a public method.

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var getState = function () { // Public.
    console.log("getState() called");
    return {
      fps: spec.fps, // This is the spec parameter passed into the "Game constructor".
      score: spec.score,
      level: spec.level,
      paused: spec.paused,
      over: spec.over, // A Boolean indicating if the game is over or not.
      sound: spec.sound // A Boolean indicating if the game should have sound or not.          
    }; // When called, returns an object that provides the current state of the game.
  }; // Game.getState()

  that.getState = getState;

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var setState = function (stateItem, value) { // Public.
    console.log("setState() called");
    switch (stateItem) {
      case "fps":
        spec.fps = value;
        break;
      case "score":
        spec.score = value;
        _scoreBox.textContent = "Score: " + spec.score;
        break;
      case "level":
        spec.level = value;
        _levelBox.textContent = "Level: " + spec.level;
        break;
      case "paused":
        spec.paused = value; // A Boolean value.
        break;
      case "over":
        spec.over = value; // A Boolean value indicating if the game is over or not.
        break;
      case "sound":
        spec.sound = value; // A Boolean value indicating if the game should have sound or not.
        break;
      default:
        console.log("Error in switch of setState()");
    } // switch
  }; // Game.setState()

  that.setState = setState;

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  var setSound = function (action) { // Public.
    console.log("setSound() called");
    for (var i = 0; i < _objects.length; i++) {
      if (_objects[i].audio) {
        switch (action) {
          case "mute":
            _objects[i].audio.muted = true;
            break;
          case "play":
            _objects[i].audio.play();
            break;
          case "pause":
            _objects[i].audio.pause();
            break;
          case "load":
            _objects[i].audio.load();
            break;
          default:
            console.log("Error in switch of setSound()");
        } // switch
      } // if
    } // for
  } // Game.setSound()

  that.setSound = setSound;

  /*---------------------------------------------------------------------------------------------------------------------------------------------------------*/

  return that; // The object returned by the Game() constructor.
})({ fps: FPS, sound: true }); // Execute this anonymous constructor now, while passing in the constructor's initial state.

The primary data structure used for the game is the _objects[] array. All game objects are stored within this array but the array does not grow without bound in that as game objects leave the playing field, they are removed from _objects[]. Although not shown in Boulder Bop Skeleton, as game objects are created, they are added to the game by pushing them into _objects[] using the array's native push method and removed using splice.

With a number of game objects in _objects[], update is used to invoke the various game object methods (move, collisionDetection, exited, and remove), thereby executing the game:

var update = function() { // Private. Update the position of the game objects, etc. 
  console.log("update() called");
  for (var i = 0; i < _objects.length; i++) { // Must use _objects.length in that the length of objects can dynamically change.
    _objects[i].move(); // Move the game object in a way that only this type of game object knows how to do.
    _objects[i].collisionDetection();
    if (_objects[i].exited()) {
      _objects[i].remove(); // This must be the last thing called in the FOR loop.
    } // if
  } // for                   
}; // Game.update()

This approach requires that each game object have (at least) the four methods move, collisionDetection, exited, and remove, even if some of them are null functions ( NOPs).

This approach is nice because each game object knows what move, collisionDetection, exited, and remove should do for itself. For example, if _objects[2] is based on an SVG circle, _objects[2].move() manipulates the circle's cx.baseVal.value and cy.baseVal.value values. Similarly, if _objects[3] is an SVG image, its move method manipulates the image's x.baseVal.value and y.baseVal.value values, and so forth.

The update function itself is called repeatedly (based on the FPS value) by setInterval:

var run = function() { // Public. Invoked when the Start button is clicked.
  console.log("run() called");
  setSound('play'); // Play any previously paused sound effects.        
      
  var msPerFrame = 1000 / spec.fps; // (1000 ms / 1 s ) / (spec.fps frames / 1 s) = the number of milliseconds per frame.
  _updateID = window.setInterval(update, msPerFrame); // Using setInterval (as opposed to requestAnimationFrame) to ensure precise frames per second rate for the game.
        
  spec.paused = false; // This is the spec parameter that is passed into the "Game constructor".
}; // Game.run()

that.run = run; // Make run() a public method.

Assuming the game is in a paused state (which is its initial condition due to setState("paused", true) in the init function), the run function is executed when the Start button is clicked. We've copied the code here for another look:

function handleStartButton() {
  console.log("handleStartButton() called");
  var game = globals.game;

  if (game.getState().over) {
    game.init(); // Note that this sets spec.paused to true.
  }

  if (game.getState().paused) {
    game.run();
    document.querySelector('#startButton text').style.textDecoration = "line-through";
  }
  else {
    game.pause();
    document.querySelector('#startButton text').style.textDecoration = "none";
  }
} // handleStartButton

Using the functional inheritance pattern, the GameObject constructor acts as the parent of all game objects. That is, all game object constructors call GameObject to inherit its various properties (active, type, core, x, y, etc.) and methods (getPosition, setPosition, move, collisionDetection, etc.) For example, the Missile constructor could take advantage of this as follows:

var Missile = function(spec) { // Private.
  var that = GameObject(spec);
        
  that.core = globals.helpers.createCircle(spec);
  that.setPosition(that.core.cx.baseVal.value, that.core.cy.baseVal.value); // Set the initial position of the missile.

  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        
  var exited = function() { // Public.
    .
    .
    .
  }; // Game.Missile.exited()
        
  that.exited = exited;        

  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

  var move = function() { // Public.
    .
    .
    .
  }; // Game.Missile.move()

  that.move = move;

  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        
  return that; // Return the object created by the Missile constructor.
}; // Game.Missile()

Here we see that the Missile constructor defines its own exited and move methods but inherits everything else from the GameObject constructor (like the remove method).

Globals.helpers

The helper functions are all straightforward with the possible exception of coordinateTransform, which converts screen coordinates to game coordinates. That is, coordinateTransform converts the position of where the user clicked or tapped (in screen coordinates) to its analogous position in the game's playing field (in game coordinates). For more info, see SVG Coordinate Transformations. The rest of the skeletal code is relatively straightforward - you should be able to understand it by reading the associated code comments.

Now, let's walk through Boulder Bop Skeleton's flow of control:

  1. The markup is parsed, setting up the DOM.

  2. The script block is parsed, setting up globals.helpers, globals.game, and running the anonymous event registering function.

  3. globals.game.init() is invoked, which sets the game's initial state to not-over, paused, on level 1, with a score of 0:

    var init = function() { // Public.
      console.log("init() called");
      setState("over", false);
      setState("paused", true);
      setState("score", 0);
      setState("level", 1);
    }; // Game.init()
    
    that.init = init;
    
  4. When the user clicks the Start button, game.run() is executed, which invokes the setInterval call that repeatedly executes update - and the game begins.

In the next section, Animating a 2D SVG HTML5 game, we cover the basics of SVG animation (which is typically easier than its canvas analog).