Chapter 10. Time Systems

In the last chapter, we temporarily modified the flow of time for individual behaviors to influence time’s derivatives—a powerful technique. In this chapter, we extend that technique to implement a time system that modifies the flow of time throughout an entire game.

Slowing or increasing the rate at which time flows through a game is useful for gameplay features and special effects. Snail Bait slows time during transitions between lives, for example, to emphasize that gameplay is suspended during the transition. Snail Bait’s developer backdoor, discussed in [Missing XREF!] and shown in Figure 10.1, lets you modify the flow of time as the game proceeds, mostly so the developer can run the game in slow motion to make play-testing more productive.

Figure 10.1. Snail Bait in slow motion

Image

In this chapter, we explore the implementation and use of Snail Bait’s simple but capable time system. That time system is less than 50 lines of JavaScript, so we won’t need much time to discuss its implementation. Most of this chapter is concerned with how Snail Bait uses the time system and how the time system affects objects that already have their own notion of time, such as behaviors, animation timers, and stopwatches.

In this chapter, you will learn how to do the following.

• Create and start the time system (Section 10.2 on p. 258)

• Incorporate the time system into Snail Bait (Section 10.3 on p. 258)

• Implement a game method that slows time or speeds it up (Section 10.3.2 on p. 260)

• Redefine the current time for stopwatches and animation timers (Section 10.4 on p. 264)

• Implement the time system (Section 10.5 on p. 268)

The online examples for this chapter are listed in Table 10.1.

Table 10.1. Time systems examples online

Image

Image Note: Slow-motion power-ups

An interesting addition to Snail Bait would be a power-up that causes the game to run in slow motion, perhaps activated when the runner captures a particular coin or jewel. Playing the game in slow motion is significantly easier than playing at full speed, giving the player an advantage while the power-up is active.



Image Note: Implement a time system early on

A time system is a fundamental aspect of any video game, so it’s best to incorporate it into the game from the very start of the game’s development.


10.1. Snail Bait’s Time System

Snail Bait’s time system has only three methods, listed in Table 10.2.

Table 10.2. Time system methods

Image

You start the time system with its start() method and you access the current game time with calculateGameTime(). To modify the flow of time through the time system, you attach a transducer function to the time system with the setTransducer() method.

By default, the time system represents time as it really is; for example, calculateGameTime() returns 5000 milliseconds if you call it exactly 5 seconds after invoking the time system’s start() method.

A time system isn’t very interesting, however, if it’s always truthful about how much time has elapsed since the game began. Things get interesting when the time system purposely reports erroneous values to modify the flow of time. In fact, that’s exactly what we did in the last chapter with easing functions to control the flow of time through a behavior so we could indirectly affect a sprite’s motion and color change over time.

The time system uses time transducer functions to affect the flow of time through an entire game. A transducer function is just like an easing function, except that easing functions modify the completion percentage of a finite behavior, whereas transducer functions modify a game’s overall elapsed time. For example, Example 10.1 shows the implementation of a transducer function that, when passed to the setTransducer() method of Snail Bait’s time system, makes the game run at half speed.

Example 10.1. A transducer that makes Snail Bait run at half speed


var halfSpeedTransducer = function (time) {
   return time / 2; // Half speed
};


Example 10.2 shows how to use the preceding transducer function to make Snail Bait run at half speed.

Example 10.2. Setting the half speed transducer function


snailBait.timeSystem.setTransducer(halfSpeedTransducer);


Snail Bait also provides a convenience method named setTimeRate() that lets you specify the rate at which time flows through the game, as you can see in Example 10.3.

Example 10.3. Using Snail Bait’s setTimeRate() method


snailBait.setTimeRate(0.5); // Run at half speed


The value that you pass to setTimeRate() represents a percent of the normal rate at which time flows through the game. Passing a value of 2.0, for example, makes the game run at twice its normal rate.

The implementation of snailBait.setTimeRate() is discussed in Section 10.3.2, “Implement a Game Method that Uses the Time System to Modify the Flow of Time,” on p. 260.

