HTML5 2D game development: Implementing Sprite behaviors

Equipping Snail Bait's characters with behaviors

In this series, HTML5 maven David Geary shows you how to implement an HTML5 2D video game one step at a time. In this installment, you'll learn how to implement the essence of any video game: sprite behaviors.

Share:

David Geary, Author and speaker, Clarity Training, Inc.

David GearyThe author of Core HTML5 Canvas, David Geary is also the co-founder of the HTML5 Denver User's Group and the author of eight Java books, including the best-selling books on Swing and JavaServer Faces. David is a frequent speaker at conferences, including JavaOne, Devoxx, Strange Loop, NDC, and OSCON, and he is a three-time JavaOne Rock Star. He wrote the JSF 2 fu and GWT fu article series for developerWorks. You can follow David on Twitter at @davidgeary.



08 January 2013

Also available in Chinese Russian Japanese Spanish

Great stories have great characters. And like books and movies, video games also need characters with interesting behaviors. For example, the protagonist in Braid — the best-selling platform game of all time — can manipulate time. That ingenious behavior set the game apart from its contemporaries.

Behaviors are the soul of any video game, and adding behaviors to Snail Bait's inert sprites implemented in the previous installment immediately makes the game more interesting, as shown in Figure 1:

Figure 1. The state of Snail Bait at the end of this article
Screen shot of Snail Bait state at the end of this article

Recall from the Sprite objects section in the preceding article that Snail Bait's sprites don't implement their own activities such as running, jumping, or exploding. Instead, sprites rely on other objects — known as behaviors — to control how they act.

Figure 1 shows the snail shooting a snail bomb. Other behaviors that you can't see in Figure 1's static image are:

  • The runner runs
  • Buttons pace back and forth on their platforms
  • Rubies and sapphires sparkle

Table 1 summarizes those behaviors:

Table 1. Behaviors discussed in this article
SpritesBehaviorDescription
Buttons, snails paceBehavior Paces back and forth along a platform
Runner runBehavior Cycles through the runner's images to make it appear as though the runner is running
Snail snailShootBehavior Shoots a snail bomb from the snail's mouth
Snail cycleBehavior Cycles through a sprite's images
Snail bomb snailBombMoveBehavior Moves the snail bomb horizontally to the left while it's visible in the canvas

Manipulating time

In Braid, the protagonist Tim manipulates time, but every video game is a master at manipulating time. In this article, you will see the undercurrent of time flowing through behaviors. And in the next two articles in this series, I'll show you how to bend time itself to implement nonlinear motion, which is the basis for realistic motion such as running and jumping.

The behaviors listed in Table 1 represent less than half of the game's behaviors — as you can see from the Snail Bait's sprites and behaviors table in the first article in this series. They are also the most basic of the sprites' behaviors; jumping, for example, is considerably more complex, as you will see in forthcoming articles. Nonetheless, there's a lot to learn from the implementation of the simpler behaviors in this article, including how to:

  • Implement behaviors and assign them to sprites
  • Cycle a sprite through a sequence of images
  • Create flyweight behaviors to save on memory usage
  • Combine behaviors
  • Use behaviors to shoot projectiles

Behavior fundamentals

Replica Island's behaviors

The idea for behaviors comes from a popular open source Android game named Replica Island. Many of Snail Bait's graphics also come from Replica Island. See Resources for links to the game and to a blog post in which the game's creator talks about behaviors.

Any object can be a behavior as long as it has an execute() method. That method takes three arguments: a sprite, the time, and the frame rate for the game's animation. A behavior's execute() method modifies the sprite's state depending on the time and animation frame rate.

Behaviors are powerful because:

  • They decouple sprites from the way they behave.
  • You can change sprite behaviors at run time.
  • You can implement behaviors that work with any sprite.
  • Stateless behaviors can be used as flyweights.

Before I discuss the implementation details of the behaviors listed in Table 1, I'll give you a high-level overview of behaviors — how to implement them and associate them with sprites — by looking at the runner's collective behaviors.


Runner behaviors

Snail Bait's runner has four behaviors, listed in Table 2:

Table 2. The runner's behaviors
BehaviorDescription
runBehavior Cycles through the runner's cells from the sprite sheet to make it appear as though the runner is running
jumpBehavior Controls all aspects of jumping: ascent, descent, and landing
fallBehavior Controls the vertical movement of the runner as she falls
runnerCollideBehavior Detects, and reacts to, collisions between the runner and the other sprites

I specify the runner's behaviors with an array of objects that I pass to the Sprite constructor, as shown in Listing 1:

