Chapter 16. Particle Systems

A smooth column of smoke rising from a cigarette quickly turns into turbulent, nondeterministic flow. You could easily represent the smooth column as a line, but nothing in Euclidean geometry is useful for modeling turbulent smoke. In fact, in the real world—as opposed to the virtual world of games—most physical phenomena exhibit chaotic behavior that can be challenging to represent graphically.

One way to represent chaotic physical systems, such as turbulent smoke or explosions, is to display a sequence of images; for example, Snail Bait implements explosions with a sequence of images. However, the sequence of images is always the same, making the next explosion look exactly like the last.

A more realistic way to represent chaotic physical systems is with particle systems.

A particle system is a collection of particles that together represent so-called fuzzy objects such as fire, water, or smoke. Fuzzy objects lack well-defined edges and, like most things in nature, do not conform to man-made geometric shapes such as rectangles, triangles, or circles.

In addition to particles, particle systems have emitters that create and emit particles. Once emitted, particles go through a well-defined life cycle as their attributes evolve over time. That evolution typically includes a random aspect to mimic the chaos that underlies even the simplest of natural systems. At the end of their life cycle, particles disappear.

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

• Use particle systems (Section 16.2 on p. 417)

• Implement particle systems (Section 16.3 on p. 420)

• Create a lifetime for particles that defines how they evolve (Section 16.3.3.4 on p. 438)

• Inject randomness into the creation of particles to emulate chaos in physical systems (Section 16.3.3.4 on p. 438)

• Pause and resume particle systems (Section 16.4 on p. 440)


Image Note: Coining the term particle system

William T. Reeves coined the term particle system while working on the Genesis effect for the movie Star Trek II: The Wrath of Khan. You can see that effect at www.siggraph.org/education/materials/HyperGraph/animation/movies/genesisp.mpg.


16.1. Smoking Holes

In this chapter, we examine Snail Bait’s smoking holes, shown in Figure 16.1, which are particle systems that contain fire particles and emit smoke bubbles. Over time, fire particles flicker and smoke bubbles dissipate.

Figure 16.1. Smoke bubbles erupt from a smoking hole and dissipate. (from bottom to top)

Image

Implementing particle systems may sound complicated, and some particle systems are indeed complicated; however, the hard part of implementing particle systems lies not in the programming, but in making particle systems look realistic. For example, Snail Bait’s smoking holes first draw 10 colored smoke bubbles—black, yellow, and orange—that dissipate more slowly than the ensuing 20 grayscale bubbles that it draws on top of the colored bubbles. Those colored bubbles impart a fiery backdrop underneath the grayscale bubbles, which makes the smoke look more realistic. The colored bubbles expand slowly so that they don’t overtake the grayscale bubbles, exposing the fiery backdrop illusion.

Drawing 10 slowly expanding colored smoke bubbles underneath 20 grayscale smoke bubbles is not difficult to do from a programming standpoint, but it’s not initially obvious. Coming up with the idea in the first place takes some experimentation.

Implementing particle systems in general involves the following steps:

1. Implement an emitter that emits particles. The emitter may or may not have a graphical representation.

2. Implement particles that continuously change their appearance depending on how long the particles have been in existence. Particles typically disappear at the end of their lifetime or are reset to their initial conditions.

Snail Bait’s smoking holes consist of three objects, summarized in Table 16.1.

Table 16.1. Smoking-hole-related objects

Image

As you can see most clearly in the top screenshot in Figure 16.1, smoking holes look like holes in Snail Bait’s background, but the game does not draw smoking holes directly. The holes in Snail Bait’s background are part of the background itself.

Snail Bait creates smoking hole objects, and adds them to its array of sprites, as depicted in Figure 16.2.

Figure 16.2. Smoking holes in Snail Bait’s sprites array

Image

Snail Bait’s sprites array contains all the game’s sprites, including two smoking hole objects at the beginning of the array. Although they reside in Snail Bait’ssprites array, smoking holes are not actually sprites; instead, they are objects that contain sprites—fire particles and smoke bubbles—as you can see in Figure 16.2.

Smoking holes are not sprites because they don’t draw anything directly; however, because they contain sprites, it’s most convenient if Snail Bait can treat smoking holes as if they are sprites instead of implementing special code to incorporate them into the game. As a result, smoking holes disguise themselves as sprites by implementing the same methods as a sprite. The game, ignorant of the fact that smoking holes are not really sprites, treats them as though they are by updating and drawing all visible smoking holes every animation frame. The smoking holes, in turn, update and draw their fire particles and smoke bubbles, which are bona-fide sprites.

The implementation of smoking holes resides in a JavaScript file of its own, so smoking holes can be used by games other than Snail Bait. That file is shown in Figure 16.3.

Figure 16.3. Snail Bait smoking hole JavaScript file

Image

Snail Bait includes the smoking hole JavaScript file in its HTML, as shown in Example 16.1.

