• 10.1 Snail Bait’s Time System
• 10.2 Create and Start the Time System
• 10.3 Incorporate the Time System into Snail Bait
• 10.4 Redefine the Current Time for Stopwatches and Animation Timers
• 10.5 Implement the Time System
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.
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.
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.
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.
Snail Bait’s time system has only three methods, listed in Table 10.2.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
SnailBait.prototype = {
...
calculateFps: function (now) {
var fps = 1 / (now - this.lastAnimationFrameTime) * 1000;
...
return fps;
},
...
};
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.
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.
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.
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.
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.
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()
.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
3.142.36.146