Listing 1. Creating SnailBait's runner
var SnailBait = function () {
   ...

   this.runner = new Sprite('runner',              // Type
                            this.runnerArtist,    //  Artist
                            [this.runBehavior,  //  Behaviors
                          this.jumpBehavior,
                          this.fallBehavior,
                          this.runnerCollideBehavior

                           ]);
};

The runner's behaviors are shown in Listing 2, with implementation details removed:

Listing 2. Runner behavior objects
var SnailBait = function () {
   ...

   this.runBehavior = {
      execute: function(sprite, time, fps) { // sprite is the runner
         ...
      }
   };
   this.jumpBehavior = {
      execute: function(sprite, time, fps) { // sprite is the runner
         ...
      }
   };
   this.fallBehavior = {
      execute: function(sprite, time, fps) { // sprite is the runner
         ...
      }
   };
   this.runnerCollideBehavior = {
      execute: function(sprite, time, fps) { // sprite is the runner
         ...
      }
   };
};

Every animation frame, Snail Bait iterates over its array of sprites, invoking each sprite's update() method, shown in Listing 3:

Listing 3. Executing behaviors
Sprite.prototype = {
   update: function (time, fps) {
      for (var i=0; i < this.behaviors.length; ++i) {
         if (this.behaviors[i] === undefined) { // You never know 
            return;
         }

         this.behaviors[i].execute(this, time, fps);
      }
   }
};

The Sprite.update() method iterates over the sprite's behaviors, invoking each behavior's execute() method. Snail Bait continuously — once per animation frame — invokes all behaviors associated with all visible sprites. A behavior's execute() method, therefore, is not like most other methods, which are invoked relatively infrequently; instead, each execute() method is like a little motor that's constantly running.

Now that you understand how sprites and behaviors fit together, I'll concentrate on implementing them individually.

The Strategy design pattern

Behaviors are an implementation of the Strategy design pattern, which encapsulates algorithms into objects (see Resources). At run time, you can mix and match those algorithms to assign a collection of behaviors to a sprite. Behaviors give you more flexibility than hard-coding their algorithms directly into individual sprites.

Running

Snail Bait does two things that make it appear as though the runner is running. First, as I discussed in the Scrolling the background section in the second article in this series, the game continuously scrolls the background, making it appear as though the runner is moving horizontally. Second, the runner's run behavior cycles the runner through a sequence of images from the game's sprite sheet, as shown in Figure 2:

Figure 2. Running sequence
Screen shot of the Snail Bait runner's running sequence

The code in Listing 4 implements the run behavior:

Listing 4. The runner's runBehavior
var SnailBait =  function () {
   ...
   this.BACKGROUND_VELOCITY = 32, // pixels/second
   this.RUN_ANIMATION_RATE = 17, // frames/second
   ...

   this.runAnimationRate,

   this.runBehavior = {
      // Every runAnimationRate milliseconds, this behavior advances the
      // runner's artist to the next frame of the sprite sheet.

      lastAdvanceTime: 0,
      
      execute: function(sprite, time, fps) {
         if (sprite.runAnimationRate === 0) {
            return;
         }
         
         if (this.lastAdvanceTime === 0) {  // skip first time
            this.lastAdvanceTime = time;
         } 
         else if (time - this.lastAdvanceTime > 1000 / sprite.runAnimationRate) {
            sprite.artist.advance();
            this.lastAdvanceTime = time;
         }
      }
   },
   ...
};

The runBehavior object's execute() method periodically advances the runner's artist to the next image in the runner's sequence from the sprite sheet. (You can see Snail Bait's sprite sheet in the Sprite artists and sprite sheets section in the fourth article in this series.)

How often the runBehavior advances the runner's image determines how quickly the runner runs. That time interval is set with the runner's runAnimationRate attribute. The runner is not running when the game starts, so its runAnimationRate is initially zero. When the player turns left or right, however, Snail Bait sets that attribute to 17 frames/second, as shown in Listing 5, and the runner starts to run:

Listing 5. Turning starts the run animation
SnailBait.prototype = {
   ...
            
   turnLeft: function () {
      this.bgVelocity = -this.BACKGROUND_VELOCITY;
      this.runner.runAnimationRate = this.RUN_ANIMATION_RATE; // 17 fps, see Listing 4
      this.runnerArtist.cells = this.runnerCellsLeft;
      this.runner.direction = this.LEFT;
   },

   turnRight: function () {
      this.bgVelocity = this.BACKGROUND_VELOCITY;
      this.runner.runAnimationRate = this.RUN_ANIMATION_RATE; // 17 fps, see Listing 4
      this.runnerArtist.cells = this.runnerCellsRight;
      this.runner.direction = this.RIGHT;
   },

};