In Section 10.5, “Implement the Time System,” on p. 268, we look at the implementation of the methods in Table 10.2, but first let’s see how Snail Bait creates and uses the time system.


Image Note: Transducers

Transducers in the physical world convert one form of energy into another. In the virtual world of games, our transducers convert time from one value to another.


The names software developers assign to variables and methods are important because good names convey how code works. The time system implements a calculateGameTime() method because that method calculates game time by optionally modifying the current time through a transducer function instead of merely accessing it.

10.2. Create and Start the Time System

Snail Bait creates an instance of the time system in its constructor function, as you can see in Example 10.4, and sets a variable representing the current time rate to 1.0. That rate is intuitive: zero means time is standing still; 1.0 means time is flowing normally; 0.5 means time is flowing at half speed, and so forth.

Example 10.4. Creating the time system


SnailBait = function () {
   ...

   this.timeSystem = new TimeSystem(); // See js/timeSystem.js
   this.timeRate = 1.0;
   ...
};


Snail Bait starts the time system in its startGame() method, as illustrated in Example 10.5.

Example 10.5. Starting the time system


SnailBait.prototype = {
   ...

   startGame: function () {
      ...

      this.timeSystem.start(); // Start the time system
      ...
   },
   ...
};


Now that you’ve seen how Snail Bait creates and starts the time system, let’s see how it subsequently uses the time system throughout the game.

10.3. Incorporate the Time System into Snail Bait

Fundamentally, using the time system is straightforward. Instead of getting the current time from the browser or a JavaScript expression such as +new Date(), we get it from the time system’s calculateGameTime() method. Practically, however, we must do several things to ensure that the current time from the time system reverberates throughout the entire game:

Use the time system to drive the game’s animation.

Implement a game method that uses the time system to modify the flow of time.

• Factor the rate at which time flows into the frame rate calculation.

Pause and resume the game by using the time system.

Let’s take a look at each of the preceding modifications to Snail Bait.

10.3.1. Use the Time System to Drive the Game’s Animation

Recall that the browser passes the current time to Snail Bait’s animate() method. Up to now, the animate() method used that time to calculate the frame rate and draw the next animation frame, as shown in Example 10.6.

Example 10.6. The original implementation of Snail Bait’s animate() method


SnailBait.prototype = {
   ...

   animate: function (now) {
      // The browser passes the current
      // time in the now argument
      ...

      snailBait.fps = snailBait.calculateFps(now);
      snailBait.draw(now);
      ...
   },
   ...
};


From now on we overwrite the time the browser passes to the animate() method with the time calculated by the game’s time system, as shown in Example 10.7.

Example 10.7. Snail Bait’s animate() method, revised


SnailBait.prototype = {
   ...

   animate: function (now) {
      // Replace the time the browser passes into the method
      // with the time from Snail Bait's time system

      now = snailBait.timeSystem.calculateGameTime();
      ...

      snailBait.fps = snailBait.calculateFps(now);
      snailBait.draw(now);
      ...
   },
   ...
};


Instead of letting the browser dictate the current time to Snail Bait, we calculate it with the time system; in effect, we have hijacked time from the browser. Now that we have done so, it’s time to tinker with time itself.

10.3.2. Implement a Game Method that Uses the Time System to Modify the Flow of Time

To affect the flow of time through the game, Snail Bait implements a setTimeRate() method, listed in Example 10.8. The lone argument to that method is a value between 0.0 and 1.0 that represents the rate at which time flows through the game.

Example 10.8. Setting the time rate


SnailBait.prototype = {
   ...

   setTimeRate: function (rate) {
      this.timeRate = rate;

      this.timeSystem.setTransducer( function (now) {
         return now * snailBait.timeRate;
      });
   },
   ...
};


The setTimeRate() method passes a function to the time system’s setTransducer() method. That function returns the current time multiplied by Snail Bait’s time rate. For example, if the actual elapsed time for the game is 10 seconds and the time rate is 0.5, the time returned by the transducer would be five seconds, making the game run at half speed. You can also specify values greater than 1.0 for Snail Bait’s timeRate property. A value of 2.0, for example, would make the game run twice as fast as normal.

