Поделиться через


HTML5 game control flow

Because Boulder Bop is somewhat complex, it can be helpful to walk through a typical flow of control scenario for the game, which is just what this topic does.

Now, consider the typical scenario where a user loads the page, clicks the game's Start button, and shoots a missile toward a descending boulder. Flow of control for this scenario looks like this:

  1. Page loads.
  2. Bunkers are drawn.
  3. Start button is clicked.
  4. Boulders descend toward bunkers.
  5. User fires missile toward descending boulder.
  6. Missile explodes at click point.
  7. Boulder is destroyed by missile explosion.

1. Page loads

The markup is parsed, drawing the game's linear gradient background in the process.

Next, the script block is parsed, setting up globals.helpers, globals.game, and running the anonymous event registering function (see HTML5 game skeleton code for Boulder Bop).

Finally, globals.game.init() is invoked, as discussed next.

2. Bunkers are dawn

As suggested by it's name, globals.game.init() initializes the game:

var init = function() { // Public.
  // Note that one could, if so inclined, cache (preload) the game's images here.
  var rightBunker = 86.5; // Given the image's width, 86.5% places the image on the far right of the viewport.
  var middleBunker = rightBunker / 2; // Center the image within the viewport.
  var leftBunker = 0; // 0% places the image on the left of the viewport.
  var offset = 10 // Offset the left and right images by this percentage value.

  for (var i = 0; i < _objects.length; i++) { // Remove all lingering game objects from the last game, if present (and they usually are).
    if (_objects[i]) {
      _objects[i].markActive(false);
      if (_objects[i].audio) { // As a saftey precaution, mute any potentially playing sound effects (defensive programming).
        _objects[i].audio.muted = true;
      } // if
    } // if
  } // for
        
  setState("over", false);
  setState("paused", true);
  setState("score", 0);
  setState("level", 1);

  rightBunker = rightBunker - offset + "%";
  middleBunker += "%";
  leftBunker = leftBunker + offset + "%";
        
  var bunkerSpec = {};
  bunkerSpec.type = 'bunker';
  bunkerSpec.image = BUNKER_IMAGE; // Ignored by GameObject(spec).
  bunkerSpec.movable = false; // Bunkers are not movable.
  bunkerSpec.y = "82%";
  bunkerSpec.width = 142;
  bunkerSpec.height = 64;

  bunkerSpec.id = 'rightBunker';
  bunkerSpec.x = rightBunker;
  _objects.push(Bunker(bunkerSpec));
        
  bunkerSpec.id = 'middleBunker';
  bunkerSpec.x = middleBunker;
  _objects.push(Bunker(bunkerSpec));
        
  bunkerSpec.id = 'leftBunker';
  bunkerSpec.x = leftBunker;
  _objects.push(Bunker(bunkerSpec));
        
  createBoulders(spec.level); // The total number of boulders (of death) that fall from the sky is a function of the game's level.
}; // Game.init()

that.init = init;

First, some local variables are set up to ease the positioning of the bunkers, then the for loop removes any lingering game objects from the last game (if applicable) and turns off any playing audio objects (if applicable):

for (var i = 0; i < _objects.length; i++) { // Remove all lingering game objects from the last game, if present (and they usually are).
  if (_objects[i]) {
    _objects[i].markActive(false);
    if (_objects[i].audio) { // As a saftey precaution, mute any potentially playing sound effects (defensive programming).
      _objects[i].audio.muted = true;
    } // if
  } // if
} // for

The markActive method simply sets the active property of the game object to false (the active property is used throughout the codebase). The custom audio property contains an HTML5 audio object, which exposes the native muted property, allowing us to mute the game object's sound.

The setState function is used to initialized the game's state to not-over, paused, on level 1, with a score of 0:

setState("over", false);
setState("paused", true);
setState("score", 0);
setState("level", 1);

Based on the value of it's two parameters, setState sets various spec properties and can update the game's score and level displays:

var setState = function(stateItem, value) { // Public.
  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;

Be aware that the spec seen in the switch statement above is the parameter passed into the anonymous constructor that returns its constructed object to globals.game:

globals.game = (function(spec) {
  .
  .
  .
})({fps: FPS, sound: true});

Because of this, spec acts as a static local variable and is within scope to all of this anonymous constructor's internal functions.

Next, init creates three bunker objects using the Bunker constructor and pushes them into the _objects[] array:

var bunkerSpec = {};
bunkerSpec.type = 'bunker';
bunkerSpec.image = BUNKER_IMAGE; // Ignored by GameObject(spec).
bunkerSpec.movable = false; // Bunkers are not movable.
bunkerSpec.y = "82%";
bunkerSpec.width = 142;
bunkerSpec.height = 64;

bunkerSpec.id = 'rightBunker';
bunkerSpec.x = rightBunker;
_objects.push(Bunker(bunkerSpec));
        
bunkerSpec.id = 'middleBunker';
bunkerSpec.x = middleBunker;
_objects.push(Bunker(bunkerSpec));
        
bunkerSpec.id = 'leftBunker';
bunkerSpec.x = leftBunker;
_objects.push(Bunker(bunkerSpec));

The Bunker constructor follows:

var Bunker = function(spec) { // Private.         
  var that = GameObject(spec); // Get all the methods and properties exposed by the GameObject constructor.       
        
  that.core = globals.helpers.createImage(spec); // Note that spec.x and spec.y could be specified in terms of percentages.
        
  if (that.core.width.baseVal.value > _maxBunkerWidth) {
    _maxBunkerWidth = that.core.width.baseVal.value; // _maxBunkerWidth is global to the Game namespace.
  }
        
  var bunkerCenterX = that.core.x.baseVal.value + (that.core.width.baseVal.value / 2);
  var bunkerCenterY = that.core.y.baseVal.value + (that.core.height.baseVal.value / 2);
        
  that.setPosition(bunkerCenterX, bunkerCenterY); // Record the center of the bunker's image as the position of the bunker.

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

  var collisionDetection = function() { // Public.
    for (var i = 0; i < _objects.length; i++) {
      if (_objects[i].type == "boulder") {
        var boulderX = _objects[i].core.cx.baseVal.value;
        var boulderY = _objects[i].core.cy.baseVal.value;
              
        if ( Math.abs(boulderX - bunkerCenterX) < TARGET_SENSITIVITY && Math.abs(boulderY - bunkerCenterY) < TARGET_SENSITIVITY) {
          _objects[i].markActive(false); // Mark the boulder object as inactive so that exited() returns true on it in the main game loop (i.e., in update()).
              
          var bunkerExplosionSpec = {};

          bunkerExplosionSpec.type = "bunkerExplosion";
          bunkerExplosionSpec.fill = "red";
          bunkerExplosionSpec.stroke = "white";
          bunkerExplosionSpec.strokeWidth = 3; // In SVG user units.                        
          bunkerExplosionSpec.radius = 0; // Start a bunker explosion with a radius of zero, since this radius sinusoidally grows from a radius equal to 0.
          bunkerExplosionSpec.movable = false; // So that the bunker explosions are drawn on top of the bunker images.
          bunkerExplosionSpec.x = bunkerCenterX;
          bunkerExplosionSpec.y = bunkerCenterY;
          bunkerExplosionSpec.targetBunker = that;
          bunkerExplosionSpec.soundPath = BUNKER_SOUND;
                
          _objects.push(BunkerExplosion(bunkerExplosionSpec));                
        } // if
      } // if          
    } // for
  }; // Game.Bunker.collisionDetection()      
        
  that.collisionDetection = collisionDetection;

The first thing the Bunker constructor does is pass its spec parameter (not to be confused with the static local spec object described above) to the GameObject constructor in order to inherit all the GameObject constructor's properties and methods into its local that variable. In this way, the Bunker constructor only needs to redefine its collisionDetection method. This collisionDetection method checks to see if any boulder object (if (_objects[i].type == "boulder")) is close enough to the bunker's center and if so, marks the bunker for destruction (_objects[i].markActive(false)), specifies the characteristics of a bunker explosion object, and pushes this object into the list of game objects via the BunkerExplosion constructor (_objects.push(BunkerExplosion(bunkerExplosionSpec))).

Here's the BunkerExplosion constructor:

var BunkerExplosion = function(spec) { // Private.
  var that = MissileExplosion(spec);
  var speed = 3.5 / FPS * GAME_SPEED; // Increase this numeric value to increase the rate at which the missile explosion expands.
  var maxBunkerExplosionRadius = _maxBunkerWidth / 1.8; // Only make this calculation once. Decrease the numeric value to make the bunker explosion radius larger. Note that _maxBunkerWidth is a Game namepspace global.
  var t = 0;          
                
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        
  var move = function() { // Public.     
    t += speed;                                                                                        
           
    var currentRadius = that.core.r.baseVal.value = maxBunkerExplosionRadius * Math.sin(t);
          
    if (Math.abs(that.radius - currentRadius) < 5) { // Returns true when the radius of the bunker explosion is close to its maximum value.
      spec.targetBunker.markActive(false); // This code is invoked multiple times while "Math.abs(that.radius - currentRadius) < 5" is true. And it's safe to mark the target bunker object as inactive (i.e., destroyed, so that it will be removed) multiple times.
    }
          
    if (currentRadius <= 0) {
      that.markActive(false); // Mark the bunker explosion object as inactive so that _objects[i].exited() returns true on it.
    }
  }; // Game.bunkerExplosion.move()
        
  that.move = move;
        
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */        
        
  var collisionDetection = function() { // Public. Don't use MissileExplosion's inherited collisionDetection() method.
    // NO-OP.
  }; // Game.bunkerExplosion.collisionDetection()
        
  that.collisionDetection = collisionDetection;
        
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */                
        
  return that;
}; // Game.bunkerExplosion()

Because a bunker explosion is quite similar to a missile explosion, the BunkerExplosion constructor calls the MissileExplosion constructor and redefines its inherited move and collisionDetection methods.

The BunkerExplosion constructor's NOPcollisionDetection method points out one of the potential problems of the functional inheritance pattern - it's occasionally necessary to disable inherited functionality. In this case, collision detection functionality for a bunker explosion makes no sense - a bunker explosion exists to gracefully remove a bunker, as described next.

The BunkerExplosion constructor's move method causes an SVG circle (centered on the middle of the bunker) to expand close to its maximum radius (i.e., Math.abs(that.radius - currentRadius) < 5), at which point it removes the bunker (by calling spec.targetBunker.markActive(false)) and then removes itself when its radius reaches 0 (by calling that.markActive(false)). Because the bunker explosion circle is on top of the bunker image, it appears as if the bunker was consumed by the explosion.

Note that the rate at which a bunker explosion circle expands and contracts (based on sine) remains constant regardless of the specified frames per second used in the game, due to the line var speed = 3.5 / FPS * GAME_SPEED (GAME_SPEED is currently set to 1).

Returning to the first line of BunkerExplosion, the called MissileExplosion constructor looks like this:

var MissileExplosion = function(spec) { // Private.
  var that = GameObject(spec); // Get all the methods and properties currently exposed by the GameObject constructor.       
  var speed = 3 / FPS * GAME_SPEED; // Increase this numeric value to increase the rate at which the missile explosion expands.
  var t = 0; // A local static variable.        
        
  that.core = globals.helpers.createCircle(spec);
  that.setPosition(that.core.cx.baseVal.value, that.core.cy.baseVal.value);
        
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

  var move = function() { // Public.
    t += speed;                                                                    
    that.core.r.baseVal.value = MAX_MISSILE_EXPLOSION_RADIUS * Math.sin(t);
    if (that.core.r.baseVal.value <= 0) {
      that.markActive(false); // Mark this game object as inactive so that objects[i].exited() returns true.
    } // if
  }; // Game.MissileExplosion.move()        
        
  that.move = move;
                
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

  var collisionDetection = function() { // Public. Note that this method is called from a FOR loop (so this is actually a doubly nested FOR loop).
    for (var i = 0; i < _objects.length; i++) {
      if (_objects[i].type == "boulder" && globals.helpers.circlesOverlap(that.core, _objects[i].core)) {
        var boomSpec = {};
              
        boomSpec.type = "boom";
        boomSpec.image = BOOM_IMAGE;
        boomSpec.width = 202; // Actual size of image in pixels - reduce if necessary.
        boomSpec.height = 156; 
        boomSpec.x = _objects[i].core.cx.baseVal.value - (boomSpec.width / 2);            
        boomSpec.y = _objects[i].core.cy.baseVal.value - (boomSpec.height / 2); 
        boomSpec.soundPath = BOOM_SOUND;
               
        _objects.push(Boom(boomSpec));

        _objects[i].markActive(false); // Mark the boulder object as inactive so that it'll be removed.              
        updateScore(BOULDER_POINTS); // Credit the score since the user destroyed a boulder with a missile.
      } // if
    } // for
  }; // Game.MissileExplosion.collisionDetection()  
        
  that.collisionDetection = collisionDetection;
                
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        
  return that;
}; // Game.MissileExplosion()

Because BunkerExplosion redefines MissileExplosion's move and collisionDetection methods, the only items of value inherited from MissileExplosion are, of course, the properties and methods inherited from GameObject and the returned bunker-explosion-SVG-circle:

var MissileExplosion = function(spec) { // Private.
  var that = GameObject(spec); // Get all the methods and properties currently exposed by the GameObject constructor.       
  .
  .
  .
  that.core = globals.helpers.createCircle(spec);
  that.setPosition(that.core.cx.baseVal.value, that.core.cy.baseVal.value);
  .
  .
  .        
  return that;
}; // Game.MissileExplosion()

This leads us to the GameObject constructor itself:

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.movable = spec.movable;
  if (typeof(that.movable) == "undefined" || that.movable == null) {
    that.movable = true;
  }        
        
  that.radius = spec.radius || 0; // It's possible that spec.radius is purposely set to zero, so honor this possibility.
  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;
        
  that.targetX = spec.targetX; // The target of the game object (like where a missile should explode).
  that.targetY = spec.targetY;
        
  var deltaX = that.targetX - that.x; // Used to calculate a unit vector (i.e., direction vector).
  var deltaY = that.targetY - that.y;
  var magnitude = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); // Distance, as the crow flies, between the two points.
        
  that.unitX = (deltaX / magnitude); // The x-component of the unit vector from the point (that.x, that.y) to the point (that.targetX, that.targetY).
  that.unitY = (deltaY / magnitude); // The y-component of the unit vector from the point (that.x, that.y) to the point (that.targetX, that.targetY).
        
  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;
    }
          
    if (that.type == "bunker") {
        updateScore(BUNKER_POINTS); // Majorly debit the user when a bunker is destroyed.
    }
          
    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()