Example 16.1. Including smoking hole JavaScript


<!DOCTYPE html>
<html>
  ...

   <body>
      ...

      <!-- The final version of Snail Bait puts all the
           game's JavaScript into a single file. See
           [Missing XREF!] for more details about how
           Snail Bait is deployed. -->

      <script src='js/smokingHole.js'></script>
      ...

      <script src='snailbait.js'></script>
      ...
  </body>
</html>


Before we discuss the implementation of Snail Bait’s smoking holes, let’s look at how Snail Bait uses them.


Image Note: Sprite containers

Snail Bait’s smoking holes are objects that contain sprites. Because they don’t have a graphical representation, Snail Bait does not implement smoking holes themselves as sprites; however, Snail Bait treats them as sprites.

Smoking holes are a specialization of more general objects, which may or may not be sprites themselves, that contain other sprites.


16.2. Use Smoking Holes

Snail Bait incorporates smoking holes in four steps:

1. Define smoking hole data

2. Create smoking holes

3. Add smoking holes to Snail Bait’s sprite array

4. Scroll smoking holes every animation frame

Let’s look at each step.

16.2.1. Define Smoking Hole Data

As it does for its sprites, Snail Bait defines metadata for its smoking holes, as shown in Example 16.2. It also defines an array to store smoking hole objects.

Example 16.2. Smoking hole data


var SnailBait = function () {
   ...

   this.smokingHoleData = [
      { left: 250, top: this.TRACK_2_BASELINE - 20 },
      { left: 850, top: this.TRACK_2_BASELINE - 20 }
   ];

   this.smokingHoles = [];
   ...
};


Snail Bait defines the upper-left corner of each smoking hole. Those coordinates correspond to the locations of holes in the background.

That’s all there is to defining smoking hole metadata. Next, let’s see how Snail Bait creates smoking holes.

16.2.2. Create Smoking Holes

Snail Bait creates smoking holes with a createSmokingHoles() method, which it invokes from createSprites(), as shown in Example 16.3.

Example 16.3. Creating smoking holes


SnailBait.prototype = {
   ...

   createSmokingHoles: function () {
      var data,
          smokingHole,
          SMOKE_BUBBLE_COUNT  = 30,
          FIRE_PARTICLE_COUNT = 3,
          SMOKING_HOLE_WIDTH  = 10;

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

         smokingHole = new SmokingHole(SMOKE_BUBBLE_COUNT,
                                       FIRE_PARTICLE_COUNT,
                                       data.left, data.top,
                                       SMOKING_HOLE_WIDTH);

         this.smokingHoles.push(smokingHole);
      }
   },
   ...

   createSprites: function () {
      ...

      this.createSmokingHoles();
      ...
   },
   ...
};


The createSmokingHoles() method iterates over Snail Bait’s smoking hole data, creating a SmokingHole object for each data instance and pushing that object onto the array declared in Example 16.2. Snail Bait’s smoking holes have three fire particles and 30 smoke bubbles and are 10 pixels wide.

16.2.3. Add Smoking Holes to Snail Bait’s Sprite Array

Recall that even though smoking holes are not sprites, Snail Bait treats them as though they are sprites, which means it adds them to the game’s sprites array, as shown in Example 16.4.

Example 16.4. SnailBait.addSpritesToSpritesArray(), revised


SnailBait.prototype = {
   ...

   addSpritesToSpriteArray: function () {
      // Smoking holes must be drawn first so that they
      // appear underneath all other sprites. Sprites
      // are drawn in the order of their appearance in the
      // sprites array.

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

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

      // Similar loops for adding other types of sprites
      // to the sprites array are omitted for brevity
   },
   ...
};


As you can see in Figure 16.1, a smoking hole’s fire particles and smoke bubbles are drawn underneath the game’s other sprites. Whatever you draw first appears underneath whatever you subsequently draw into an HTML5 canvas element, so Snail Bait adds smoking holes to the sprites array before adding the rest of the game’s sprites. Because Snail Bait draws sprites in the order of their appearance in the sprites array, it draws smoking holes before it draws the other sprites.

16.2.4. Scroll Smoking Holes Every Animation Frame

As we discussed above, Snail Bait creates smoking holes and adds them to its array of sprites at startup. As the game is running, however, Snail Bait must scroll smoking holes in concert with the background, so we revise the setSpriteOffsets() method previously discussed in Chapter 3 as shown in Example 16.5.

Example 16.5. Scrolling smoking holes in concert with the background


SnailBait.prototype = {
   ...

   setSpriteOffsets: function (now) {
      var sprite,
          i;

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

         // Smoking holes scroll in concert with the background

         if ('smoking hole' === sprite.type) {
            sprite.hOffset = this.backgroundOffset;
         }
         ...
      }
   },
   ...
};


