Chapter 8. Time, Part I: Finite Behaviors and Linear Motion

The last chapter discussed how to encapsulate actions—such as running, falling, pacing, or sparkling—that sprites undertake in JavaScript objects known as behaviors. At runtime, you can configure a sprite’s array of behaviors however you desire. Among its many benefits, that flexibility encourages exploration of game aspects that might otherwise lie dormant.

This chapter continues to discuss sprite behaviors, with a few twists. To start, this is the first of two consecutive chapters devoted to a single sprite behavior: the runner’s jump behavior. By the end of the next chapter, Snail Bait will ultimately arrive at the natural jump sequence, which involves nonlinear motion. Figure 8.1 illustrates that sequence.

Figure 8.1. A natural jump sequence

Image

First, though, in this chapter we implement the simpler case of linear motion during the jump.

Second, jumps last for a specific amount of time, unlike the behaviors discussed in the preceding chapter. Because of that simple difference, Snail Bait must track time as jumps progress. That requirement means we need something that lets us time jumps and other finite behaviors, so in this chapter we also implement a JavaScript stopwatch and use it to time the runner’s ascent and descent as she jumps.

Third, we must be able to pause and resume finite behaviors that time themselves, so at the end of this chapter we implement that functionality.

In this chapter you will learn how to do the following:

• Encapsulate jumping in a sprite behavior (Section 8.2 on p. 212)

• Implement a JavaScript stopwatch (Section 8.4 on p. 217)

• Use stopwatches to time finite animations (Section 8.5 on p. 220)

• Implement linear motion (Section 8.6 on p. 223)

• Pause and resume a finite behavior such as jumping (Section 8.7 on p. 227)

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

Table 8.1. Linear motion examples online

Image

8.1. Implement an Initial Jump Algorithm

We begin with a simple algorithm for jumping that we refine throughout this chapter and the next. Our first attempt at jumping is listed in Example 8.1.

Example 8.1. Keyboard handling for jumps


window.addEventListener(
   'keydown',

   function (e) {
      var key = e.keyCode;

      if (key === 74) { // 'j'
         if (snailBait.runner.track === 3) { // At the top; nowhere to go
            return;
         }

         snailBait.runner.track++;

         snailBait.runner.top =
            snailBait.calculatePlatformTop(snailBait.runner.track) -
                                        snailBait.RUNNER_CELLS_HEIGHT;
      }
   }
);


When the player presses the j key, the preceding event handler immediately puts the runner’s feet on the track above her if she is not already on the top track. Figure 8.2 illustrates instantaneous jumping.

Figure 8.2. Instantanious jumping: 1) Player presses the j key. 2) Runner instantly jumps to the next track.

Image

The jumping implementation shown in Figure 8.2 has two serious drawbacks. First, the runner moves instantly from one level to another, which is far from the desired effect. Second, the jumping implementation is at the wrong level of abstraction. A window event handler has no business directly manipulating the runner’s properties; instead, the runner itself should be responsible for jumping.

8.2. Shift Responsibility for Jumping to the Runner

Example 8.2 shows a modified implementation of the window’s onkeydown event handler. It’s simpler than the implementation in Example 8.1, and it shifts responsibility for jumping from the event handler to the runner.

Example 8.2. The window’s key handler, delegating to the runner


window.addEventListener(
   'keydown',

   function (e) {
      var key = e.keyCode;
      ...

      if (key === 74) { // 'j'
         snailBait.runner.jump();
      }
   }
);


Next we implement the runner’s jump() method. We begin by modifying Snail Bait’s initializeSprites() method, which Snail Bait calls at the beginning of the game. The updated version of initializeSprites() invokes a helper method named equipRunner(), as shown in Example 8.3.

Example 8.3. Equipping the runner at the start of the game


SnailBait.prototype = {
   ...

   initializeSprites: function() {
      ...

      this.equipRunner();
   },

   equipRunner: function () {
      ...

      this.equipRunnerForJumping(); // Equip the runner for falling later
   },
   ...
};


The equipRunner() method invokes a method named equipRunnerForJumping(). In Chapter 12, we also equip the runner for falling with an equipRunnerForFalling() method, but for now we focus on jumping.

An initial implementation of the equipRunnerForJumping() method is listed in Example 8.4.

Example 8.4. Equipping the runner: The runner’s jump() method