Because the GameObject constructor is the progenitor of all game objects, it's worth reviewing the details. We start by looking at the properties and then each of the methods:

  • The spec parameter can contain a myriad of properties (i.e., game object specifications) that might have special meaning to particular constructors, but the GameObject constructor is only concerned with the most general ones:

    var that = {};        
    
    that.active = spec.active;
    if (typeof(that.active) == "undefined" || that.active == null) {
      that.active = true;
    }
    
    that.movable = spec.movable;
    if (typeof(that.movable) == "undefined" || that.movable == null) {
      that.movable = true;
    }        
    
    that.radius = spec.radius || 0; // It's possible that spec.radius is purposely set to zero, so honor this possibility.
    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;
    
    that.targetX = spec.targetX; // The target of the game object (like where a missile should explode).
    that.targetY = spec.targetY;
    
    var deltaX = that.targetX - that.x; // Used to calculate a unit vector (i.e., direction vector).
    var deltaY = that.targetY - that.y;
    var magnitude = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); // Distance, as the crow flies, between the two points.
    
    that.unitX = (deltaX / magnitude); // The x-component of the unit vector from the point (that.x, that.y) to the point (that.targetX, that.targetY).
    that.unitY = (deltaY / magnitude); // The y-component of the unit vector from the point (that.x, that.y) to the point (that.targetX, that.targetY).
    
    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
    

    that.active is used throughout the codebase but in particular, it's used by the exited method (i.e., return (!that.active)). In turn, exited is used by update to remove game objects that have been destroyed or have left the game's playing field:

    var update = function() {
      for (var i = 0; i < _objects.length; i++) {
        _objects[i].move();
        _objects[i].collisionDetection();
        if (_objects[i].exited()) { // INDICATES THE GAME OBJECT SHOULD BE REMOVED
          _objects[i].remove(); // REMOVES THE GAME OBJECT FROM _objects[]
        } // if
      } // for
      .
      .
      .
    }; // Game.update()
    

    The next item in GameObject is that.movable. This property is used to determine if one object should be drawn on top of another. For example, in the createCircle helper function, we have:

    if (spec.movable) {
      _activeGameObjectsContainer.appendChild(circle); // Place the circle in the active DOM container.
    } 
    else {
      _staticGameObjectsContainer.appendChild(circle); // Place the circle in the static DOM container.
    }
    

    Because <g id="staticGameObjectsContainer"> is physically below <g id="activeGameObjectsContainer">, objects inserted into the staticGameObjectsContainer container appear on top of objects inserted into the activeGameObjectsContainer container. For example, in Bunker's collisionDetection method, we have bunkerExplosionSpec.movable = false. This ensures that the bunker-explosion-SVG-circle is appended to the staticGameObjectsContainer container thereby drawing it on top of any objects drawn in the activeGameObjectsContainer container (as well as the previously drawn bunker image itself).

    that.radius and that.type are self explanatory. that.core is used to store the core SVG object associated with the "abstracted" game object. This allows the game object to move itself, as in Boulder's move method:

    var move = function() { // Public.
      that.core.cx.baseVal.value += that.unitX * BOULDER_SPEED;
      that.core.cy.baseVal.value += that.unitY * BOULDER_SPEED;
    
      if (that.exited()) { // Use the Missile constructor's exited() method here.
        that.markActive(false); // Mark the boulder object as inactive so as to get it out of _objects[] (and thus out of the game).
      }
    }; // Game.Boulder.move()        
    
    that.move = move;
    

    Here, the that.core property is used to move the SVG circle (i.e., boulder) based on its normalized direction vector (that is, a unit vector):

    The red arrow is the direction vector and the green arrow its unit vector. unitX and unitY (calculated from the that.x, that.y, that.targetX, and that.targetY values) are the x- and y-components of the (green) unit vector, respectively.

    The last GameObject property to discuss is that.audio. In order for multiple sound effects to play currently, each object that specifies an MP3 file path is given an HTML5 audio object:

    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
    

    Note that if spec.soundPath is undefined (for example, the spec parameter for a bunker object doesn't have a sound effect, by design), that.audio is undefined, and that.audio.play() is never called.

    We next move onto the methods of the GameObject constructor.

  • The public setPosition and getPosition methods were created for convenience.

  • A generic game object (i.e., an object created via GameObject) cannot know how to move itself or detect a collision, so both its move and collisionDetection methods are NOPs.

  • The GameObject's exited method is defined next, in that all game objects should remove themselves if their active property is false:

    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;
    
  • We originally used the functional inheritance pattern because all game objects share the same _objects[] removal code, specifically:

    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;
      }
    
      if (that.type == "bunker") {
          updateScore(BUNKER_POINTS); // Majorly debit the user when a bunker is destroyed.
      }
    
      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 is good!
        return;                    
      } // if
    
      _objects.splice(arrayIndex, 1); // Remove the game object from the array of game objects.                 
    }; // Game.GameObject.remove()
    

    In most web browsers, indexOf() is capable of finding a generic object within an array of objects. We use this capability to quickly find the index of the game object to be removed. Then, the game object's associated core object (i.e., that.core) is removed from the DOM (i.e., core.parentNode.removeChild(core)), which removes it from the screen. Lastly, the game object itself is removed from the array of game objects using the found index value (i.e., _objects.splice(arrayIndex, 1)).

    Take a look at this defensive programming code:

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

    Proactive but simple techniques like this can be helpful finding and fixing bugs (recall that indexOf() returns -1 if it can't find the given object in its array). Another example is the simple act of placing an "unnecessary" default clause within switch statements, as we see in the setState function:

    default:
      console.log("Error in switch of setState()");
    
  • GameObject's last method, markActive, was created for convenience:

    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;
    

Finally, the last line of the GameObject constructor returns the object it constructed (via return that), which is passed to its children - the various game object constructors that call it directly or indirectly through another constructor.

3. Start button is clicked

With the page fully loaded, the user clicks the Start button which, thanks to startButton.addEventListener('click', handleStartButton, false), invokes the handleStartButton event handler:

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

Because globals.game.init()``spec.paused to true, the game.getState().paused clause is executed, invoking game.run():

var run = function() { // Public. Invoked when the Start button is clicked.
  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".
  globals.helpers.removeText({id: "gameOver"}); // If "Game Over" text is on the screen, this removes it.                
  globals.helpers.opacify(false); // Un-opacify the screen (whether it needs it or not).
}; // Game.run()

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

As described in HTML5 game skeleton code for Boulder Bop, the run function invokes update about every 16.67 milliseconds (assuming FPS = spec.fps = 60) and update causes the boulders to descend toward the bunkers, as discussed next.

4. Boulders descend toward bunkers

As shown in step 3, the last line of the init function is createBoulders(spec.level), and here's its code:

var createBoulders = function(level) { // Private.
  var boulders = level; // Determines how many boulders fall from the sky per level. Note that Math.floor(1.125*level) + 1 is a bit more aggressive.
  var gameBackground = document.getElementById('gameBackground');        
        
  while (boulders) {
    var boulderSpec = {};
                    
    boulderSpec.radius = Math.floor( Math.random() * (MAX_BOULDER_RADIUS - MIN_BOULDER_RADIUS + 1) ) + MIN_BOULDER_RADIUS; // Returns an integer between the min and max values.
          
    boulderSpec.x = Math.floor( Math.random() * (gameBackground.width.baseVal.value + 1) ); // Returns an integer between 0 and the SVG viewport's width.
    boulderSpec.y = -boulderSpec.radius; // Position the boulder just off screen.
                    
    boulderSpec.targetBunker = getRandomBunker();
    boulderSpec.targetX = boulderSpec.targetBunker.x;
    boulderSpec.targetY = boulderSpec.targetBunker.y;
          
    boulderSpec.type = "boulder";
    boulderSpec.fill = "#963";
    boulderSpec.stroke = "white";
    boulderSpec.strokeWidth = 2; // In SVG user units.  
    boulderSpec.movable = true; // Allow the user to click on top of a boulder to send another missile, if they're so inclined.
                              
    _objects.push(Boulder(boulderSpec));
          
    boulders--;
  } // while
}; // Game.createBoulders()

Because this is the beginning of the game, the level parameter is equal to 1, hence one boulder object gets pushed into the _objects[] array via the Boulder constructor. Before we describe this constructor, let's look at the initialization of boulderSpec.

boulderSpec's initialization code randomly chooses a boulder radius (between MAX_BOULDER_RADIUS and MIN_BOULDER_RADIUS), randomly chooses a starting location for the boulder just above the playing field (boulderSpec.x and boulderSpec.x), and randomly chooses one of the bunkers as the target for the boulder:

boulderSpec.targetBunker = getRandomBunker();
boulderSpec.targetX = boulderSpec.targetBunker.x;
boulderSpec.targetY = boulderSpec.targetBunker.y;

Here, getRandomBunker() returns a bunker object (whose location is specified via its x and y properties). The rest of boulderSpec's initialization code is straightforward.

With boulderSpec initialized, we move onto the constructor that consumes it:

var Boulder = function(spec) { // Private.
  var that = Missile(spec); // A boulder is a lot like a missile, so use it's constructor.
                           
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

  var move = function() { // Public.
    that.core.cx.baseVal.value += that.unitX * BOULDER_SPEED;
    that.core.cy.baseVal.value += that.unitY * BOULDER_SPEED;
          
    if (that.exited()) { // Use the Missile constructor's exited() method here.
      that.markActive(false); // Mark the boulder object as inactive so as to get it out of _objects[] (and thus out of the game).
    }
  }; // Game.Boulder.move()        
        
  that.move = move;
        
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        
  return that; // Return the object created by the Boulder constructor.
}; // Game.Boulder()

Because a boulder is essentially a missile (i.e., they're both SVG circles), the Boulder constructor calls the Missile constructor. The only difference between the two is the rate at which their associated objects move across the screen (i.e., MISSILE_SPEED vs. BOULDER_SPEED), hence Boulder's redefinition of its inherited move method.

This, then, leads us to the Missile constructor:

var Missile = function(spec) { // Private. The Boulder() constructor inherits from this constructor.
  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.
    var offset = -(MAX_BOULDER_RADIUS + 1); // In SVG user units. This ensures that the boulders initially positioned just off screen won't be tagged as having exited the playing field.
          
    if (!that.active) { // Any game object that is marked as inactive has logically exited the game.
      return true;
    }
                    
    if (that.core.cx.baseVal.value < 0 || that.core.cy.baseVal.value < offset) { // Left and top part of the playing field.
      return true;
    } // if

    if (that.core.cx.baseVal.value > _gameClickPlaneWidth || that.core.cy.baseVal.value > _gameClickPlaneHeight) { // Right and bottom part of the playing field.
      return true;
    } // if
          
    return false;
  }; // Game.Missile.exited()
        
  that.exited = exited;        

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

  var move = function() { // Public.
    var positionX = (that.core.cx.baseVal.value += that.unitX * MISSILE_SPEED); 
    var positionY = (that.core.cy.baseVal.value += that.unitY * MISSILE_SPEED); 
          
    var colorString = 'fill: rgb(';    
    colorString = colorString + parseInt(Math.abs(positionX) % 256) + ', '; // Constrain the values to be an integer inclusively between 0 and 255.
    colorString = colorString + parseInt(Math.abs(positionY) % 256) + ', ';
    colorString = colorString + parseInt(Math.abs(positionX + positionY) % 256) + ')';
    that.core.setAttribute('style', colorString);
                    
    if ( Math.abs(positionX - that.targetX) < TARGET_SENSITIVITY && Math.abs(positionY - that.targetY) < TARGET_SENSITIVITY) {
      that.markActive(false); // Mark the missile object as inactive so that _objects[i].exited() returns true on it (in the main game loop in update()).
            
      var missileExplosionSpec = {};
      missileExplosionSpec.type = "missileExplosion";
      missileExplosionSpec.fill = "gold";
      missileExplosionSpec.stroke = "white";
      missileExplosionSpec.strokeWidth = 2; // In SVG user units.
      missileExplosionSpec.x = that.targetX;
      missileExplosionSpec.y = that.targetY;
      missileExplosionSpec.radius = 0; // Start a missile explosion with a radius of zero, since this radius sinusoidally grows from a radius equal to 0.
      missileExplosionSpec.movable = true; // Allow the user to click on top of an explosion to send another missile.
             
      _objects.push(MissileExplosion(missileExplosionSpec));
    } // if
  }; // Game.Missile.move()

  that.move = move;

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

The Missile constructor inherits from GameObject, creates an SVG circle (based on its spec parameter), and redefines its inherited exited and move methods.

The redefined exited method returns true if the missile is not active (i.e., !that.active) or it has left the playing field. The one caveat is the that.core.cy.baseVal.value < offset check. If this check was that.core.cy.baseVal.value < 0, then a boulder's exited method would immediately return true as soon as the boulder was positioned above the playing field via createBoulders. By setting offset to -(MAX_BOULDER_RADIUS + 1), we let the boulders be positioned just above the sky so they appear to descend from the heavens toward their target bunkers.

Because Boulder redefines Missile's move method, it is not discussed here but covered next.

5. User fires missile toward descending boulder

With a boulder descending towards a bunker from the top of the playing field, the user tries to destroy it by clicking (or tapping) near it. Thanks to gameClickPlane.addEventListener('click', handleGameClickPlane, false), this click event is handled by the handleGameClickPlane event handler:

function handleGameClickPlane(evt) {
  var helpers = globals.helpers;
  var game = globals.game;
  var point = null;

  if (!game.getState().paused) { // If the game is not paused, then it's running.
    point = helpers.createPoint(evt.pageX, evt.pageY);
    point = helpers.coordinateTransform(point, evt.target); // Using the same point object, convert screen coordinates to playing field coordinates.
    game.createMissile(point); // Create a new missile object and add it to the list of core game objects.
  } // if
} // handleGameClickPlane

Here, the click position (evt.pageX, evt.pageY) is converted from screen coordinates to game playing field coordinates (as described in SVG Coordinate Transformations (Windows)) and used to fire a missile via game.createMissile(point):

var createMissile = function(target) { // Public. The target parameter is a point object representing the location (center point) of where the missile should explode.
  var missileSpec = getClosestBunker(target).getPosition(); // Get the position of the bunker that is closest to target.
  missileSpec.targetX = target.x;
  missileSpec.targetY = target.y;
  missileSpec.type = "missile";
  missileSpec.radius = MISSILE_RADIUS; // In SVG user units. 
  missileSpec.stroke = "white";

  _objects.push(Missile(missileSpec));
        
  updateScore(MISSILE_POINTS); // Debit the user for firing a missile.
}; // Game.createMissile()
      
that.createMissile = createMissile; // A public method for one or more event handers.

To decide which bunker the missile should be fired from, we need to know which bunker is closest to the user's click point (i.e., the target parameter). This is handled by the getClosestBunker function (which uses the globals.helpers.euclideanDistance function).

With this info and some other details safely ensconced in missileSpec, we push a missile object into _objects[] via the Missile constructor. Except for its move method, the Missile constructor was described in step 4. We now describe this move method (copied from step 4):

var Missile = function(spec) { // Private. The Boulder() constructor inherits from this constructor.
  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.
    var offset = -(MAX_BOULDER_RADIUS + 1); // In SVG user units. This ensures that the boulders initially positioned just off screen won't be tagged as having exited the playing field.
          
    if (!that.active) { // Any game object that is marked as inactive has logically exited the game.
      return true;
    }
                    
    if (that.core.cx.baseVal.value < 0 || that.core.cy.baseVal.value < offset) { // Left and top part of the playing field.
      return true;
    } // if

    if (that.core.cx.baseVal.value > _gameClickPlaneWidth || that.core.cy.baseVal.value > _gameClickPlaneHeight) { // Right and bottom part of the playing field.
      return true;
    } // if
          
    return false;
  }; // Game.Missile.exited()
        
  that.exited = exited;        

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

  var move = function() { // Public.
    var positionX = (that.core.cx.baseVal.value += that.unitX * MISSILE_SPEED); 
    var positionY = (that.core.cy.baseVal.value += that.unitY * MISSILE_SPEED); 
          
    var colorString = 'fill: rgb(';    
    colorString = colorString + parseInt(Math.abs(positionX) % 256) + ', '; // Constrain the values to be an integer inclusively between 0 and 255.
    colorString = colorString + parseInt(Math.abs(positionY) % 256) + ', ';
    colorString = colorString + parseInt(Math.abs(positionX + positionY) % 256) + ')';
    that.core.setAttribute('style', colorString);
                    
    if ( Math.abs(positionX - that.targetX) < TARGET_SENSITIVITY && Math.abs(positionY - that.targetY) < TARGET_SENSITIVITY) {
      that.markActive(false); // Mark the missile object as inactive so that _objects[i].exited() returns true on it (in the main game loop in update()).
            
      var missileExplosionSpec = {};
      missileExplosionSpec.type = "missileExplosion";
      missileExplosionSpec.fill = "gold";
      missileExplosionSpec.stroke = "white";
      missileExplosionSpec.strokeWidth = 2; // In SVG user units.
      missileExplosionSpec.x = that.targetX;
      missileExplosionSpec.y = that.targetY;
      missileExplosionSpec.radius = 0; // Start a missile explosion with a radius of zero, since this radius sinusoidally grows from a radius equal to 0.
      missileExplosionSpec.movable = true; // Allow the user to click on top of an explosion to send another missile.
             
      _objects.push(MissileExplosion(missileExplosionSpec));
    } // if
  }; // Game.Missile.move()

  that.move = move;

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

The first 2 lines move the missile across the screen:

var positionX = (that.core.cx.baseVal.value += that.unitX * MISSILE_SPEED); 
var positionY = (that.core.cy.baseVal.value += that.unitY * MISSILE_SPEED); 

The next 5 lines change the color of the missile based on its current position:

var colorString = 'fill: rgb(';    
colorString = colorString + parseInt(Math.abs(positionX) % 256) + ', '; // Constrain the values to be an integer inclusively between 0 and 255.
colorString = colorString + parseInt(Math.abs(positionY) % 256) + ', ';
colorString = colorString + parseInt(Math.abs(positionX + positionY) % 256) + ')';
that.core.setAttribute('style', colorString);

Now, when the missile is close enough to its intended target (i.e., where the user clicked/tapped), it explodes:

if ( Math.abs(positionX - that.targetX) < TARGET_SENSITIVITY && Math.abs(positionY - that.targetY) < TARGET_SENSITIVITY) {
  that.markActive(false); // Mark the missile object as inactive so that _objects[i].exited() returns true on it (in the main game loop in update()).
            
  var missileExplosionSpec = {};
  missileExplosionSpec.type = "missileExplosion";
  missileExplosionSpec.fill = "gold";
  missileExplosionSpec.stroke = "white";
  missileExplosionSpec.strokeWidth = 2; // In SVG user units.
  missileExplosionSpec.x = that.targetX;
  missileExplosionSpec.y = that.targetY;
  missileExplosionSpec.radius = 0; // Start a missile explosion with a radius of zero, since this radius sinusoidally grows from a radius equal to 0.
  missileExplosionSpec.movable = true; // Allow the user to click on top of an explosion to send another missile.
             
  _objects.push(MissileExplosion(missileExplosionSpec));
} // if

This missile explosion is covered next.

6. Missile explodes at click point

From the code in step 5, we see that the missile is close enough to its intended target when the following is true:

Math.abs(positionX - that.targetX) < TARGET_SENSITIVITY && 
Math.abs(positionY - that.targetY) < TARGET_SENSITIVITY

That is, the position of the missile (positionX, positionY) is "close enough" to where the user clicked (that.targetX, that.targetY) when their absolute difference is less than TARGET_SENSITIVITY (which has an empirically derived value of 15).

When this condition is true, we mark the missile object for destruction (that.markActive(false)), initialize missileExplosionSpec, and push a missile explosion object into _objects[] via MissileExplosion (copied from step 5):

var MissileExplosion = function(spec) { // Private.
  var that = GameObject(spec); // Get all the methods and properties currently exposed by the GameObject constructor.       
  var speed = 3 / FPS * GAME_SPEED; // Increase this numeric value to increase the rate at which the missile explosion expands.
  var t = 0; // A local static variable.        
        
  that.core = globals.helpers.createCircle(spec);
  that.setPosition(that.core.cx.baseVal.value, that.core.cy.baseVal.value);
        
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

  var move = function() { // Public.
    t += speed;                                                                    
    that.core.r.baseVal.value = MAX_MISSILE_EXPLOSION_RADIUS * Math.sin(t);
    if (that.core.r.baseVal.value <= 0) {
      that.markActive(false); // Mark this game object as inactive so that objects[i].exited() returns true.
    } // if
  }; // Game.MissileExplosion.move()        
        
  that.move = move;
                
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

  var collisionDetection = function() { // Public. Note that this method is called from a FOR loop (so this is actually a doubly nested FOR loop).
    for (var i = 0; i < _objects.length; i++) {
      if (_objects[i].type == "boulder" && globals.helpers.circlesOverlap(that.core, _objects[i].core)) {
        var boomSpec = {};
              
        boomSpec.type = "boom";
        boomSpec.image = BOOM_IMAGE;
        boomSpec.width = 202; // Actual size of image in pixels - reduce if necessary.
        boomSpec.height = 156; 
        boomSpec.x = _objects[i].core.cx.baseVal.value - (boomSpec.width / 2);            
        boomSpec.y = _objects[i].core.cy.baseVal.value - (boomSpec.height / 2); 
        boomSpec.soundPath = BOOM_SOUND;
               
        _objects.push(Boom(boomSpec));

        _objects[i].markActive(false); // Mark the boulder object as inactive so that it'll be removed.              
        updateScore(BOULDER_POINTS); // Credit the score since the user destroyed a boulder with a missile.
      } // if
    } // for
  }; // Game.MissileExplosion.collisionDetection()  
        
  that.collisionDetection = collisionDetection;
                
  /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        
  return that;
}; // Game.MissileExplosion()

As update processes the _objects[] array, it eventually executes the missile explosion object's move method:

var move = function() { // Public.
  t += speed;                                                                    
  that.core.r.baseVal.value = MAX_MISSILE_EXPLOSION_RADIUS * Math.sin(t);
  if (that.core.r.baseVal.value <= 0) {
    that.markActive(false); // Mark this game object as inactive so that objects[i].exited() returns true.
  } // if
}; // Game.MissileExplosion.move()        
        
that.move = move;

Due to Math.sin(t), the missile-explosion-SVG-circle expands to a maximum radius and then contracts to zero (or below). When it hits zero (or goes negative), it's marked for destruction.

Next, we have MissileExplosion's collisionDetection method:

var collisionDetection = function() { // Public. Note that this method is called from a FOR loop (so this is actually a doubly nested FOR loop).
for (var i = 0; i < _objects.length; i++) {
  if (_objects[i].type == "boulder" && globals.helpers.circlesOverlap(that.core, _objects[i].core)) {
    var boomSpec = {};
              
    boomSpec.type = "boom";
    boomSpec.image = BOOM_IMAGE;
    boomSpec.width = 202; // Actual size of image in pixels - reduce if necessary.
    boomSpec.height = 156; 
    boomSpec.x = _objects[i].core.cx.baseVal.value - (boomSpec.width / 2);            
    boomSpec.y = _objects[i].core.cy.baseVal.value - (boomSpec.height / 2); 
    boomSpec.soundPath = BOOM_SOUND;
               
    _objects.push(Boom(boomSpec));

    _objects[i].markActive(false); // Mark the boulder object as inactive so that it'll be removed.              
    updateScore(BOULDER_POINTS); // Credit the score since the user destroyed a boulder with a missile.
  } // if
} // for
}; // Game.MissileExplosion.collisionDetection()  
        
that.collisionDetection = collisionDetection;

Again, the update method goes through all game objects in _objects[] and eventually invokes the missile explosion's collisionDetection method. The for loop (in collisionDetection) goes through all active boulders (SVG circles) to determine if any of them overlap with this missile explosion (another SVG circle):

if (_objects[i].type == "boulder" && 
    globals.helpers.circlesOverlap(that.core, _objects[i].core)) {
  .
  .
  .
}

If so, we prepare to destroy the boulder, as described next.

7. Boulder is destroyed by missile explosion

When a boulder (SVG circle) overlaps with a missile explosion (SVG circle), a boom object is specified (boomSpec), created (via Boom), pushed into _objects[], the struck boulder is marked for destruction, and the user's points are updated (for successfully destroying a boulder):

var boomSpec = {};

boomSpec.type = "boom";
boomSpec.image = BOOM_IMAGE;
boomSpec.width = 202; // Actual size of image in pixels - reduce if necessary.
boomSpec.height = 156; 
boomSpec.x = _objects[i].core.cx.baseVal.value - (boomSpec.width / 2);            
boomSpec.y = _objects[i].core.cy.baseVal.value - (boomSpec.height / 2); 
boomSpec.soundPath = BOOM_SOUND;
               
_objects.push(Boom(boomSpec));

_objects[i].markActive(false); // Mark the boulder object as inactive so that it'll be removed.              
updateScore(BOULDER_POINTS); // Credit the score since the user destroyed a boulder with a missile.

Hopefully, this flow of control walkthrough helps you understand how Boulder Bop works in all of its game playing scenarios.

In the next two sections, Debugging an HTML5 game and Replacing setInterveral with requestAnimationFrame in a HTML5 game, we cover some basic debugging techniques and show how to replace Boulder Bop's setInterval call with requestAnimationFrame.