10.3.3. Factor the Time Rate into the Frame Rate Calculation

Up to now, Snail Bait calculated the game’s frame rate as shown in Example 10.9.

Now, however, when Snail Bait sets a transducer function that modifies the current time, the value of the calculateFps() method’s now parameter does not reflect the actual elapsed time, and therefore calculateFps() must account for the time rate so that the calculated frame rate is accurate, as shown in Example 10.10.

Example 10.9. The original implementation of Snail Bait’s calculateFps() method


SnailBait.prototype = {
   ...

   calculateFps: function (now) {
      var fps = 1 / (now - this.lastAnimationFrameTime) * 1000;
      ...

      return fps;
   },
   ...
};


Example 10.10. Snail Bait’s calculateFps() method, revised


SnailBait.prototype = {
   ...

   calculateFps: function (now) {
      var fps = 1 / (now - this.lastAnimationFrameTime) *
                1000 * this.timeRate;
      ...

      return fps;
   },
   ...
};


The revised version of Snail Bait’s calculateFps() method simply multiplies the frame rate by the game’s time rate to account for any transducers that are (or have been) attached to the game’s time system.

10.3.4. Pause and Resume the Game by Using the Time System

Recall that when Snail Bait resumes from a pause, it adjusts the time of the last animation frame to account for the pause, as you can see from the game’s togglePaused() method, which is listed in Example 10.11.

Example 10.11. The original implementation of Snail Bait’s togglePaused() method


SnailBait.prototype = {
   ...

   togglePaused: function () {

      var now = +new Date();

      this.paused = !this.paused;

      this.togglePausedStateOfAllBehaviors();

      if (this.paused) {
         this.pauseStartTime = now;
      }
      else {
         this.lastAnimationFrameTime += (now - this.pauseStartTime);
      }
   },
   ...
};


The togglePaused() method in the preceding listing calculates game time with the construct +new Date(), coercing a Date object into a number. The revised implementation of togglePaused() is shown in Example 10.12.

Example 10.12. Snail Bait’s togglePaused() method, revised


SnailBait.prototype = {
   ...

   togglePaused: function () {
      var now = this.timeSystem.calculateGameTime();

      this.paused = !this.paused;

      this.togglePausedStateOfAllBehaviors(now);

      if (this.paused) {
         this.pauseStartTime = now;
      }
      else {
         this.lastAnimationFrameTime += (now - this.pauseStartTime);
      }
   },
   ...
};


The revised implementation of togglePaused() calculates game time with Snail Bait’s time system. Besides using the time system to track the amount of time the game pauses, the revised implementation of togglePaused() passes the time calculated by the time system to togglePausedStateOfAllBehaviors().

The revised implementation of Snail Bait’s togglePausedStateOfAllBehaviors() method passes the current time to each behavior’s pause() and unpause() methods, as you can see from Example 10.13.

Example 10.13. Snail Bait’s togglePausedStateOfAllBehaviors() method, revised


SnailBait.prototype = {
   ...
   togglePausedStateOfAllBehaviors: function (now) {
      var behavior;

      for (var i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];

         for (var j=0; j < sprite.behaviors.length; ++j) {
            behavior = sprite.behaviors[j];

            if (this.paused) {
               if (behavior.pause) {
                  behavior.pause(sprite, now);
               }
            }
            else {
               if (behavior.unpause) {
                  behavior.unpause(sprite, now);
               }
            }
         }
      }
   },
   ...
};


Snail Bait passes the current time to the behavior’s pause() and unpause() methods so that those methods can, in turn, forward the current time to any timers the behavior is using, as shown in Example 10.14, which lists the pulse behavior’s pause() and unpause() methods.

Example 10.14. Pausing pulse behaviors, revised


PulseBehavior.prototype = {
   pause: function(sprite, now) {
      if (!this.timer.isPaused()) {
         this.timer.pause(now);
      }
      this.paused = true;
   },

   unpause: function(sprite, now) {
      if (this.timer.isPaused()) {
         this.timer.unpause(now);
      }

      this.paused = false;
   },
   ...
};