Recall that Snail Bait invokes setSpriteOffsets() every animation frame, so each smoking hole’s horizontal offset, represented by its hOffset property, stays in sync with the background’s offset. See Chapter 6 for more about sprites and their hOffset property.

Now that you’ve seen how Snail Bait uses smoking holes, let’s see how to implement them.

16.3. Implement Smoking Holes

We implement smoking holes with the following steps:

Disguise smoking holes as sprites

• Incorporate fire particles

• Incorporate smoke bubbles

16.3.1. Disguise Smoking Holes as Sprites

Smoking holes, like Snail Bait itself, are JavaScript objects, so they have a constructor function and a prototype. To disguise smoking holes as sprites, the SmokingHole constructor invokes the SmokingHole object’s disguiseAsSprite() method, as shown in Example 16.6.

Example 16.6. Constructing smoking holes: Disguise as sprite


var SmokingHole = function (smokeBubbleCount, fireParticleCount,
                            left, top, width) {
   ...

   this.disguiseAsSprite(left, top, width);
   ...
};


Snail Bait disguises smoking holes as sprites by fitting them with properties, methods, and behaviors that correspond to the properties, methods, and behaviors in sprites, as you can see from Example 16.7.

Example 16.7. Disguising smoking holes as sprites


SmokingHole.prototype = {
   ...

   disguiseAsSprite: function (left, top, width) {
      this.addSpriteProperties(left, top, width);
      this.addSpriteMethods();
      this.addBehaviors();
   },
   ...
};


The SmokingHole object’s disguiseAsSprite() method invokes methods that add sprite properties, methods, and behaviors to the SmokingHole object.

Recall that sprites have a type. A smoking hole’s type is smoking hole. Sprites also keep track of their upper-left corner, how wide and tall they are, and whether or not they are visible. The SmokingHole object’s addSpriteProperties() method adds those properties to smoking holes as shown in Example 16.8.

Example 16.8. Adding sprite properties to smoking holes


SmokingHole.prototype = {
   ...

   addSpriteProperties: function (left, top, width) {
      this.type    = 'smoking hole';
      this.top     = top;
      this.left    = left;
      this.width   = width;
      this.height  = width; // Square
      this.visible = true;
   },
   ...
};


Sprites implement two mandatory methods: draw() and update(), so the SmokingHole object’s addSpriteMethods() adds identical methods to the SmokingHole object’s prototype, as shown in Example 16.9.

Example 16.9. Adding sprite methods to smoking holes


SmokingHole.prototype = {
   ...

   addSpriteMethods: function () {
      this.draw = function (context) {
         // TODO: Draw fire particles and smoke bubbles
      };

      this.update = function (now, fps,
                              context, lastAnimationFrameTime) {
         // TODO: 1. Update smoke bubbles
         //       2. Execute smoking hole behaviors
         //
         // It's not necessary to update fire particles because
         // they have no behaviors.
      };
   },
   ...
};


For now the SmokingHole object’s draw() and update() methods are placeholders. In the sections that follow, we modify those methods to draw and update smoking holes. Besides updating smoke bubbles, the update() method executes smoking hole behaviors. Smoking holes have only one behavior that emits smoke bubbles. That smoking hole behavior is created in the SmokingHole object’s addBehaviors() method, which is also initially a placeholder, as you can see in Example 16.10. Recall that the diguiseAsSprite() method invokes addBehaviors().

Example 16.10. Adding behaviors to smoking holes


SmokingHole.prototype = {
   ...

   addBehaviors: function () {
      // TODO: Add an array of behaviors to the smoking hole
   },
   ...
};


In Section 16.3.3.3, “Emit Smoke Bubbles,” on p. 436 we modify the addBehaviors() method to add a smoking hole behavior that emits smoke bubbles.


Image Note: A recap

So far we’ve created smoking holes, disguised them as sprites, and added them to the game. Because we added smoking holes to Snail Bait’s array of sprites, Snail Bait invokes each visible smoking hole’s update() and draw() methods every animation frame. For the remainder of this chapter, we flesh out the implementation of SmokingHole.draw() and SmokingHole.update() to draw and update fire particles and smoke bubbles. In addition, we implement two behaviors: one for smoking holes that emits smoke bubbles, and another for smoke bubbles that makes them dissipate.


16.3.2. Incorporate Fire Particles

Snail Bait’s fire particles are small yellow circles that flicker, as shown in Figure 16.4.

Figure 16.4. Fire particles flickering (in the smoking hole in the middle of the screenshots)

Image

Fire particles are simple, so we have only two things to do to incorporate them into Snail Bait:

• Create fire particles

• Draw and update fire particles every animation frame

16.3.2.1. Create Fire Particles

Each smoking hole maintains an array of fire particles, which it creates with a createFireParticles() method, as shown in the revised SmokingHole constructor in Example 16.11.

Example 16.11. Constructing smoking holes: Incorporating fire particles