SnailBait.prototype = {
   ...
   equipRunnerForJumping: function () {
      var INITIAL_TRACK = 1;

      this.runner.jumping = false;
      this.runner.track   = INITIAL_TRACK;

      this.runner.jump = function () {
         if (this.jumping) // 'this' is the runner
            return;

         this.jumping = true;
      };

      this.runner.stopJumping = function () {
         this.jumping = false;
      };
   },
   ...
};


The equipRunnerForJumping() method adds two methods to the runner JavaScript object: jump() and stopJumping().

If the runner is not already jumping, runner.jump() merely sets the value of the runner’s jumping property to true, so the method doesn’t really implement jumping at all, because, as it does for all sprite behaviors, Snail Bait implements the act of jumping in a separate behavior object. The runner’s jumping property acts as a trigger for the runner’s jump behavior, as you’ll see in Section 8.3, “Implement the Jump Behavior,” on p. 216.

The runner’s stopJumping() method simply sets the runner’s jumping property to false, which disengages the jump behavior.

When it creates the runner, Snail Bait adds the jump behavior to the runner’s array of behaviors, as shown in Example 8.5.

Example 8.5. Running and jumping


var SnailBait = function () {
   ...

   this.jumpBehavior = {
      execute: function(sprite, now, fps, context,
                         lastAnimationFrameTime) {

         // Implement jumping here

      },
      ...
   };

   this.runner = new Sprite(
      'runner',           // type

      new SpriteSheetArtist(this.spritesheet,
                            this.runnerCellsRight),  // artist

      [  // behaviors
         this.runBehavior,

         this.jumpBehavior,
         ...
      ]
   );
   ...
};


Every animation frame, Snail Bait invokes the execute() method of the runner’s jump behavior. That behavior does nothing until the player presses the j key (or taps the screen on mobile devices) to make the runner jump. When that happens, as you saw in Example 8.4, the runner’s jump() method sets the runner’s jumping property to true, which acts as a trigger that engages the jump behavior.

Now that the infrastructure is in place for initiating a jump, we can concentrate solely on the jump behavior.

8.3. Implement the Jump Behavior

Our first implementation of the runner’s jump behavior, listed in Example 8.6, mimics the functionality of our original attempt at jumping.

Example 8.6. An instantaneous jump behavior


var SnailBait =  function () {
   ...

   this.jumpBehavior = {
      ...

      execute: function(sprite, time, fps) {
         if ( ! sprite.jumping || sprite.track === 3) {
            return;
         }

         sprite.track++;

         sprite.top = snailBait.calculatePlatformTop(sprite.track) -
                      snailBait.RUNNER_CELLS_HEIGHT;

         sprite.jumping = false;
      }
   };
   ...
};


Compared with many of Snail Bait’s other sprite behaviors, jumping is a relatively infrequent occurrence. Most of the time the runner’s jump behavior does nothing because the behavior’s trigger – the runner’s jumping property – is false. When the runner’s jump() method sets that property to true, the behavior’s trigger is tripped and the jump behavior takes action in the next animation frame, provided the runner has a track above her.

With the code for jumping encapsulated in a behavior, we’re ready to refine that behavior to implement more realistic jumping, but first we must implement stopwatches so we can time jumps.


Image Note: From j key to jump behavior

Recall that every animation frame, Snail Bait iterates over all behaviors associated with each visible sprite, invoking each behavior’s execute() method. The runner sprite is always visible, which means Snail Bait invokes the jump behavior in every animation frame.



Image Note: Behavior triggers

Behaviors that last for a finite amount of time typically have some sort of trigger–such as the runner’s jumping property–-that causes the behavior to take action. When the behavior concludes, it resets the trigger and lies dormant until the next time the game trips the trigger.


8.4. Time Animations with Stopwatches

All the motion we’ve implemented so far in Snail Bait has been constant. For example, all the game’s sprites, except for the runner, scroll continuously in the horizontal direction, and buttons and snails constantly pace back and forth on their platforms. Coins, sapphires, and rubies bob up and down without ever stopping to take a break.

Jumping, however, is not constant; it has a definite start and end. To implement jumping, therefore, we need a way to monitor how much time has elapsed since a jump began. What we need is a stopwatch.

Example 8.7 shows the implementation of a Stopwatch constructor function.

Stopwatches keep track of whether they are running; the time they started running; and the time that elapsed since they started running. Stopwatches also keep track of whether they are paused, the start time of the pause, and the total time the stopwatch has been paused.