Up to now, the AnimationTimer.pause() and AnimationTimer.unpause() methods invoked in the preceding code calculated game time internally with the +new Date() JavaScript expression, so those methods did not take any arguments. Now, however, Snail Bait passes the current time that it obtains from its time system to those methods so that animation timers pause and unpause in concert with the game.

AnimationTimer.pause() and AnimationTimer.unpause() are not the only animation timer methods that use the current time. In fact, nearly all AnimationTimer methods use the current time, so we must revise AnimationTimer methods to take the current time as an argument instead of using +new Date().

10.4. Redefine the Current Time for Stopwatches and Animation Timers

Snail Bait has several sprite behaviors that time their sprite’s activities with animation timers. Those animation timers previously calculated game time internally with the +new Date() JavaScript expression; now, however, Snail Bait passes the game time to AnimationTimer methods, as illustrated in Example 10.15.

Example 10.15. Running the jump behavior on game time as opposed to system time


SnailBait.prototype = {
   ...
   equipRunnerForJumping: function () {
      this.runner.jump = function () {
         ...
         this.ascendAnimationTimer.start(
            snailBait.timeSystem.calculateGameTime());
         ...
      };
   },
   ...
};


As discussed in Chapter 8, the runner’s jump method starts the runner’s ascend animation timer. With the time system installed, Snail Bait now passes the game time to the animation timer’s start() method, whereas that start() method previously did not take any arguments.

To keep animation timers and stopwatches in sync with the game’s time system, we must revise the implementations of those objects so that we can pass the current time to their methods. Example 10.16 shows the revised implementation of the AnimationTimer object.

Example 10.16. Animation timers, revised


AnimationTimer.prototype = {
   start: function (now) {
      this.stopwatch.start(now);
   },

   stop: function (now) {
      this.stopwatch.stop(now);
   },

   pause: function (now) {
      this.stopwatch.pause(now);
   },

   unpause: function (now) {
      this.stopwatch.unpause(now);
   },

   isPaused: function () {
      return this.stopwatch.isPaused();
   },

   getElapsedTime: function (now) {
      var elapsedTime = this.stopwatch.getElapsedTime(now),
          percentComplete = elapsedTime / this.duration;

      if (this.easingFunction == undefined || percentComplete === 0 ||
          percentComplete > 1) {
         return elapsedTime;
      }

      return elapsedTime *
             (this.easingFunction(percentComplete) / percentComplete);
   },
   isRunning: function(now) {
      return this.stopwatch.running;
   },

   isExpired: function (now) {
      return this.stopwatch.getElapsedTime(now) > this.duration;
   },

   reset: function(now) {
      this.stopwatch.reset(now);
   }
};


Animation timers just pass the current time through to their underlying stopwatch, as you can see from the preceding listing, so we must revise stopwatches in addition to animation timers. The revised implementation of the Stopwatch object’s methods is shown in Example 10.17.

Example 10.17. Stopwatches, revised


Stopwatch.prototype = {
   start: function (now) {
      this.startTime = now;
      this.elapsedTime = undefined;
      this.running = true;
      this.totalPausedTime = 0;
      this.startPause = 0;
   },

   stop: function (now) {
      if (this.paused) {
         this.unpause();
      }

      this.elapsed = now -  this.startTime - this.totalPausedTime;
      this.running = false;
   },

   pause: function (now) {
      if (this.paused) {
         return;
      }

      this.startPause = now;
      this.paused = true;
      },

   unpause: function (now) {
      if (!this.paused) {
         return;
      }

      this.totalPausedTime += now - this.startPause;
      this.startPause = 0;
      this.paused = false;
   },

   isPaused: function () {
      return this.paused;
   },

   getElapsedTime: function (now) {
      if (this.running) {
          return now - this.startTime - this.totalPausedTime;
      }
      else {
        return this.elapsed;
      }
   },

   isRunning: function() {
      return this.running;
   },

   reset: function(now) {
     this.elapsed = 0;
     this.startTime = now;
     this.elapsedTime = undefined;
     this.running = false;
   }
};


