Chapter 12. Gravity

In Chapter 9 you saw how to simulate gravity during a sprite’s jumps and bounces by using easing functions that slow time on the sprite’s ascent and accelerate it on the descent. That manipulation of time affects all time’s derivatives, including motion, so sprites slow as they ascend and speed up as they descend, similar to how gravity acts on the objects the sprites represent.

Sometimes, however, an approximation of gravity is not enough; for example, an artillery game should be more precise about how gravity affects rounds as they fly through the air. In this short chapter, you will see how to directly affect a falling sprite’s motion to simulate gravity. You will also see how to detect collisions between the runner and platforms when the runner is falling, a collision detection edge case that’s not properly handled by the more general collision detection we implemented in Chapter 11.

You can find the online example for this chapter at corehtml5games.com/book/code/gravity.

12.1. Falling

Snail Bait’s runner falls when she either runs off the edge of a platform or collides with a platform from underneath it. Figure 12.1 shows the runner falling off the edge of a platform.

Figure 12.1. Falling off the edge of a platform

Image

The runner also falls when she misses a platform at the end of a jump’s descent, as illustrated in Figure 12.2.

Figure 12.2. Falling at the end of a jump

Image

The runner falls by virtue of her fall behavior. Example 12.1 shows the instantiation of the runner sprite, specifying her final array of behaviors.

Example 12.1. Creating the runner with a fall behavior


SnailBait.prototype = {
   ...

   createRunnerSprite: function () {
      ...

      this.runner = new Sprite(
                           'runner',

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

                           [
                              this.runBehavior,
                              this.jumpBehavior,
                              this.collideBehavior,
                              this.runnerExplodeBehavior,
                              this.fallBehavior
                           ]
                        );
      ...
   },
   ...
};


Recall that when the game starts, Snail Bait invokes the equipRunner() method, which equips the runner for both jumping and falling by invoking equipRunnerForJumping() and equipRunnerForFalling(). The latter method, listed in Example 12.2, contains the implementation of the runner’s fall() and stopFalling() methods.

Example 12.2. The runner’s fall() method


SnailBait.prototype = {
   ...

   equipRunner: function () {
      ...

      this.equipRunnerForJumping();
      this.equipRunnerForFalling();
   },

   equipRunnerForFalling: function  () {
       this.runner.fallTimer = new AnimationTimer(); // this is snailBait
       this.runner.falling   = false;

       this.runner.fall = function  (initialVelocity) {
          this.falling = true;  // this is the runner

          this.velocityY        = initialVelocity || 0;
          this.initialVelocityY = initialVelocity || 0;

          this.fallTimer.start(snailBait.timeSystem.calculateGameTime());
       };

       this.runner.stopFalling = function () {
          this.falling   = false; // this is the runner
          this.velocityY = 0;

          this.fallTimer.stop(snailBait.timeSystem.calculateGameTime());
       };
    },
    ...
};


The fall() method sets the runner’s initial vertical velocity, sets her falling property to true, and starts an animation timer to track her fall’s elapsed time.

The runner’s fall behavior is triggered with the runner’s falling property. The runner’s fall() method sets that property to true, thereby triggering the fall behavior.

While the runner’s falling property is true, the fall behavior takes action every animation frame. When the runner’s stopFalling() method sets the property to false, the fall behavior is deactivated. The stopFalling() method also stops the animation timer and sets the runner’s vertical velocity to zero.

We discuss the runner’s fall behavior in Section 12.2.1, “The Runner’s Fall Behavior,” on p. 310, but first let’s see how Snail Bait incorporates gravity in general.

12.2. Incorporating Gravity

Near the Earth’s surface, falling objects accelerate quickly. Without allowing for wind resistance, a falling object’s velocity increases by nearly 36 km/hr (or 21 miles/hr) every second. The consequence of gravity for game developers is that calculating a falling sprite’s position means they must first calculate its constantly changing velocity.

Fortunately, calculating a falling object’s velocity is straightforward. Multiply the force of gravity (9.81 m/s/s or 32 ft/s/s) by the sprite’s elapsed fall time, and add that value to the sprite’s initial vertical velocity when it began to fall. The equation is

v = vi + G*t

v represents the sprite’s vertical velocity, vi is the initial vertical velocity, G is the force of gravity, and t is the elapsed time of the fall.

As is often the case for equations, the confusing part is not the math but the units, because the result of the preceding equation leaves you with meters (or feet) per second. To make that value more useful, Snail Bait converts it to pixels per second. First, the game calculates a pixels:meter ratio at the beginning of the game as follows.

1. In the game’s HTML, define the width of the game in pixels. SnailBait’s width is 800 pixels.

2. In the game’s JavaScript, define the width of the game in meters. SnailBait’s width is 13 meters.

3. Divide the width in pixels by the width in meters to get a ratio of pixels-to-meter.