Example 8.7. The Stopwatch constructor


Stopwatch = function ()  {
   this.startTime = 0;
   this.running = false;
   this.elapsed = undefined;

   this.paused = false;
   this.startPause = 0;
   this.totalPausedTime = 0;
};


The stopwatch’s methods reside in the Stopwatch object’s prototype, listed in Example 8.8.

Example 8.8. Stopwatch methods


Stopwatch.prototype = {
   start: function () {
      var now = +new Date();

      this.startTime = now;
      this.running = true;
      this.totalPausedTime = 0;
      this.startPause = 0;
   },

   stop: function () {
      var now = +new Date();

      if (this.paused) {
         this.unpause();
      }

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

   pause: function () {
      var now = +new Date();

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

   unpause: function () {
      var now = +new Date();

       if (!this.paused) {
          return;
       }

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

   getElapsedTime: function () {
      var now = +new Date();

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

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

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

   reset: function() {
      var now = +new Date();

      this.elapsed = 0;
      this.startTime = now;
      this.running = false;
      this.totalPausedTime = 0;
      this.startPause = 0;
   }
};


You can start, stop, pause, unpause, and reset stopwatches. You can also get their elapsed time, and you can determine whether they are running or paused.

In Chapter 4, you saw how to resume a paused game exactly where it left off by accounting for the amount of time the game was paused. Like the game itself, paused stopwatches must resume exactly where they leave off, so they also account for the amount of time they’ve been paused.

The stopwatch implementation, though simple, is important because it lets us implement behaviors that last for a finite amount of time.


Image Note: +new Date(): The current time according to stopwatches

Stopwatch methods access the current time with the construct +new Date(), which coerces a Date object into a number. The important thing, however, is not the construct, but the fact that–-for the time being–-the concept of the current time is internal to stopwatches. When we discuss time systems in Chapter 10, we modify the Stopwatch object so the current time is no longer defined inside Stopwatch methods, but is passed to those methods by the game itself.



Image Note: Timestamps: JavaScript vs. other languages

Most computer languages provide a convenient way to get a timestamp; for example, C++’s time() method. Typically, such timestamp methods return the number of seconds since the Epoch (00:00 at January 1, 1970). JavaScript on the other hand, returns the number of milliseconds since the Epoch.

It’s important to realize that a timestamp’s actual value is irrelevant, as long as it represents the current time (however it’s defined) and as long as the value consistently represents the same thing. Games are only interested in the flow of time; elapsed times are important, not absolute time values.


8.5. Refine the Jump Behavior

Now that we have stopwatches, let’s use them to time the jump behavior. First, we modify the equipRunnerForJumping() method from Example 8.4 as shown in Example 8.9.

Example 8.9. Revised equipRunner() method


SnailBait.prototype = {
   ...
   equipRunnerForJumping: function () {
      var INITIAL_TRACK = 1;

      this.runner.JUMP_HEIGHT   = 120;   // pixels
      this.runner.JUMP_DURATION = 1000;  // milliseconds

      this.runner.jumping = false;
      this.runner.track   = INITIAL_TRACK;

      this.runner.ascendTimer  = new Stopwatch();
      this.runner.descendTimer = new Stopwatch();

      this.runner.jump = function () {
         if (this.jumping) // 'this' is the runner
            return;

         this.jumping = true;

         this.runAnimationRate = 0; // Freeze the runner while jumping
         this.verticalLaunchPosition = this.top;
         this.ascendTimer.start();
      };

      this.runner.stopJumping = function () {
         this.jumping = false;
      };
   },
   ...
};


The revised implementation of equipRunnerForJumping() creates two stopwatches: runner.ascendTimer for the jump’s ascent and runner.descendTimer for its descent.

When the jump begins, the jump() method starts the runner’s ascend stopwatch; sets the runner’s run animation rate to zero, to freeze her while she’s in the air; and records the runner’s vertical position, to return her to that position when the jump completes.

The runner properties set in Example 8.9 are summarized in Table 8.2.

Table 8.2. The runner’s jump-related properties

Image

Next, in Example 8.10, we modify the jump behavior originally implemented in Example 8.6.

Example 8.10. The jump behavior, revisited


var SnailBait =  function () {
   ...
   this.jumpBehavior = {
      ...
      execute: function(sprite, context, time, fps) {
         if ( ! sprite.jumping) {                 // Not currently jumping
            return;                               // Nothing to do
         }

         if (this.isAscending(sprite)) {          // Ascending
            if ( ! this.isDoneAscending(sprite))  // Not done ascending
               this.ascend(sprite);               // Ascend
            else
               this.finishAscent(sprite);         // Finish ascending
         }
         else if (this.isDescending(sprite)) {    // Descending
            if ( ! this.isDoneDescending(sprite)) // Not done descending
               this.descend(sprite);              // Descend
            else
               this.finishDescent(sprite);        // Finish descending
         }
      }
   };
   ...
};


The jump behavior in Example 8.10 is the implementation of a high-level abstraction that leaves jumping details to other methods, such as ascend() and isDescending(). All that remains is to fill in those details by using the runner’s ascend and descend stopwatches to implement the following eight methods:

ascend()

isAscending()

isDoneAscending()

finishAscent()

descend()

isDescending()

isDoneDescending()

finishDescent()

8.6. Implement Linear Motion

To begin, we implement jumping with linear motion, meaning the runner ascends and descends at a constant rate of speed, as depicted in Figure 8.3.

Figure 8.3. Smooth linear jump sequence

Image

Linear motion results in a jumping motion that’s unnatural because gravity should constantly decelerate the runner when she’s ascending and accelerate her as she descends. In Chapter 9, we modify time so the jump behavior results in nonlinear motion, as depicted in Figure 8.1, but for now we’ll stick to the simpler case of linear motion.

8.6.1. Ascending

The jump behavior’s methods dealing with ascending are shown in Example 8.11.

Example 8.11. Ascending


SnailBait = function () {
   ...

   this.jumpBehavior = {
      isAscending: function (sprite) {
         return sprite.ascendTimer.isRunning();
      },

      ascend: function (sprite) {
         var elapsed = sprite.ascendTimer.getElapsedTime(),
             deltaY  = elapsed / (sprite.JUMP_DURATION/2) * sprite.JUMP_HEIGHT;

         sprite.top = sprite.verticalLaunchPosition - deltaY; // Moving up
      },

      isDoneAscending: function (sprite) {
         return sprite.ascendTimer.getElapsedTime() > sprite.JUMP_DURATION/2;
      },

      finishAscent: function (sprite) {
         sprite.jumpApex = sprite.top;
         sprite.ascendTimer.stop();
         sprite.descendTimer.start();
      }
   };
   ...
};


The methods in Example 8.11 are summarized in Table 8.3.

Table 8.3. The jump behavior’s ascend methods

Image

Recall that the runner’s jump() method, listed in Example 8.9, starts the runner’s ascend stopwatch. Subsequently, that running stopwatch causes the jump behavior’s isAscending() method to temporarily return true. Also recall that until the runner is done ascending—-meaning the jump is halfway over—-the runner’s jump behavior repeatedly calls the ascend() method, as you can see from Example 8.10.

The ascend() method calculates the number of pixels to move the runner vertically for each animation frame by dividing the stopwatch’s elapsed time (milliseconds) by one-half of the jump’s duration (milliseconds) and multiplying that value by the height of the jump (pixels). The milliseconds cancel out, yielding pixels as the unit of measure for the deltaY value.

When the runner finishes her ascent, the jump behavior’s finishAscent() method records the sprite’s position at the jump apex, stops the ascend stopwatch, and starts the descend stopwatch.

8.6.2. Descending

The jump behavior methods associated with descending are shown in Example 8.12.

Example 8.12. Descending


SnailBait = function () {
   ...

   this.jumpBehavior = {
      isDescending: function (sprite) {
         return sprite.descendTimer.isRunning();
      },

      descend: function (sprite, verticalVelocity, fps) {
         var elapsed = sprite.descendTimer.getElapsedTime(),
             deltaY  = elapsed / (sprite.JUMP_DURATION/2) * sprite.JUMP_HEIGHT;

         sprite.top = sprite.jumpApex + deltaY; // Moving down
      },

      isDoneDescending: function (sprite) {
         return sprite.descendTimer.getElapsedTime() > sprite.JUMP_DURATION/2;
      },

      finishDescent: function (sprite) {
         sprite.top = sprite.verticalLaunchPosition;
         sprite.descendTimer.stop();
         sprite.jumping =  false;
         sprite.runAnimationRate = snailBait.RUN_ANIMATION_RATE;
      }
   };
   ...
};


The methods in Example 8.12 are summarized in Table 8.4.

Table 8.4. The jump behavior’s descend methods

Image

There’s a lot of symmetry between the ascend methods in Table 8.3 and the descend methods in Table 8.4. Both ascend() and descend() calculate the number of pixels to move the runner in the vertical direction for the current frame in exactly the same manner. The descend() method, however, adds that value to the jump’s apex, whereas ascend() subtracts it from the vertical launch position (recall that the Canvas Y axis increases from top to bottom).

When the jump’s descent is finished, finishDescent() places the runner at the same vertical position at which she began the jump and restarts her run animation.

8.7. Pause Behaviors

Behaviors that time their activities with stopwatches must pause and resume those stopwatches when the game in which they reside pauses and resumes; otherwise, behaviors can get out of sync with the rest of the game. For example, the preceding implementation of the jump behavior does not pause its stopwatches when Snail Bait pauses during a jump, causing jumps to move ahead in time while the game is paused. When the game resumes, therefore, the jump behavior jumps ahead in time.

To pause and resume sprite behaviors and therefore keep behaviors in sync with Snail Bait, we add a togglePausedStateOfAllBehaviors() method to Snail Bait. The game invokes that method from its togglePaused() method, as shown in Example 8.13.

Example 8.13. Pausing and unpausing all of Snail Bait’s sprite behaviors


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 togglePausedStateOfAllBehaviors() method is listed in Example 8.14.

Example 8.14. Pausing and unpausing all of Snail Bait’s sprite behaviors


SnailBait.prototype = {
   ...

   togglePausedStateOfAllBehaviors: function () {
      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);
                }
             }
             else {
                if (behavior.unpause) {
                   behavior.unpause(sprite);
                }
             }
          }
       }
   },
   ...
};