Initially, none of the Stopwatch or AnimationTimer methods took any arguments because they calculated the current game time internally with the +new Date() JavaScript expression. Now, nearly all the methods take a single argument representing the current game time, which comes from the time system. At this point, the time system has permeated throughout the entire game.

Now that you’ve seen how to incorporate the time system into Snail Bait, it’s time to see how to implement the time system.

10.5. Implement the Time System

Snail Bait’s time system uses an animation timer that runs continuously to calculate game time. By default, that animation timer returns the actual elapsed time that the game has been running; however, by fitting the time system with a transducer function, you can modify the flow of time, as you’ve already seen in this chapter.

Example 10.18 shows the time system’s constructor.

Example 10.18. The time system’s constructor


var TimeSystem = function () {
   this.gameTime = 0;
   this.timer = new AnimationTimer();
   this.transducer = function (elapsedTime) { return elapsedTime; };
   this.lastTimeTransducerWasSet = 0;
};


The constructor assigns values to the time system’s four properties, which are listed in Table 10.3.

Table 10.3. Time system properties

Image

The time system’s methods are listed in Example 10.19.

The time system’s animation timer is based on the actual time obtained with the +new Date() JavaScript expression.

Example 10.19. The time system’s methods


TimeSystem.prototype = {
   start: function () {
      this.timer.start();
   },

   reset: function () {
      this.timer.stop();
      this.timer.reset();
      this.timer.start();
      this.lastTimeTransducerWasSet = this.gameTime;
   },

   setTransducer: function (transducerFunction, duration) {
      // Duration is optional. If you specify it, the transducer is
      // applied for the specified duration; after the duration ends,
      // the permanent transducer is restored. If you don't specify the
      // duration, the transducer permanently replaces the current
      // transducer.

      var lastTransducer = this.transducer,
          self = this;

      this.calculateGameTime();
      this.reset();
      this.transducer = transducerFunction;

      if (duration) {
         setTimeout( function (e) {
            self.setTransducer(lastTransducer);
         }, duration);
      }
   },

   calculateGameTime: function () {
      this.gameTime = this.lastTimeTransducerWasSet +
                      this.transducer(this.timer.getElapsedTime());
      this.reset();

      return this.gameTime;
   }
};


By keeping track of the last time the game set a transducer function, the time system’s calculateGameTime() method easily calculates game time by adding the elapsed time since the transducer was set–-modified by the transducer function in itself–-to the last time the transducer was set.

By default, transducer functions stay in effect until the next time you set the transducer function. You can set the transducer function for a specific duration, however, by specifying the duration parameter for the time system’s setTransducer() method.

10.6. Conclusion

Some video games are more sophisticated than others, but they all have a time system. Up to this chapter, our time system consisted of the browser, which passes the current time to our animate() method. In this chapter, we overwrote that value with time calculated from a time system that can modify the flow of time with transducer functions.

The ability to modify the flow of time through your game gives you more interesting options for user interface effects and gameplay mechanics. For example, at slower speeds most video games are easier to play, whereas they are more difficult at faster speeds, so if players can temporarily slow play during difficult areas of the game, they can progress more easily.

10.7. Exercises

1. Modify Snail Bait’s keydown event handler to run the game in slow motion when the player presses the S key by invoking Snail Bait’s setTimeRate() method. If the game is already running in slow motion when the player presses the S key, return the game to normal speed.

2. Modify Snail Bait’s setTimeRate() method to take a second argument: the duration, in milliseconds, that Snail Bait runs the game at the specified rate. If the rate is unspecified, set the time rate until it is reset. Recall that the time system’s setTransducer() method already implements that functionality, so you just need to pass the duration from Snail Bait’s setTimeRate() to the time system’s setTransducer() method.

3. Implement a slow-motion power-up that Snail Bait activates when the runner captures a coin. The power-up should cause the game to run at 20 percent of its normal rate for a duration of five seconds. After five seconds, deactivate the power-up and return the time rate to normal.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.141.27