var SmokingHole = function (smokeBubbleCount, fireParticleCount,
                            left, top, width) {
   this.fireParticles = [];

   this.disguiseAsSprite   (left, top, width);
   this.createFireParticles(fireParticleCount, left, top);
   ...
};


The createFireParticles() method is listed in Example 16.12.

Example 16.12. Creating fire particles


SmokingHole.prototype = {
   ...

   createFireParticles: function (fireParticleCount, left, top) {
      var radius,
          offset;

      for (i = 0; i < fireParticleCount; ++i) {
         radius = Math.random() * 1.5;
         offset = Math.random() * (radius * 2);

         if (i % 2 === 0) {
            fireParticle = this.createFireParticle(left + offset,
                                                   top - offset,
                                                   radius);
         }
         else {
            fireParticle = this.createFireParticle(left - offset,
                                                   top + offset,
                                                   radius);
         }

         this.fireParticles.push(fireParticle);
      }
   },
   ...
};


createFireParticles() randomly varies both the size and position of the fire particles it creates. It then pushes each fire particle onto the smoking hole’s array of fire particles. To create individual fire particles, createFireParticles() delegates to a createFireParticle() method, which is listed in Example 16.13.

Example 16.13. Creating individual fire particles


SmokingHole.prototype = {
   ...

   createFireParticle: function (left, top, radius) {
      var sprite = new Sprite(
             'fire particle',
             this.createFireParticleArtist(left, top, radius));

      sprite.left    = left;
      sprite.top     = top;
      sprite.radius  = radius;
      sprite.visible = true;

      return sprite;
   },
   ...
};


Recall that because they have no graphical representation, smoking holes are not sprites; however, fire particles and smoke bubbles do have graphical representations, so Snail Bait implements them as sprites.

Like all sprites, fire particles are drawn by an artist, which is created by the smoking hole’s createFireParticleArtist() method. That method, which is listed in Example 16.14, is called from createFireParticle() in the previous listing.

Example 16.14. Creating fire particle artists


SmokingHole.prototype = {
   ...

   createFireParticleArtist: function (left, top, radius) {
      var YELLOW_PREAMBLE = 'rgba(255,255,0,';

      return { // Return a JavaScript object with a draw() method
         draw: function (sprite, context) {
            context.save();

            context.fillStyle = YELLOW_PREAMBLE +
                                Math.random().toFixed(2) + '),';

            context.beginPath();
            context.arc(sprite.left, sprite.top,
                        sprite.radius*1.5, 0, Math.PI*2, false);
            context.fill();

            context.restore();
         }
      };
   },
   ...
};


The createFireParticleArtist() method creates an object with a draw() method, as required by all sprite artists. That draw() method draws a yellow filled circle with a random opacity. It’s that random opacity, which is different every time the draw() method draws the particle, that makes particles appear to flicker.

16.3.2.2 Draw and Update Fire Particles Every Animation Frame

Unlike its other sprites, Snail Bait does not draw or update fire particles or smoke bubbles directly. That’s because fire particles and smoke bubbles reside in smoking holes, instead of in Snail Bait’s sprites array; it’s the smoking holes that Snail Bait updates and draws every animation frame. To draw fire particles, we modify the SmokingHole object’s draw() method, as shown in Example 16.15.

Example 16.15. Adding sprite methods, revised for fire particles


SmokingHole.prototype = {
   ...

   addSpriteMethods: function () {
      this.draw = function (context) {
         // TODO: Draw smoke bubbles

         this.drawFireParticles(context);
         ...
      };
      ...
   },
   ...
};


The drawFireParticles() method is listed in Example 16.16.

Example 16.16. Drawing fire particles


SmokingHole.prototype = {
   ...
   drawFireParticles: function (context) {
      for (var i=0; i < this.fireParticles.length; ++i) {
         this.fireParticles[i].draw(context);
      }
   },
   ...
};


drawFireParticles() iterates over the smoking hole’s fire particles, drawing each one in turn. Recall that fire particles are sprites, so their draw() method delegates to the sprite’s artist. For fire particles, that artist is the one listed in Example 16.14.

Now that you’ve seen how to incorporate fire particles into Snail Bait, let’s see how to incorporate smoke bubbles.


Image Note: Fire particles do not have behaviors

Fire particles are sprites, but they don’t have any behaviors. A fire particle’s apparent flickering is caused by its artist, which draws fire particles in yellow with a random alpha component. Smoke bubbles, on the other hand, which are considerably more complicated than fire particles, do have a behavior. That smoke bubble behavior makes the bubbles dissipate.


16.3.3. Incorporate Smoke Bubbles

As Figure 16.5 illustrates, smoke bubbles begin as small opaque circles that dissipate.

Figure 16.5. One smoke bubble dissipates (counterclockwise from bottom to top)

Image