The togglePausedStateOfAllBehaviors() method iterates over all of Snail Bait’s sprites and subsequently iterates over each sprite’s behaviors, invoking each behavior’s pause() or unpause() method. Those methods are optional; typically, if a behavior does not time its activities, it does not need to implement pause() or unpause().

Example 8.15 shows the implementations of pause() and unpause() for the jump behavior.

The jump behavior’s pause() and unpause() methods pause and unpause the behavior’s timers if they are running.

Example 8.15. Pausing the jump behavior


SnailBait = function () {
   ...

   this.jumpBehavior = {
      ...

      pause: function (sprite) {
         if (sprite.ascendTimer.isRunning()) {
            sprite.ascendTimer.pause();
         }
         else if (sprite.descendTimer.isRunning()) {
            sprite.descendTimer.pause();
         }
      },

      unpause: function (sprite) {
         if (sprite.ascendTimer.isRunning()) {
            sprite.ascendTimer.unpause();
         }
         else if (sprite.descendTimer.isRunning()) {
            sprite.descendTimer.unpause();
         }
      },
      ...
   };
   ...
};


8.8. Conclusion

Prior to this chapter we’ve dealt with time implicitly, mostly as a backdrop to animation; however, our need to time finite animations has led us to deal with time explicitly by implementing stopwatches and using them to time a jump’s ascent and descent.

