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


Replacing setInterveral with requestAnimationFrame in a HTML5 game

Here we describe how to improve Boulder Bop's performance by replacing its setInterval with requestAnimationFrame. This improved version of Boulder Bop is called Boulder Bop Perf.

requestAnimationFrame is generally more performant than setInterval (for info about this see Internet Explorer 10 Guide for Developers). With respect to Boulder Bop, the key to converting setInterval to requestAnimationFrame is based on two things:

  • Distance = speed · time (see Speed for more info).
  • requestAnimationFrame returns to its callback function a single argument indicating the time at which the animation frame is scheduled to occur. By subtracting the prior scheduled time from the currently scheduled time, a small time increment can be calculated (Δt).

Now, consider the Boulder constructor'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;

Using the distance equation above, we have Δx = sₓ·Δt, where sₓ is the speed in the x-direction. Similarly, Δy = sy·Δt.

If we pass Δt (i.e., the parameter dt) into the move method, this code becomes:

var move = function(dt) { // Public.
  that.core.cx.baseVal.value += (dt * sx);
  that.core.cy.baseVal.value += (dt * sy);
    .
    .
    .
}; // Game.Boulder.move()        

that.move = move;

And if we calculate the required speed in both the x and y directions (sx and sy), we're done.

To calculate sx and sy, we first adjust the value of BOULDER_SPEED appropriately. Then, sx is that.unitX * BOULDER_SPEED (the component of a fixed speed in the x-direction) and sy is that.unitY * BOULDER_SPEED (the component of a fixed speed in the y-direction). Thus, with BOULDER_SPEED adjusted appropriately, we now have:

var move = function(dt) { // Public.
  that.core.cx.baseVal.value += (dt * that.unitX * BOULDER_SPEED);
  that.core.cy.baseVal.value += (dt * 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;

To calculate dt, we consider that requestAnimationFrame returns to its callback the time at which said callback is schedule to be executed. To understand this, let's start with the run function:

var run = function() { // Public. Invoked when the Start button is clicked.
  setSound('play'); // Play any previously paused sound effects.        
  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).
  _priorTimeStamp = window.performance.now(); // This is used in update().
  _requestAnimationFrameID = window.requestAnimationFrame(update); // Using requestAnimationFrame generally provides better performance than setInterval. Note that requestAnimationFrame's callback is passed a single argument, which indicates the time at which the animation frame is scheduled to occur.
}; // Game.run()

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

The _priorTimeStamp = window.performance.now() line stores the time when the user clicked the Start button. The next line, _requestAnimationFrameID = window.requestAnimationFrame(update), schedules update to be run in the near future and passes the scheduled time to it (additionally, an ID value is returned, which can be used to cancel the animation request). This scheduled time argument becomes the currentTimeStamp parameter in update:

var update = function(currentTimeStamp) { // Private. Update the position of the game objects, etc. Note the requestAnimationFrame sets the currentTimeStamp parameter.
  var dt = currentTimeStamp - _priorTimeStamp; // dt is the time interval between animation frames, in milliseconds.
  _priorTimeStamp = currentTimeStamp;

  for (var i = 0; i < _objects.length; i++) { // Must use _objects.length in that the length of objects can dynamically change.
    if (DEBUG) { console.log("length: " + _objects.length + ", type: " + _objects[i].type + ", id: " + _objects[i].core.id); }
    _objects[i].move(dt); // 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                   

  var activeBunkers = 0;
  var activeBoulders = 0;
  for (var i = 0; i < _objects.length; i++) { // If _objects.length is 0, then activeBunkers will be 0, and the game terminates.         
    if (_objects[i].type == "bunker") {
      activeBunkers++;
    }

    if (_objects[i].type == "boulder") {
      activeBoulders++;
    }
  } // for 

  if (activeBunkers == 0) { // All bunkers have been destroyed.
    gameOver();
    return;
  }

  if (!globals.helpers.bossLevel(spec.level) && activeBoulders == 0) { // Player cleared current non-boss level.
    nextLevel(); // Move to the next level.
  }

  _requestAnimationFrameID = window.requestAnimationFrame(update); // Note that requestAnimationFrame passes to its callback (i.e., update) a single argument, which indicates the time at which the animation frame is scheduled to occur.
}; // Game.update()

On the first iteration, dt is the time difference between when the user clicked the Start button and when the browser scheduled its first animation frame (that is, execute update). Thereafter, dt is the time between successive animation frames. If the system is fast, dt is small, offsetting the high rate at which update is called (by the system). If the system is slow, dt is comparatively large, offsetting the low rate at which update is called (by the system). This keeps the game's rate of play constant regardless of the user's hardware (and is generally smoother in that requestAnimationFrame is more "advanced" than setInterval).

Other than these changes, the difference between Boulder Bop and Boulder Bop Perf is small, as you can tell from red text in this comparison:

By studying the differences above, you should be able to see how Boulder Bop (setInteval) was converted into the more performant Boulder Bop Perf (requestAnimationFrame).

You should now have sufficient knowledge to create your own casual 2D game. As a prelude to this, you may first want to make Boulder Bop Perf a bit more challenging by adding a few more game elements, such as a flying saucer that randomly drop bombs (based, perhaps, on a boulder object) towards the bunkers, or some other interesting game element - the sky is basically the limit.

Additional resources for HTML5 game development