Smoke bubbles, which move, change size, and change opacity over time, are more complicated than fire particles which merely flicker. As a result of that added complexity, we break the task of incorporating smoke bubbles into four steps, as follows:

Create smoke bubbles

Draw and update smoke bubbles every animation frame

• Emit smoke bubbles from a smoking hole

Dissipate smoke bubbles

Let’s look at each step in turn.

16.3.3.1. Create Smoke Bubbles

To create smoke bubbles, we once again revise the SmokingHole constructor as shown in Example 16.17.

Example 16.17. Constructing smoking holes: Incorporate smoke bubbles (final implementation)


var SmokingHole = function (smokeBubbleCount, fireParticleCount,
                            left, top, width) {
   this.smokeBubbles  = [];
   this.fireParticles = [];

   this.disguiseAsSprite   (left, top, width);
   this.createFireParticles(fireParticleCount, left, top);
   this.createSmokeBubbles (smokeBubbleCount, left, top);

   this.smokeBubbleCursor = 0;
};


The final version of the SmokingHole constructor declares a smokeBubbles array, which it fills with smoke bubble objects in the createSmokeBubbles() method, listed in Example 16.18. The constructor also initializes a smokeBubbleCursor variable to 0. That variable, which comes into play when we implement emitting smoke bubbles in Section 16.3.3.3, “Emit Smoke Bubbles,” on p. 436, is an index into the smokeBubbles array.

Example 16.18. Creating smoke bubbles


SmokingHole.prototype = {
   ...

   createSmokeBubbles: function (smokeBubbleCount, left, top) {
      var smokeBubble; // smokeBubble is a sprite

      for (i = 0; i < smokeBubbleCount; ++i) {
         if (i % 2 === 0) { // i is an even number
            smokeBubble = this.createBubbleSprite(
               left + Math.random()*3,
               top - Math.random()*3,
               1,  // radius

               Math.random() * 8,  // velocityX
               Math.random() * 5); // velocityY
         }
         else {
            smokeBubble = this.createBubbleSprite(
               left + Math.random()*10,
               top + Math.random()*6,
               1,  // radius
               Math.random() * 8,  // velocityX
               Math.random() * 5); // velocityY
         }

         this.setInitialSmokeBubbleColor(smokeBubble, i);

         if (i < 10) {
            // Make sure colored smoke bubbles don't overtake
            // the grayscale bubbles on top.
            smokeBubble.dissipatesSlowly =  true;
         }

         this.smokeBubbles.push(smokeBubble);
      }
   },
   ...
};


The createSmokeBubbles() method creates smoke bubbles with varying characteristics by invoking a helper method—createBubbleSprite()—that creates individual smoke bubbles. The createSmokeBubbles() method then sets the initial colors for the smoke bubble with the setInitialSmokeBubbleColor() method listed in Example 16.19 and sets the dissipatesSlowly flag for the first 10 smoke bubbles.

Example 16.19. Setting smoke bubble initial colors


SmokingHole.prototype = {
   ...

   setInitialSmokeBubbleColor: function (smokeBubble, i) {
      var ORANGE = 'rgba(255,104,31,0.3)',
          YELLOW = 'rgba(255,255,0,0.3)',
          BLACK  = 'rgba(0,0,0,0.5)';

      if (i <= 5)       smokeBubble.fillStyle = BLACK;
      else if (i <= 8)  smokeBubble.fillStyle = YELLOW;
      else if (i <= 10) smokeBubble.fillStyle = ORANGE;
      else
         smokeBubble.fillStyle =
            'rgb('  + (220+Math.random()*35).toFixed(0) +
            ',' + (220+Math.random()*35).toFixed(0) +
            ',' + (220+Math.random()*35).toFixed(0) + ')';
   },
   ...
};


The first 10 smoke bubbles are black-, yellow-, or orange-colored bubbles that the game draws underneath the grayscale bubbles, to make the smoke look more realistic by creating a fiery backdrop. Those first 10 smoke bubbles also dissipate slowly so they don’t expose the fiery backdrop illusion by overtaking the grayscale bubbles above them. The first 10 smoke bubbles dissipate slowly because the SmokingHole constructor sets their dissipatesSlowly property to true.

As you saw in Example 16.18, individual smoke bubbles—which are sprites—are created by the SmokingHole.createBubbleSprite() method, listed in Example 16.20.

Example 16.20. Creating smoke bubble sprites


SmokingHole.prototype = {
   ...

   createBubbleSprite: function (left, top, radius,
                                 velocityX, velocityY) {
      var DEFAULT_BUBBLE_LIFETIME = 10000; // 10 seconds

      sprite = new Sprite('smoke bubble',
                           this.createBubbleArtist(),
                           [ this.createDissipateBubbleBehavior() ]);

      this.setBubbleSpriteProperties(sprite, left, top, radius,
                                     velocityX, velocityY);

      this.createBubbleSpriteTimer(sprite, DEFAULT_BUBBLE_LIFETIME);

      return sprite;
   },
   ...
};