In this chapter, you saw how to use those stopwatches to implement a jump behavior with linear motion. As you also saw in this chapter, implementing linear motion is relatively straightforward; however, jumping with linear motion is unnatural, so the next thing to do is to make that linear motion nonlinear.

In the two chapters that follow, we delve more deeply into time as it pertains to video games. In the next chapter, we implement nonlinear motion for the runner’s jump behavior by modifying the flow of time through that behavior, without making any changes to the runner’s jump behavior. Then follows a chapter in which you will see how to implement a time system that lets you modify the flow of time throughout an entire game. That handy feature is the source of all kinds of interesting effects and features, such as slow-motion power-ups.

8.9. Exercises

1. Run the version of Snail Bait corresponding to linear jumping (core-html5games.com/book/code/linear-motion/linear/index.html) and press the p key to pause the game during a jump. Wait a few seconds and press the p key again to resume the game. Watch where the runner resumes. Does she resume exactly where she left off? How so, or why not?

2. There are many ways to get a timestamp in JavaScript. Modify the Stopwatch object so that it uses another way besides +new Date() to get the current time and restart the game. Does the game work the same after the change? Explain.

3. Change the runner’s JUMP_DURATION property from 1000ms to 2000ms and restart the game. The runner should jump just as high, but the jump should take twice as long. Is that the case?

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

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