With a pixel:meter ratio in hand, Snail Bait positions falling sprites as follows.

1. When the runner begins to fall, the runner’s fall() method records her initial vertical velocity (see Section 12.1, “Falling,” on p. 306).

2. Subsequently, for every animation frame, Snail Bait uses the pixel:meter ratio to position the falling sprite:

a) Use v = vi + G*t to calculate the sprite’s velocity in meters/second.

b) Convert the velocity to pixels/second by multiplying the velocity in meters/second by the pixel:meter ratio.

c) Calculate the number of pixels to move the sprite in the vertical direction by multiplying the sprite’s vertical velocity in pixels/second times the elapsed time of the preceding animation frame in seconds, which yields a value representing pixels. See Chapter 3 for more details on time-based motion.

Let’s convert the preceding algorithm into code. The first step is to define the force of gravity and the game’s pixel:meter ratio, as shown in Example 12.3.

Example 12.3. Constants pertaining to gravity and falling


var SnailBait = function () {
   ...

   this.GRAVITY_FORCE = 9.81; // meters / second / second
   this.CANVAS_WIDTH_IN_METERS = 13;  // Proportional to sprite sizes

   this.PIXELS_PER_METER = this.canvas.width /
                           this.CANVAS_WIDTH_IN_METERS;
   ...
};


The second step is to calculate the runner’s vertical velocity – depending on her initial vertical velocity when the fall began and the amount of time she has been falling – and position her accordingly. That functionality is the responsibility of the runner’s fall behavior, which we discuss next.

12.2.1. The Runner’s Fall Behavior

The runner’s fall behavior has four distinct responsibilities:

• Start falling when the runner is running without a platform underneath her.

• Adjust the runner’s vertical position as she falls.

• When the runner falls on a platform, stop falling and place the runner’s feet on the platform.

• When the runner falls through the bottom of the game, invoke Snail Bait’s loseLife() method.

Like all behaviors, the runner’s fall behavior has an execute() method, listed in Example 12.4, that Snail Bait invokes for every animation frame. Also, because the fall behavior is associated with the runner, the sprite that Snail Bait passes to the behavior’s execute() method is always the runner. See Chapter 7 for more about sprite behaviors and how Snail Bait invokes them.

If the runer is falling and she’s in play and not exploding, the fall behavior’s execute() method adjusts her vertical position with the behavior’s moveDown() method.

If the runner is exploding during a fall, she stops falling. If she’s out of play, meaning she has fallen through the bottom of the canvas, she also stops falling and the player loses a life.

If the runner is neither falling nor jumping and does not have a platform underneath her, she starts falling.

Example 12.4. The fall behavior’s execute() method


var SnailBait = function () {
   ...

   this.fallBehavior = {
      ...

      execute: function (sprite, now, fps, context,
                         lastAnimationFrameTime) {
         if (sprite.falling) {
            if ( ! this.isOutOfPlay(sprite) && ! sprite.exploding) {
                this.moveDown(sprite, now, lastAnimationFrameTime);
            }
            else { // Out of play or exploding
               sprite.stopFalling();

               if (this.isOutOfPlay(sprite)) {
                  snailBait.loseLife();
                  snailBait.runner.visible = false;
                  ...
               }
            }
         }
         else { // Not falling
            if ( ! sprite.jumping &&
                 ! snailBait.platformUnderneath(sprite)) {
               sprite.fall();
            }
         }
      },
      ...
   };
   ...
};


The fall behavior’s moveDown() method is listed in Example 12.5.

The moveDown() method sets the runner’s vertical velocity by invoking the behavior’s setSpriteVelocity() method. The moveDown() method subsequently calculates the distance the runner will drop given the newly calculated velocity, the current time, and the time of the last animation frame, by invoking the behavior’s calculateVerticalDrop() method.

If, given the drop distance, the runner will not fall below her current track (recall that platforms move on three horizontal tracks, as discussed in Chapter 3), moveDown() moves the runner down by that distance. And since the Y coordinates for the canvas coordinate system increase from top to bottom, moveDown() adds to the runner’s Y coordinate to move her down, instead of subtracting, as you might think.

If the runner’s feet will fall below her current track and a platform is underneath her, moveDown() puts the runner on the platform and stops her fall. If the runner will fall below her current track and no platform is underneath her, moveDown() decrements her track property and moves her down by the drop distance.

Example 12.5. The fall behavior’s moveDown() method