The createBubbleSprite() method creates a new sprite whose type is smoke bubble, with a bubble artist and a behavior that dissipates the bubble. Subsequently, createBubbleSprite() method sets initial values for the sprite’s properties with setBubbleSpriteProperties(), listed in Example 16.21.

Example 16.21. Setting bubble sprite properties


SmokingHole.prototype = {
   ...

   setBubbleSpriteProperties: function (sprite, left, top, radius,
                                        velocityX, velocityY) {
      sprite.left   = left;
      sprite.top    = top;
      sprite.radius = radius;

      sprite.originalLeft   = left;
      sprite.originalTop    = top;
      sprite.originalRadius = radius;

      sprite.velocityX = velocityX;
      sprite.velocityY = velocityY;
   },
   ...
};


The createBubbleArtist() method, invoked from createBubbleSprite() in Example 16.20 and listed in Example 16.22, creates the smoke bubble’s artist.

Example 16.22. Creating the bubble artist


SmokingHole.prototype = {
   ...

   createBubbleArtist: function () {
      return {
         draw: function (sprite, context) {
            var TWO_PI = Math.PI * 2;

            if (sprite.radius > 0) {
               context.save();
               context.beginPath();

               context.fillStyle = sprite.fillStyle;
               context.arc(sprite.left, sprite.top,
                           sprite.radius, 0, TWO_PI, false);

               context.fill();
               context.restore();
            }
         }
      };
   },
   ...
};


Smoke bubble artists simply draw a filled circle in the game’s canvas. Recall that the circle’s fill style was set by SmokingHole.setInitialSmokeBubbleColor(), listed in Example 16.19.

After setting the bubble sprite’s properties, the createBubbleSprite() method listed in Example 16.23 creates a timer with the createBubbleSpriteTimer() method, listed in Example 16.23.

Example 16.23. Bubble sprite helper methods


SmokingHole.prototype = {
   ...

   createBubbleSpriteTimer: function (sprite, bubbleLifetime) {
      sprite.timer = new AnimationTimer(
           bubbleLifetime,
           AnimationTimer.makeEaseOutEasingFunction(1.5));
      ...
   },
   ...
};


The smoke bubble’s timer is used by the smoke bubble’s dissipate bubble behavior, discussed in Section 16.3.3.4, “Dissipate Smoke Bubbles,” on p. 438.

The timer runs for 10 seconds, during which it modifies the flow of time with an easing-out function. That easing-out function means that smoke bubbles expand more slowly as they become larger; see Chapter 9 for more information about easing functions.

Now that you’ve seen how Snail Bait creates smoke bubbles, let’s see how the game draws and updates them.

16.3.3.2. Draw and Update Smoke Bubbles Every Animation Frame

Recall that Snail Bait treats smoking holes as sprites, even though they are not. That means Snail Bait invokes each visible smoking hole’s draw() and update() methods every animation frame. In Example 16.24, we update those methods.

Example 16.24. SmokingHole.addSpriteMethods(), revised


SmokingHole.prototype = {
   ...

   addSpriteMethods: function () {
      this.draw = function (context) {
         this.drawFireParticles(context);
         this.drawSmokeBubbles(context);
      };
      this.update = function (now, fps,
                              context, lastAnimationFrameTime)  {
         this.updateSmokeBubbles(now, fps, context,
                                 lastAnimationFrameTime);
         ...
      };
   },
   ...
};


When Snail Bait draws smoking holes, smoking holes draw their fire particles and smoke bubbles. When Snail Bait updates smoking holes, smoking holes update their smoke bubbles (but don’t update their fire particles because fire particles have no behaviors). The SmokingHole.drawSmokeBubbles() and SmokingHole.updateSmokeBubble() methods are listed in Example 16.25.

Example 16.25. Drawing and updating smoke bubbles


SmokingHole.prototype = {
   ...

   drawSmokeBubbles: function (context) {
      for (var i=0; i < this.smokeBubbles.length; ++i) {
         this.smokeBubbles[i].draw(context);
      }
   },

   updateSmokeBubbles: function (now, fps, context,
                                 lastAnimationFrameTime) {
      for (var i=0; i < this.smokeBubbles.length; ++i) {
         this.smokeBubbles[i].update(now, fps, context,
                                     lastAnimationFrameTime);
      }
   },
   ...
};


The preceding methods are straightforward. Each iterates over the smoking hole’s smokeBubbles array and either draws or updates individual smoke bubbles.

Now that you’ve seen how to create smoke bubbles and how Snail Bait draws and updates them, two things remain: emitting smoke bubbles from a smoking hole and subsequently dissipating the smoke bubble. First, let’s look at emitting smoke bubbles.

16.3.3.3 Emit Smoke Bubbles