The flow of time

Like the runner's run behavior, nearly all behaviors are predicated on time. And because a game's animation is constantly in effect, many functions that modify a game's behavior, such as turnLeft() and turnRight() in Listing 5, do so by simply setting game variables. When the game draws the next animation frame, those variables influence the game's behavior.

The turnLeft() and turnRight() methods, which are invoked by the game's keyboard event handlers, control how quickly the runner cycles through its image sequence with the runAnimationRate attribute, as I discussed previously. Those methods also control how fast the runner moves from left to right by setting the bgVelocity attribute, which represents the rate at which the background scrolls.


Flyweight behaviors

The runner's run behavior discussed in the preceding section maintains state — namely, the time the behavior last advanced the sprite's image. That state tightly couples the runner to the behavior. So, for instance, if you wanted to make another sprite run, you would need to have another run behavior.

Behaviors that do not maintain state are more flexible; for example, they can be used as flyweights. A flyweight is a single instance of an object, used by many other objects simultaneously. Figure 3 illustrates a stateless pace behavior that makes sprites pace back and forth on a platform. A single instance of that behavior is used for the game's buttons and its snail, all of which pace back and forth on their platforms, shown in Figure 3:

Figure 3. Button pacing sequence
Screen shot of the Snail Bait button pacing sequence

Listing 6 shows Snail Bait's createButtonSprites() method, which adds the lone pace behavior to each button:

Listing 6. Creating pacing buttons
SnailBait.prototype = {
   ...

   createButtonSprites: function () {
      var button,
          buttonArtist = new SpriteSheetArtist(this.spritesheet,
                                               this.buttonCells),
      goldButtonArtist = new SpriteSheetArtist(this.spritesheet,
                                               this.goldButtonCells);

      for (var i = 0; i < this.buttonData.length; ++i) {
         if (i === this.buttonData.length - 1) {
            button = new Sprite('button',
                                 goldButtonArtist,
                                 [ this.paceBehavior ]);
         }
         else {
            button = new Sprite('button',
                                 buttonArtist,
                                 [ this.paceBehavior ]);
         }

         button.width = this.BUTTON_CELLS_WIDTH;
         button.height = this.BUTTON_CELLS_HEIGHT;

         button.velocityX = this.BUTTON_PACE_VELOCITY;
         button.direction = this.RIGHT;

         this.buttons.push(button);
      }
   },
   ...
};

Listing 7 shows the paceBehavior object:

Listing 7. The pace behavior
var SnailBait = function () {
   ...

   this.paceBehavior = {
      checkDirection: function (sprite) {
         var sRight = sprite.left + sprite.width,
             pRight = sprite.platform.left + sprite.platform.width;

         if (sRight > pRight && sprite.direction === snailBait.RIGHT) {
            sprite.direction = snailBait.LEFT;
         }
         else if (sprite.left < sprite.platform.left &&
                  sprite.direction === snailBait.LEFT) {
            sprite.direction = snailBait.RIGHT;
         }
      },
      
      moveSprite: function (sprite, fps) {
         var pixelsToMove = sprite.velocityX / fps;

         if (sprite.direction === snailBait.RIGHT) {
            sprite.left += pixelsToMove;
         }
         else {
            sprite.left -= pixelsToMove;
         }
      },

      execute: function (sprite, time, fps) {
         this.checkDirection(sprite);
         this.moveSprite(sprite, fps);
      }
   },

The pace behavior modifies a sprite's horizontal position. The behavior implements time-based motion to calculate how many pixels to move the sprite for the current animation frame by dividing the sprite's velocity (which is specified in pixels/second) by the animation's frame rate (in frames/second), which results in pixels/frame. (See the Time-based motion section in the second article in this series for more information about time-based motion.)


Game-unspecific behaviors

Flyweights and state

The paceBehavior can be used as a flyweight because it's stateless. It's stateless because it stores state — each sprite's position and direction — in the sprites themselves.

The first behavior I discussed in this article — runBehavior — is a stateful behavior that's tightly coupled to a single sprite. The paceBehavior, which I discussed next, is a stateless behavior, which decouples it from individual sprites, so a single instance can be used by multiple sprites.

Behaviors can be generalized even further: You can decouple them not only from individual sprites, but also from the game itself. Snail Bait uses three behaviors that can be used in any game:

  • bounceBehavior
  • cycleBehavior
  • pulseBehavior

The bounce behavior bounces a sprite up and down, the cycle behavior cycles a sprite through a set of images, and the pulse behavior manipulates a sprite's opacity to make it appear as though the sprite is pulsating.

The bounce and pulse behaviors both involve nonlinear animation, which I will discuss in forthcoming articles. The cycle behavior cycles through a sprite's images linearly, however, so I will use the implementation of that behavior to illustrate implementing behaviors that can be used in any game.

Sparkling rubies

Snail Bait's rubies and sapphires sparkle, as shown in Figure 4:

Figure 4. Sparkling ruby sequence
Screen shot of Snail Bait's sparkling-ruby sequence

Snail Bait's sprite sheet contains sequences of images for both rubies and sapphires; cycling through those images creates the sparkling illusion.

Listing 8 shows the Snail Bait method that creates rubies. A nearly identical method (not shown) creates sapphires. The createRubySprites() method also creates a cycle behavior that every 500ms displays the next image from the ruby-sparkling sequence for 100ms.

Listing 8. Creating rubies
SnailBait.prototype = {
   ...
   createRubySprites: function () {
      var ruby,
          rubyArtist = new SpriteSheetArtist(this.spritesheet, this.rubyCells);
   
      for (var i = 0; i < this.rubyData.length; ++i) {
         ruby = new Sprite('ruby', rubyArtist,
                           [ new CycleBehavior(100,     // animation duration
                                                 500) ]); // interval between animations
         ...
      }
   },
   ...
};

Listing 9 shows the cycle behavior:

Listing 9. The CycleBehavior behavior
// This behavior advances the sprite artist through
// the sprite's images at a specified animation rate.

CycleBehavior = function (duration, interval) {
   this.duration = duration || 0;  //  milliseconds
   this.interval = interval || 0;
   this.lastAdvance = 0;
};

CycleBehavior.prototype = { 
   execute: function(sprite, time, fps) {
      if (this.lastAdvance === 0) { 
         this.lastAdvance = time;
      }

      // During the interval start advancing if the interval is over

      if (this.interval && sprite.artist.cellIndex === 0) {
         if (time - this.lastAdvance < this.interval) {
            sprite.artist.advance();
            this.lastAdvance = time;
         }
      }
      // Otherwise, if the behavior is cycling, advance if duration is over

      else if (time - this.lastAdvance > this.duration) {
         sprite.artist.advance();
         this.lastAdvance = time;
      }
   }
};

Generalizing behaviors

It's a good idea to look for opportunities to generalize behaviors so they can be used in a wider range of circumstances.

The cycle behavior will work with any sprite that has a sprite sheet artist, meaning the behavior is not specific to Snail Bait and so can be reused in a different game. The sprite-specific run behavior in Listing 4 has a lot in common with the game-unspecific cycle behavior in Listing 9; in fact, the cycle behavior was derived from the run behavior. (The run behavior could be a more general cycle behavior, but the run behavior also takes into account the runner's animation rate.)


Combining behaviors

Individual behaviors encapsulate specific actions such as running, pacing, or sparkling. You can also combine behaviors for more complicated effects; for example, as the snail paces back and forth on its platform, it periodically shoots snail bombs, as shown in Figure 5:

Figure 5. The snail shooting sequence
Screen shot of Snail Bail's snail shooting sequence

The snail shooting sequence is a combination of three behaviors:

  • paceBehavior
  • snailShootBehavior
  • snailBombMoveBehavior

paceBehavior and snailShootBehavior are associated with the snail; snailBombMoveBehavior is associated with snail bombs. When Snail Bait creates sprites, it specifies the first two behaviors in the Sprite constructor, as you can see in Listing 10:

Listing 10. Creating snails
SnailBait.prototype = {
   ...

   createSnailSprites: function () {
      var snail,
          snailArtist = new SpriteSheetArtist(this.spritesheet, this.snailCells);
   
      for (var i = 0; i < this.snailData.length; ++i) {
         snail = new Sprite('snail',
                            snailArtist,
                            [ this.paceBehavior,
                              this.snailShootBehavior,
                              new CycleBehavior(300,  // 300ms per image
                                                1500) // 1.5 seconds between sequences
                            ]);

         snail.width  = this.SNAIL_CELLS_WIDTH;
         snail.height = this.SNAIL_CELLS_HEIGHT;

         snail.velocityX = this.SNAIL_PACE_VELOCITY;
         snail.direction = this.RIGHT;

         this.snails.push(snail); // Push snail onto snails array
      }
   },
};

Every 1.5 seconds, the snail's CycleBehavior cycles through the snail's images in the sprite sheet, shown in Figure 6, and displays each image for 300 ms, which makes it look as though the snail is periodically opening and closing its mouth. The snail's paceBehavior moves the snail back and forth on its platform.

Figure 6. Sprite sheet images for the Snail shooting sequence
Screen shot of the sprite sheet images for Snail Bait's snail shooting sequence

Snail bombs are created by the armSnails() method, shown in Listing 11, which Snail Bait calls when the game begins. That method iterates over the game's snails, creates a snail bomb for each snail, equips each bomb with a snailBombMoveBehavior, and stores a reference to the snail in the snail bomb.

Listing 11. Arming snails
SnailBait.prototype = {
   ...

   armSnails: function () {
      var snail,
          snailBombArtist = new SpriteSheetArtist(this.spritesheet, this.snailBombCells);

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

         snail.bomb = new Sprite('snail bomb',
                                  snailBombArtist,
                                  [ this.snailBombMoveBehavior ]);

         snail.bomb.width  = snailBait.SNAIL_BOMB_CELLS_WIDTH;
         snail.bomb.height = snailBait.SNAIL_BOMB_CELLS_HEIGHT;

         snail.bomb.top = snail.top + snail.bomb.height/2;
         snail.bomb.left = snail.left + snail.bomb.width/2;
         snail.bomb.visible = false;

         this.sprites.push(snail.bomb);
      }
   },
};

The snail's snailShootBehavior shoots the snail's snail bomb, as shown in Listing 12:

Listing 12. Shooting snail bombs
SnailBait.prototype = {
   ...

   this.snailShootBehavior = { // sprite is the snail
      execute: function (sprite, time, fps) {
         var bomb = sprite.bomb;

         if (! bomb.visible && sprite.artist.cellIndex === 2) {
            bomb.left = sprite.left;
            bomb.visible = true;
         }
      }
   },

};

Behavior-based games

With a behavior-based game, once you've got the basic infrastructure implemented, fleshing out the game is mostly a matter of implementing behaviors. Freed from the concerns of the game's underlying mechanics, such as animation, frame rates, scrolling backgrounds, and so forth, you can make your game come to life by concentrating almost exclusively on implementing behaviors. And because behaviors can be mixed and matched at runtime, you can rapidly prototype scenarios by combining behaviors.

Because the snailShootBehavior is associated with the snail, the sprite passed to the behavior's execute() method is the snail.

A snail maintains a reference to its snail bomb, so the snailShootBehavior accesses the bomb through the snail. The snailShootBehavior then checks to see if the snail's current image is the one on the far right in Figure 6, meaning the snail is on the verge of opening its mouth; if that's the case, the behavior puts the bomb in the snail's mouth and makes it visible.

Shooting the snail bomb, therefore, involves positioning the bomb and making it visible under the right conditions. Subsequently moving the bomb is the responsibility of the snailBombMoveBehavior, shown in Listing 13:

Listing 13. Snail bomb move behavior
SnailBait = function () {
   this.SNAIL_BOMB_VELOCITY = 450,
   ...
};

SnailBait.prototype = {
   this.snailBombMoveBehavior = {
      execute: function(sprite, time, fps) {  // sprite is the bomb
         if (sprite.visible && snailBait.spriteInView(sprite)) {
            sprite.left -= snailBait.SNAIL_BOMB_VELOCITY / fps;
         }

         if (!snailBait.spriteInView(sprite)) {
            sprite.visible = false;
         }
      }
   },

As long as the snail bomb is in view, the snailBombMoveBehavior moves the bomb to the left at a rate of snailBait.SNAIL_BOMB_VELOCITY (450) pixels/second. Once the bomb has moved out of view, the behavior makes the bomb invisible.


Next time

In the next article in this series, I delve further into time and behaviors by examining the runner's jump behavior. You'll see how to implement a JavaScript stopwatch to time the jump. That fundamental technique — timing animations — is something that you'll use a lot in your own games.


Download

DescriptionNameSize
Sample codej-html5-game5.zip1.2MB

Resources

Learn

Get products and technologies

  • Replica Island: You can download the source for this popular open source platform video game for Android.

Discuss

  • Get involved in the developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Web development
ArticleID=853948
ArticleTitle=HTML5 2D game development: Implementing Sprite behaviors
publish-date=01082013