var SnailBait = function () {
   ...

   this.fallBehavior = {
      ...

      moveDown: function (sprite, now, lastAnimationFrameTime) {
         var dropDistance;

         this.setSpriteVelocity(sprite, now);

         dropDistance = this.calculateVerticalDrop(
                           sprite, now, lastAnimationFrameTime);

         if ( ! this.willFallBelowCurrentTrack(sprite, dropDistance)) {
            sprite.top += dropDistance;
         }
         else { // will fall below current track
            if (snailBait.platformUnderneath(sprite)) {
                this.fallOnPlatform(sprite);
                sprite.stopFalling();
            }
            else { // below current track with no platform underneath
               sprite.track--;
               sprite.top += dropDistance;
            }
         }
      },
      ...
   };
   ...
};


The support methods invoked by the fall behavior’s execute() and moveDown() methods are listed in Example 12.6.

Example 12.6. Fall behavior support methods


var SnailBait = function () {
   ...

   this.fallBehavior = {
      ...

      isOutOfPlay: function (sprite) {
         return sprite.top > snailBait.canvas.height;
      },

      setSpriteVelocity: function (sprite, now) {
         sprite.velocityY = sprite.initialVelocityY +
            snailBait.GRAVITY_FORCE *
            (sprite.fallTimer.getElapsedTime(now)/1000) *
            snailBait.PIXELS_PER_METER;
      },

      calculateVerticalDrop: function (sprite, now,
                                       lastAnimationFrameTime) {
         return sprite.velocityY * (now - lastAnimationFrameTime) / 1000;
      },

      willFallBelowCurrentTrack: function (sprite, dropDistance) {
         return sprite.top + sprite.height + dropDistance >
                snailBait.calculatePlatformTop(sprite.track);
      },

      fallOnPlatform: function (sprite) {
         sprite.stopFalling();
         snailBait.putSpriteOnTrack(sprite, sprite.track);
         ...
      },
      ...
   };
   ...
};


The setSpriteVelocity() method calculates how much speed the runner has picked up since she started to fall by multiplying the force of gravity times the runner’s elapsed fall time, yielding velocity in meters per second. The setSpriteVelocity() method subsequently multiplies that velocity by Snail Bait’s pixels-to-meter ratio to turn the velocity’s units into pixels per second. Finally, setSpriteVelocity() determines the runner’s current velocity by adding the calculated velocity to the runner’s initial velocity as she began to fall.

The calculateVerticalDrop() method uses the time-based motion discussed in Chapter 3 to calculate how far the runner should drop for the current animation frame: multiply the runner’s velocity times the amount of time, in seconds, that has passed since the last animation frame.

The moveDown() method invokes Snail Bait’s platformUnderneath() method, which is listed in Example 12.7. The platformUnderneath() method returns a reference to the platform underneath the runner if there is one, or undefined otherwise.

Example 12.7. Snail Bait’s platformUnderneath() method


SnailBait.prototype = {
   ...

   platformUnderneath: function (sprite, track) {
      var platform,
          platformUnderneath,
          sr = sprite.calculateCollisionRectangle(), // sprite rect
          pr; // platform rectangle

      if (track === undefined) {
         track = sprite.track; // Look on sprite track only
      }

      for  (var i=0; i < snailBait.platforms.length; ++i) {
         platform = snailBait.platforms[i];
         pr = platform.calculateCollisionRectangle();

         if (track === platform.track) {
            if (sr.right > pr.left && sr.left < pr.right) {
               platformUnderneath = platform;
               break;
            }
         }
      }
      return platformUnderneath;
   },
   ...
};


Next let’s see how to calculate the runner’s initial velocity when she falls at the end of the jump.

12.2.2. Calculating Initial Falling Velocities

When the runner runs off the end of a platform or collides with one from underneath, she starts to fall with no vertical velocity. When she misses a platform at the end of a jump, however, she begins to fall with the vertical velocity she had at the end of the jump’s descent. Example 12.8 shows how the runner’s jump behavior uses the GRAVITY_FORCE and PIXELS_PER_METER constants that are defined in Example 12.3 to calculate that initial velocity.

Example 12.8. Falling at the end of a jump


var SnailBait = function () {
   ...

   this.jumpBehavior = {
      ...

      finishDescent: function (sprite, now) {
         sprite.stopJumping();

         if (snailBait.platformUnderneath(sprite)) {
            sprite.top = sprite.verticalLaunchPosition;
         }
         else {
            sprite.fall(snailBait.GRAVITY_FORCE *
               (sprite.descendTimer.getElapsedTime(now)/1000) *
               snailBait.PIXELS_PER_METER);
         }
      },
      ...
   };
   ...
};


The runner’s vertical velocity as she’s falling is the force of gravity times the elapsed descent time, times the game’s pixel-to-meter ratio: (9.81 m/s/s) * (elapsed descent time in seconds) * (800 pixels / 13 m).

The runner’s fall behavior is only concerned with placing the runner vertically. The fall behavior doesn’t need to modify the runner’s horizontal position. Although it looks like the runner is moving horizontally, she never actually moves in the horizontal direction at all. Instead, the background moves beneath her to make it look as though she’s moving horizontally. See Chapter 3 for more information on scrolling the game’s background.