Smoking holes emit smoke bubbles with a behavior, and because smoking holes are not really sprites, we must manually execute their behaviors. We do that in SmokingHole.update(), as shown in Example 16.26.

Example 16.26. SmokingHole.update()s final implementation


SmokingHole.prototype = {
   ...

   addSpriteMethods: function () {
      this.draw = function (context) {
         this.drawFireParticles(context);
         this.drawSmokeBubbles(context);
      };

      this.update = function (now, fps,
                              context, lastAnimationFrameTime) {

         this.updateSmokeBubbles(now, fps, context,
                                 lastAnimationFrameTime);

         for (var i=0; i < this.behaviors.length; ++i) {
            this.behaviors[i].execute(this, now, fps,
                                      context, lastAnimationFrameTime);
         }
      };
   },
   ...
};


Next, we create the smoking hole’s behavior with the SmokingHole. addBehaviors() method that we introduced in Section 16.3.1, “Disguise Smoking Holes as Sprites,” on p. 421. That method, as you can see in Example 16.27, creates an array with a single behavior.

Example 16.27. Emitting smoke bubbles


SmokingHole.prototype = {
   ...

   addBehaviors: function () {
       this.behaviors = [
         {
            ...
            execute: function (sprite, now, fps,
                               context, lastAnimationFrameTime) {
               // Reveal a smoke bubble every animation frame
               // until all the smoking hole's smoke bubbles have
               // been revealed.

               if (sprite.hasMoreSmokeBubbles()) {
                  sprite.emitSmokeBubble();
                  sprite.advanceCursor();
               }
            }
         }
      ];
   },
   ...
};


Snail Bait invokes the preceding behavior’s execute() method for every animation frame in which the smoking hole is visible, passing to the execute() method the smoking hole to which the behavior is attached. The execute() method checks to see if the smoking hole has more smoke bubbles to emit; if so, it emits the next smoke bubble and advances the cursor into the smoking hole’s array of smoke bubbles. The helper methods invoked in Example 16.27 are listed in Example 16.28.

Example 16.28. Smoking hole behavior support methods


SmokingHole.prototype = {
   ...

   hasMoreSmokeBubbles: function () {
      return this.smokeBubbleCursor !== this.smokeBubbles.length-1;
   },

   emitSmokeBubble: function () {
      this.smokeBubbles[this.smokeBubbleCursor].visible = true;
   },

   advanceCursor: function () {
      if (this.smokeBubbleCursor <= this.smokeBubbles.length - 1) {
          ++this.smokeBubbleCursor;
      }
      else {
         this.smokeBubbleCursor = 0;
      }
   },
   ...
};


That’s all there is to emitting smoke bubbles. Next, let’s see how they dissipate.

16.3.3.4 Dissipate Smoke Bubbles

As you saw in the preceding section, smoking holes emit smoke bubbles with a behavior. Likewise, a smoke bubble behavior dissipates the bubble. That behavior’s execute() method is listed in Example 16.29.

Example 16.29. Create the dissipate bubble behavior


SmokingHole.prototype = {
   ...

   createDissipateBubbleBehavior: function () {
      return {
         FULLY_OPAQUE: 1.0,
         BUBBLE_EXPANSION_RATE: 15,
         BUBBLE_SLOW_EXPANSION_RATE: 10,
         BUBBLE_X_SPEED_FACTOR: 8,
         BUBBLE_Y_SPEED_FACTOR: 16,

         execute: function (sprite, now, fps, context,
                            lastAnimationFrameTime) {
            if ( ! sprite.timer.isRunning()) {
               sprite.timer.start(now);
            }
            else if ( ! sprite.timer.isExpired(now)) {
               this.dissipateBubble(sprite, now,
                               fps, lastAnimationFrameTime);
            }
            else { // timer is expired
               sprite.timer.reset();
               this.resetBubble(sprite, now); // resets the timer
            }
         },
         ...
      };
   },
   ...
};


The preceding behavior’s execute() method dissipates bubbles when the bubble’s timer is running and has not expired. If the timer is not running, the execute() method starts it, and if the timer has expired, the execute() method resets the timer and the bubble.

The dissipateBubble() and resetBubble() helper methods used in Example 16.29 are listed in Example 16.30.

Example 16.30. The dissipate bubble behavior’s helper methods


SmokingHole.prototype = {
   ...

   createDissipateBubbleBehavior: function () {
      return {
         ...

         dissipateBubble: function (sprite, now, fps,
                               lastAnimationFrameTime) {
            var elapsedTime = sprite.timer.getElapsedTime(now),
                velocityFactor = (now - lastAnimationFrameTime) / 1000;

            sprite.left += sprite.velocityX * velocityFactor;
            sprite.top  -= sprite.velocityY * velocityFactor;

            sprite.opacity = this.FULLY_OPAQUE - elapsedTime /
                             sprite.timer.duration;

            if (sprite.dissipatesSlowly) {
                sprite.radius +=
                   this.BUBBLE_SLOW_EXPANSION_RATE * velocityFactor;
            }
            else {
               sprite.radius += this.BUBBLE_EXPANSION_RATE *
                                velocityFactor;
            }
         },

         resetBubble: function (sprite, now) {
            sprite.opacity = this.FULLY_OPAQUE;
            sprite.left    = sprite.originalLeft;
            sprite.top     = sprite.originalTop;
            sprite.radius  = sprite.originalRadius;

            sprite.velocityX = Math.random() *
                               this.BUBBLE_X_SPEED_FACTOR;

            sprite.velocityY = Math.random() *
                               this.BUBBLE_Y_SPEED_FACTOR;

            sprite.opacity = 0;
         }
      };
   },
   ...
};


The dissipateBubble() method dissipates bubbles by adjusting their location, size, and opacity to make bubbles grow larger and less opaque. Recall from Example 16.20 that the smoke bubble’s timer runs for 10 seconds and uses an easing-out easing function, which causes a smoke bubble’s expansion to slow as it grows.

The resetBubble() method resets smoke bubbles to their initial configurations, with a random component mixed in for their velocity.

At this point, smoking holes are spewing smoke according to plan; however, if you pause the game and resume it, the smoking holes will not resume exactly where they left off. We take care of that last implementation detail in the next section.

16.4. Pause Smoking Holes

Recall that behaviors that time their activities should implement pause() and unpause() methods to stay in sync with the game. As you can see from Example 16.31, smoking hole behaviors implement pause() and unpause() methods, which iterate over the smoking hole’s smoke bubbles, pausing or unpausing each one.

Example 16.31. Adding pause() and unpause() methods to smoke bubble behaviors


SmokingHole.prototype = {
   ...
   addBehaviors: function () {
      this.behaviors = [
         {
            pause: function (sprite, now) {
               for (i=0; i < sprite.smokeBubbles.length; ++i) {
                  sprite.smokeBubbles[i].pause(now);
               }
            },

            unpause: function (sprite, now) {
               for (i=0; i < sprite.smokeBubbles.length; ++i) {
                  sprite.smokeBubbles[i].unpause(now);
               }
            },
            ...
         }
      ];
   },
   ...
};


The smoke bubble pause() and unpause() methods use the smoke bubble’s timer. The createBubbleSpriteTimer() method creates the timer and implements the methods, as shown in Example 16.32.

Example 16.32. Adding pause() and unpause() methods to smoke bubble behaviors


SmokingHole.prototype = {
   ...

   createBubbleSpriteTimer: function (sprite, bubbleLifetime) {
      sprite.timer = new AnimationTimer(
           bubbleLifetime,
           AnimationTimer.makeEaseOutEasingFunction(1.5));

      sprite.pause = function (now) {
         this.timer.pause(now);
      };

      sprite.unpause = function (now) {
         this.timer.unpause(now);
      };
   },
   ...
};


16.5. Conclusion

This chapter is ostensibly about particle systems. In this case the particle system is a smoking hole that displays fire particles and emits smoke bubbles, but the concepts easily translate to other types of particle systems that emit other kinds of particles.

An underlying theme in this chapter is the implementation of sprites that contain other sprites. In our case, smoking holes, which strictly speaking are not sprites but behave like them, contain fire particles and smoke bubbles, which are sprites.

Another underlying theme in this chapter is the use of duck typing. Because smoking holes themselves do not have a graphical representation, we did not implement them as sprites; however, it was convenient for the game to treat them as though they were sprites, and to do that we used duck typing.

Some programming languages force objects to conform to specific interfaces; for example, if you want to treat something as a sprite, it must actually be a sprite. Duck typing, on the other hand, does not force objects to conform to interfaces; for example, if you want to treat something as a sprite, it doesn’t actually have to be a sprite, it just needs to act like one. If something looks like a duck and acts like a duck, duck typing treats it like a duck without requiring duck credentials.

Duck typing let us disguise smoking holes as sprites by merely adding methods and properties that look identical to sprite methods and properties. Perhaps most importantly, we did not have to modify a class hierarchy to get Snail Bait to accept smoking holes as sprites. In some object-oriented languages, maintaining class hierarchies can become cumbersome.

16.6. Exercises

1. Change the number of smoke bubbles to 10 and run the game. What happened to the smoke?

2. Increment the number of smoke bubbles by 5, run the game, and watch the fps meter in the upper-left corner. Did it change considerably after you added 5 smoke bubbles? If not, add 5 more and keep adding 5 at a time until you see an impact on performance.

3. As implemented in this chapter, smoking holes immediately start smoking after they are created. Modify the SmokingHole constructor to introduce a delay by setting the smoking hole’s visibility to false, and then, after a timeout lasting for a specific number of seconds, reset the smoking hole’s visiblity to true.

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

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