12.2.3. Pausing When the Runner Is Falling

As we discussed in Chapter 8, behaviors that time their activities must implement pause() and unpause() methods so that they can pause and resume in concert with the game as a whole. The implementation of those methods for the runner’s fall behavior is shown in Example 12.9.

Example 12.9. Pausing and unpausing the fall behavior


var SnailBait = function () {
   ...

   this.fallBehavior = {
      ...

      pause: function (sprite) {
         sprite.fallTimer.pause();
      },

      unpause: function (sprite) {
         sprite.fallTimer.unpause();
      },
      ...
   };
   ...
};


The fall behavior tracks elapsed time during falls with the runner’s fall timer. Therefore, the behavior’s pause() and unpause() methods simply pause and unpause that timer.


Image Note: Gravity is a special case

Gravity produces nonlinear motion. Previously, we implemented nonlinear jumping by using easing functions that approximate gravity with ease-out and ease-in easing functions. If you vary those functions, you can produce an infinite spectrum of nonlinear motion, meaning that gravity is a special case of nonlinear motion in general.


12.3. Collision Detection, Redux

Recall that the fall behavior’s moveDown() method listed in Example 12.5 detects when the runner collides with a platform while she’s falling and places her on that platform. In effect, the moveDown() method is dabbling in collision detection, which may seem strange considering that we’ve already implemented collision detection in the previous chapter. However, the collision detection we implemented in the last chapter is insufficient to detect collisions between the falling runner and a platform underneath.

The collision detection we implemented in the last chapter checks to see if one of five points within the runner’s collision rectangle lies within another sprite’s collision rectangle; if so, the runner has collided with the other sprite, as shown in Figure 12.3.

Figure 12.3. The runner colliding with a coin (yellow dots represent points in the runner’s collision rectangle)

Image

Most of Snail Bait’s sprites, such as bees, bats, and coins, are sufficiently wide and tall enough so that no matter how fast the runner is moving, the game reliably detects when one of the five points within the runner’s collision rectangle intersects another sprite’s collision rectangle.

Platforms however, are a different story because they are very thin, and when the runner is moving fast enough, the five points in her collision rectangle can jump over a platform in a single animation frame.

When the runner jumps and hits her head on a platform above her, as shown in Figure 12.4, she is moving slowly enough that the collision detection we implemented in the last chapter detects the collision.

Figure 12.4. The runner collides with a platform from underneath

Image

However, when the runner is moving very fast, checking those five points for intersection with the platform’s collision rectangle will not work; for example, Figure 12.5 shows the runner just as she starts to fall after a jump. When that screenshot was taken, the runner already had considerable vertical velocity from the descent of her jump, and by the time she reaches the bottom most platform, she will be moving at approximately 475 pixels per second. If the game is running at 60 frames per second, that means the runner is falling at 475 ÷ 60 = 8 pixels per frame. Because platforms are eight pixels high, the points in the runner’s collision rectangle can — and regularly do –- jump over a platform in a single animation frame, especially if the frame rate dips a little (for a frame rate of 30 frames per second, the runner falls at 16 pixels per frame). Because those collisions are not detected by the general collision detection we implemented in the last chapter, the runner’s fall behavior implements that edge case.

Figure 12.5. The runner falls after completing a jump

Image

12.4. Conclusion

In this chapter, you saw how to make sprites fall under the influence of gravity and how to detect collisions when one or more of the participants is very small or moving very fast. Both of those skills are essential for implementing just about any kind of video game, from pinball games to platformers.

Collision detection is one of the most difficult aspects of game development. Typically, game developers add refinements such as spatial partitioning we discussed in the last chapter, to proven collision detection techniques such as bounding boxes or ray casting, along with specialized code for edge cases. In this chapter you saw some specialized code in the fall behavior’s moveDown() method to detect collisions between the runner and platforms when the runner is falling.

At this point we are nearly done with implementing gameplay. In the next chapter you will see how to implement sprite animations and add special effects to your games.

12.5. Exercises

1. Add a log statement to the finishDescent() method of the runner’s jump behavior that prints her vertical velocity, in pixels per second, after she starts to fall. You should see approximately 310 pixels per second. If the game is running at 60 frames per second, how many pixels per second is the runner falling for each animation frame?

2. Add a log statement to the runner’s falling behavior that prints her vertical velocity, in pixels per second, when she falls below the bottom track. How many pixels per second is she moving vertically at that time?

3. With the log statements from the last two exercises intact, modify Snail Bait so that it runs in slow motion (at 25% speed). See [Missing XREF!] for details about how to do that with Snail Bait’s setTimeRate() method. Subsequently, play the game, and check the log statements. Were the values roughly one-quarter of what they were before, or were they the same? Explain the numbers.

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

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