HTML5 2D game development: Sprites

Implementing Snail Bait's cast of characters

In this series, HTML5 maven David Geary shows you how to implement an HTML5 2D video game one step at a time. Sprites — graphical objects that you can assign behaviors to — are one of the most fundamental and important aspects of video games. In this installment, you'll learn how to implement the sprites that make up Snail Bait's cast of characters.

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.



04 December 2012

Also available in Chinese Russian Japanese Vietnamese

Like other art forms — such as film, drama, and fiction — games have a cast of characters, each of which plays a particular role. For example, Snail Bait has the runner (the game's protagonist), coins, rubies, sapphires, bees, bats, buttons, and a snail, most of which are shown in Figure 1. In the first article in this series (see its Sprites: The cast of characters section), I discussed those characters and their roles in the game.

Figure 1. Snail Bait's characters
Screen shot of Snail Bait's sprites

Each character in Snail Bait is a sprite. Sprites are graphical objects that you can endow with behaviors; for example, the runner can run, jump, fall, and collide with other sprites in game, whereas rubies and sapphires sparkle, bob up and down, and disappear when they collide with the runner.

Coining the term sprite

One of the implementers of the Texas instruments 9918A video-display processor was the first to use the term sprite for animated characters. (In standard English, the word — derived from the Latin spiritus — means elf or fairy.) Sprites have been implemented in both software and hardware; the Commodore Amiga in 1985 supported up to eight hardware sprites.

Because sprites are one of the most fundamental aspects of any game, and because games typically have many sprites, it makes sense to encapsulate their basic capabilities in reusable objects. In this article you will learn how to:

  • Implement a Sprite object that you can reuse in any game
  • Decouple sprites from the objects that draw them (known as sprite artists) for flexibility at run time
  • Use sprite sheets to reduce startup time and memory requirements
  • Create sprites with metadata
  • Incorporate sprites into a game loop

See Download to get the full sample code for this article.

Sprite objects

I implemented Snail Bait's sprites as JavaScript objects that can be used in any game, so sprites reside in a file of their own. I include the file in Snail Bait's HTML, like this: <script src='js/sprites.js'></script>.

Table 1 lists Sprite attributes:

Table 1. Sprite attributes
AttributeDescription
artist Object that draws the sprite.
behaviors An array of behaviors, each of which manipulates its sprite in some fashion.
left The X coordinate of the sprite's upper left corner.
top The Y coordinate of the sprite's upper left corner.
width The sprite's width in pixels.
height The sprite's height in pixels.
opacity Whether a sprite is opaque, transparent, or somewhere in between.
type A string representing the sprite's type, such as bat, bee, or runner.
velocityX The sprite's horizontal velocity, specified in pixels/second.
velocityY The sprite's vertical velocity, specified in pixels/second.
visible The sprite's visibility. If the value is false, the sprite is not drawn.

Sprites are simple objects that maintain their location and size (known as the sprite's bounding box), velocity, and visibility. They also have a type, which can be used to distinguish one sprite from another, and an opacity, which means that sprites can be partially transparent.

Sprites delegate how they are drawn and how they behave to other objects, known as artists and behaviors, respectively.

Listing 1 shows the Sprite constructor, which sets a sprite's attributes to initial values:

Listing 1. Sprite constructor
var Sprite = function (type, artist, behaviors) { // constructor
   this.type = type || '';
   this.artist = artist || undefined;
   this.behaviors = behaviors || [];

   this.left = 0;
   this.top = 0;
   this.width = 10;   // Something other than zero, which makes no sense
   this.height = 10;  // Something other than zero, which makes no sense
   this.velocityX = 0;
   this.velocityY = 0;
   this.opacity = 1.0;
   this.visible = true;

   return this;
};

Presentation vs. behaviors

The signatures of the Sprite methods enforce a separation of concerns between presentation and behavior: draw() uses the Canvas context to draw a sprite, whereas update() is designed to update only a sprite's state based on the current time and animation frame rate. Behaviors should not draw and artists should not manipulate sprite state.

All the constructor's arguments in Listing 1 are optional. If you don't specify behaviors, the constructor creates an empty array, and if you create a sprite without specifying a type, its type is an empty string. If you don't specify an artist, it is simply undefined.

Besides attributes, sprites have two methods, listed in Table 2:

Table 2. Sprite methods
MethodDescription
draw(context)Calls the draw() method of the sprite's artist if the sprite is visible and has an artist.
update(time, fps) Invokes the update() method for each of the sprite's behaviors.

The implementations of the methods listed in Table 2 are shown in Listing 2:

Listing 2. Sprite method implementations
Sprite.prototype = { // methods
   draw: function (context) {
     context.save();

     // Calls to save() and restore() make the globalAlpha setting temporary

     context.globalAlpha = this.opacity; 
      
     if (this.artist && this.visible) {
        this.artist.draw(this, context);
     }

     context.restore();
   },

   update: function (time, fps) {
      for (var i=0; i < this.behaviors.length; ++i) {
         if (this.behaviors[i] === undefined) { // Modified while looping?
            return;
         }

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

Sprite velocities: Specified in pixels/second

As you saw in the second article in this series (see its Time-based motion section), sprite movement must be independent of the underlying frame rate of the game's animation. Because of that requirement, sprite velocities are specified in pixels/second.

As you can see from Listing 1 and Listing 2, sprites are not complicated. Much of the complexity surrounding sprites is encapsulated in a sprite's artist and behaviors. It's also important to understand that you can change a sprite's artist and behaviors at run time, because sprites are decoupled from those objects. In fact, as you will see in the next article in this series, it's possible — and often highly desirable — to implement general behaviors that can be used with multiple sprites.

Now that you've seen how sprites are implemented, you're ready to take a look at implementing sprite artists.


Sprite artists and sprite sheets

Sprite artists can be implemented in one of three ways:

  • Stroke and fill artist: Draws graphics primitives, such as lines, arcs, and curves
  • Image artist: Draws an image with the 2D context's drawImage() method
  • Sprite sheet artist: Draws an image from a sprite sheet (also with drawImage())

Regardless of an artist's type, all sprite artists, as you can see from Listing 2, must fulfill only one requirement: They must be objects that implement a draw() method that takes a sprite and a Canvas 2D context as arguments.

Next I discuss each type of artist, with an interlude to examine sprite sheets.

Stroke and fill artists

Stroke and fill artists do not have a canonical implementation; instead, you implement them in ad hoc fashion using the graphics capabilities of the Canvas 2D context. Listing 3 shows the implementation of the stroke and fill artist that draws Snail Bait's platform sprites:

Listing 3. Stroke and fill artists
// Stroke and fill artists draw with Canvas 2D drawing primitives

var SnailBait =  function (canvasId) { // constructor
   ...

   this.platformArtist = {
      draw: function (sprite, context) {
         var top;
         
         context.save();

         top = snailBait.calculatePlatformTop(sprite.track);

         // Calls to save() and restore() make the following settings temporary

         context.lineWidth = snailBait.PLATFORM_STROKE_WIDTH;
         context.strokeStyle = snailBait.PLATFORM_STROKE_STYLE;
         context.fillStyle = sprite.fillStyle;

         context.strokeRect(sprite.left, top, sprite.width, sprite.height);
         context.fillRect  (sprite.left, top, sprite.width, sprite.height);

         context.restore();
      }
   },
};

Platforms, as you can see from Figure 1, are simply rectangles. The platform artist listed in Listing 3 draws those rectangles with the Canvas 2D context strokeRect() and fillRect() methods. The second article in this series (see its HTML5 Canvas overview section) has more information about those methods. The location and size of the ensuing rectangle is determined by the platform sprite's bounding box.

Image artists

Unlike stroke and fill artists, image artists have a canonical implementation, shown in Listing 4:

Listing 4. Image artist
// ImageArtists draw an image

var ImageArtist = function (imageUrl) { // constructor
   this.image = new Image();
   this.image.src = imageUrl;
};

ImageArtist.prototype = { // methods
   draw: function (sprite, context) {
      context.drawImage(this.image, sprite.left, sprite.top);
   }
};

You construct an image artist with an image URL, and the artist's draw() method draws the entire image at its sprite's location.

Snail Bait does not use image artists, because it's more efficient to draw images from a sprite sheet.

Sprite sheets

One of the most effective ways to ensure that your website loads quickly is to reduce the number of HTTP requests you make to a bare minimum. Most games use lots of images, and your start-up time will suffer if you make separate HTTP requests for each of them. For that reason, HTML5 game developers create a single large image that contains all their game's images. That single image is known as a sprite sheet. Figure 2 shows Snail Bait's sprite sheet:

Figure 2. The Snail Bait sprite sheet
Screen shot of the Snail Bait sprite sheet

Given a sprite sheet, you need a way to draw a specific rectangle from that sprite sheet onto a canvas. Fortunately, the Canvas 2D context lets you easily do that with the drawImage() method. That technique is used by sprite sheet artists.

Sprite sheet artists

The implementation of sprite sheet artists is shown in Listing 5:

Listing 5. Sprite sheet artist
// Sprite sheet artists draw an image from a sprite sheet

SpriteSheetArtist = function (spritesheet, cells) { // constructor
   this.cells = cells;
   this.spritesheet = spritesheet;
   this.cellIndex = 0;
};

SpriteSheetArtist.prototype = { // methods
   advance: function () {
      if (this.cellIndex == this.cells.length-1) {
         this.cellIndex = 0;
      }
      else {
         this.cellIndex++;
      }
   },
   
   draw: function (sprite, context) {
      var cell = this.cells[this.cellIndex];

      context.drawImage(this.spritesheet,
               cell.left,   cell.top,     // source x, source y
               cell.width,  cell.height,  // source width, source height
               sprite.left, sprite.top,   // destination x, destination y
               cell.width,  cell.height); // destination width, destination height
   }
};

You instantiate sprite sheet artists with a reference to a sprite sheet and an array of bounding boxes, called cells. Those cells represent rectangular areas within the sprite sheet, each of which encompasses a single sprite image.

Sprite sheet artists also maintain an index into their cells. The sprite sheet's draw() method uses that index to access the current cell and then uses the nine-argument version of the Canvas 2D context's drawImage() to draw the contents of that cell into a canvas at the sprite's location.

The sprite sheet artist's advance() method advances the cell index to the next cell, wrapping around to the beginning when the index points to the last cell. A subsequent call to the sprite sheet artist's draw() method draws the corresponding image. By repeatedly advancing the index and drawing, sprite sheet artists can draw a set of images sequentially from a sprite sheet.

Sprite sheet artists, as you can see from Listing 5, are easy to implement. They are also easy to use; you just instantiate the artist with a sprite sheet and cells, and subsequently invoke the advance() and draw() methods as desired. The tricky part is defining the cells.

Defining sprite sheet cells

Listing 6 shows cell definitions within Snail Bait's sprite sheet for the game's bats, bees, and snail:

Listing 6. Snail Bait sprite sheet cell definitions
var BAT_CELLS_HEIGHT = 34,

    BEE_CELLS_WIDTH  = 50,
    BEE_CELLS_HEIGHT = 50,


    ...

    SNAIL_CELLS_WIDTH = 64,
    SNAIL_CELLS_HEIGHT = 34,

    ...

    // Spritesheet cells................................................

    batCells = [
       { left: 1,   top: 0, width: 32, height: BAT_CELLS_HEIGHT },
       { left: 38,  top: 0, width: 46, height: BAT_CELLS_HEIGHT },
       { left: 90,  top: 0, width: 32, height: BAT_CELLS_HEIGHT },
       { left: 129, top: 0, width: 46, height: BAT_CELLS_HEIGHT },
    ],

    beeCells = [
       { left: 5,   top: 234, width: BEE_CELLS_WIDTH, height: BEE_CELLS_HEIGHT },
       { left: 75,  top: 234, width: BEE_CELLS_WIDTH, height: BEE_CELLS_HEIGHT },
       { left: 145, top: 234, width: BEE_CELLS_WIDTH, height: BEE_CELLS_HEIGHT }
    ],
    ...

    snailCells = [
       { left: 142, top: 466, width: SNAIL_CELLS_WIDTH, height: SNAIL_CELLS_HEIGHT },
       { left: 75,  top: 466, width: SNAIL_CELLS_WIDTH, height: SNAIL_CELLS_HEIGHT },
       { left: 2,   top: 466, width: SNAIL_CELLS_WIDTH, height: SNAIL_CELLS_HEIGHT },
    ];

Determining cell bounding boxes is a tedious task, so it's worth the time investment to implement a tool that can do it for you. Figure 3 shows just such a tool, available to run online at the Core HTML Canvas site (see Resources):

Figure 3. A simple sprite sheet inspector
Screen shot of a simple spritesheet inspector

The game developer's toolchest

A game developer's work is not all fun and games. Game developers spend a lot of time on tedious tasks such as determining sprite sheet cells and designing game levels. Most game developers, therefore, spend a fair amount of time implementing tools, such as the one shown in Figure 3, to assist them with those tedious tasks.

The application shown in Figure 3 displays an image and tracks mouse movement within that image. As you move the mouse, the application draws guide lines and updates a readout in the upper-left corner of the application that displays the current location of the mouse cursor. The tool makes it easy to determine bounding boxes for each image and sprite sheet.

Now that you have a good idea how to implement sprites and their artists, it's time to take a look at how Snail Bait creates and initializes its sprites.


Creating and initializing Snail Bait's sprites

Snail Bait defines arrays that ultimately contain sprites, as shown in Listing 7:

Listing 7. Defining sprite arrays in the game constructor
var SnailBait = function (canvasId) { // constructor
   ...

   this.bats         = [],
   this.bees         = [], 
   this.buttons      = [],
   this.coins        = [],
   this.platforms    = [],
   this.rubies       = [],
   this.sapphires    = [],
   this.snails       = [],

   this.runner = new Sprite('runner', this.runnerArtist);

   this.sprites = [ this.runner ]; // Add other sprites later
   ...
};

Each array in Listing 7 contains sprites of the same type; for example, the bats array contains bat sprites, the bees array contains bee sprites, and so on. The game also maintains an array containing all the game's sprites. The individual arrays for bees, bats, and so on are not strictly necessary — in fact, they are redundant — but they facilitate performance; for example, when the game checks to see if the runner has landed on a platform, it's more efficient to iterate over the platforms array than to iterate over the sprites array searching for platforms.

Listing 7 also shows how the game creates the runner sprite and how it adds that sprite to the sprites array. There's no array for runners, because the game has only one runner. Notice that the game instantiates the runner with a type —runner— and an artist, but it does not specify any behaviors when the runner is instantiated. Those behaviors, which I discuss in the next article in this series, are added later on in the code.

When the game starts, Snail Bait (along with doing other things) invokes a createSprites() method, as you can see in Listing 8:

Listing 8. Starting the game
SnailBait.prototype = { // methods
   ...
   start: function () {
      this.createSprites();
      this.initializeImages();
      this.equipRunner();
      this.splashToast('Good Luck!');
   },
};

The createSprites() method, which creates all the game's sprites with the exception of the runner, is shown in Listing 9:

Listing 9. Creating and initializing Snail Bait sprites
SnailBait.prototype = { // methods
   ...
   createSprites: function() {  
      this.createPlatformSprites();
      
      this.createBatSprites();
      this.createBeeSprites();
      this.createButtonSprites();
      this.createCoinSprites();
      this.createRubySprites();
      this.createSapphireSprites();
      this.createSnailSprites();

      this.initializeSprites();

      this.addSpritesToSpriteArray();
   },

createSprites() invokes helper functions to create the different types of sprites, followed by methods that initialize the sprites and add them to the sprites array. The implementations of those helper functions are shown in Listing 10:

Listing 10. Creating individual sprites
SnailBait.prototype = { // methods
  ...
  createBatSprites: function () {
    var bat,  batArtist = new SpriteSheetArtist(this.spritesheet, this.batCells),
   redEyeBatArtist = new SpriteSheetArtist(this.spritesheet, this.batRedEyeCells);

    for (var i = 0; i < this.batData.length; ++i) {
      if (i % 2 === 0) bat = new Sprite('bat', batArtist);
      else             bat = new Sprite('bat', redEyeBatArtist);

      bat.width  = this.BAT_CELLS_WIDTH;
      bat.height = this.BAT_CELLS_HEIGHT;

      this.bats.push(bat);
    }
  },

  createBeeSprites: function () {
    var bee, beeArtist = new SpriteSheetArtist(this.spritesheet, this.beeCells);

    for (var i = 0; i < this.beeData.length; ++i) {
      bee = new Sprite('bee', beeArtist);
      bee.width  = this.BEE_CELLS_WIDTH;
      bee.height = this.BEE_CELLS_HEIGHT;

      this.bees.push(bee);
    }
  },

  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);
      }
      else {
         button = new Sprite('button', buttonArtist);
      }

      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);
    }
  },

  createCoinSprites: function () {
    var coin, coinArtist = new SpriteSheetArtist(this.spritesheet, this.coinCells);

    for (var i = 0; i < this.coinData.length; ++i) {
      coin        = new Sprite('coin', coinArtist);
      coin.width  = this.COIN_CELLS_WIDTH;
      coin.height = this.COIN_CELLS_HEIGHT;

      this.coins.push(coin);
    }
  },

  createPlatformSprites: function () {
    var sprite, pd;  // Sprite, Platform data

    for (var i=0; i < this.platformData.length; ++i) {
      pd = this.platformData[i];
      sprite           = new Sprite('platform-' + i, this.platformArtist);
      sprite.left      = pd.left;
      sprite.width     = pd.width;
      sprite.height    = pd.height;
      sprite.fillStyle = pd.fillStyle;
      sprite.opacity   = pd.opacity;
      sprite.track     = pd.track;
      sprite.button    = pd.button;
      sprite.pulsate   = pd.pulsate;
      sprite.power     = pd.power;
      sprite.top       = this.calculatePlatformTop(pd.track);

      this.platforms.push(sprite);
    }
  },

  createSapphireSprites: function () {
    // Listing omitted for brevity. Discussed in the next article in this series.
  },

  createRubySprites: function () {
    // Listing omitted for brevity. Discussed in the next article in this series.
  },

  createSnailSprites: function () {
    // Listing omitted for brevity. Discussed in the next article in this series.
  },
};

The methods shown in Listing 10 are noteworthy for three reasons. First, the methods are all pretty simple: Each method creates sprites, sets their width and height, and adds them to the individual sprite arrays. Second, createBatSprites() and createButtonSprites() use more than one artist to create sprites of the same type. The createBatSprites() method alternates artists so that half the bats have red eyes and the other half have white eyes, as you can see from Figure 4. The createButtonSprites() method employs artists that draw either blue or gold buttons.

Figure 4. Red- and white-eyed bats
Screen shot of Snail Bait running in Chrome

The third and most interesting aspect of the methods in Listing 10 is that they all create sprites from arrays of sprite metadata.


Creating sprites with metadata

Listing 11 shows some of Snail Bait's sprite metadata:

Listing 11. Sprite metadata
var SnailBait = function (canvasId) {
  // Bats..............................................................
   
   this.batData = [
      { left: 1150, top: this.TRACK_2_BASELINE - this.BAT_CELLS_HEIGHT },
      { left: 1720, top: this.TRACK_2_BASELINE - 2*this.BAT_CELLS_HEIGHT },
      { left: 2000, top: this.TRACK_3_BASELINE }, 
      { left: 2200, top: this.TRACK_3_BASELINE - this.BAT_CELLS_HEIGHT },
      { left: 2400, top: this.TRACK_3_BASELINE - 2*this.BAT_CELLS_HEIGHT },
   ],
   
   // Bees..............................................................

   this.beeData = [
      { left: 500,  top: 64 },
      { left: 944,  top: this.TRACK_2_BASELINE - this.BEE_CELLS_HEIGHT - 30 },
      { left: 1600, top: 125 },
      { left: 2225, top: 125 },
      { left: 2295, top: 275 },
      { left: 2450, top: 275 },
   ],

   // Buttons...........................................................

   this.buttonData = [
      { platformIndex: 7 },
      { platformIndex: 12 },
   ],

   // Metadata for Snail Bait's other sprites is omitted for brevity
};

Creating sprites from metadata is a good idea because:

  • Sprite metadata is located in one place, instead of being spread throughout the code.
  • Methods that create sprites are simpler when they are decoupled from the metadata.
  • Metadata can come from anywhere.

Because the sprite metadata is located in one place in the code, it's easy to find and modify. Also, because the metadata is defined outside of the methods that create the sprites, those methods are simpler, and therefore easier to understand and modify. Finally, although the metadata for Snail Bait is embedded directly in the code, sprite metadata can come from anywhere — including, for example, a level editor that might create metadata at run time. So in a nutshell, metadata is easier to modify, and it's more flexible than specifying sprite data directly within methods that create sprites.

Recall from Listing 9 that Snail Bait's createSprites() method invokes two methods —initializeSprites() and addSpritesToSpriteArray()— after creating the game's sprites. Listing 12 shows the initializeSprites() method:

Listing 12. Initializing Snail Bait sprites
SnailBait.prototype = { // methods
   ...
  
   initializeSprites: function() {  
      this.positionSprites(this.bats,       this.batData);
      this.positionSprites(this.bees,       this.beeData);
      this.positionSprites(this.buttons,    this.buttonData);
      this.positionSprites(this.coins,      this.coinData);
      this.positionSprites(this.rubies,     this.rubyData);
      this.positionSprites(this.sapphires,  this.sapphireData);
      this.positionSprites(this.snails,     this.snailData);
   },

   positionSprites: function (sprites, spriteData) {
      var sprite;

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

         if (spriteData[i].platformIndex) { // put sprite on a platform
            this.putSpriteOnPlatform(sprite, this.platforms[spriteData[i].platformIndex]);
         }
         else {
            sprite.top  = spriteData[i].top;
            sprite.left = spriteData[i].left;
         }
      }
   },
};

initializeSprites() invokes positionSprites() for each of the game's sprite arrays. That method, in turn, positions sprites at locations specified by the sprite's metadata. Notice that some sprites, such as buttons and snails, reside on top of platforms. The putSpriteOnPlatform() method is shown in Listing 13:

Listing 13. Putting sprites on platforms
SnailBait.prototype = { // methods
   ...

   putSpriteOnPlatform: function(sprite, platformSprite) {
      sprite.top  = platformSprite.top - sprite.height;
      sprite.left = platformSprite.left;
      sprite.platform = platformSprite;
   },
}

Given a sprite and a platform, the putSpriteOnPlatform() method positions the sprite on top of the platform and stores a reference to the platform in the sprite for further reference.

As you might suspect, and as Listing 14 verifies, adding individual sprites to the all encompassing sprites array is a simple matter:

Listing 14. Creating and initializing Snail Bait sprites
SnailBait.prototype = { // methods
   ...
  
   addSpritesToSpriteArray: function () {
      var i;
  
      for (i=0; i < this.bats.length; ++i) {
         this.sprites.push(this.bats[i]);
      }

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

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

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

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

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

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

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

Now that you've seen how to implement sprites and sprite artists in addition to how Snail Bait creates and initializes its sprites, I'll show you how sprites are incorporated into Snail Bait's game loop.


Incorporating sprites into the game loop

Recall from the second article in this series (see its Scrolling the background section) that nearly all horizontal motion in Snail Bait is the result of translating the Canvas 2D context. Snail Bait always draws the vast majority of its sprites at the same horizontal location, and their apparent horizontal motion is purely a result of that translation. Most of Snail Bait's sprites move horizontally in concert with the game's platforms, as shown in Listing 15:

Listing 15. Updating sprite offsets
SnailBait.prototype = {
   draw: function (now) {
      this.setPlatformVelocity();
      this.setTranslationOffsets();

      this.drawBackground();

      this.updateSprites(now);
      this.drawSprites();
   },

   setPlatformVelocity: function () {
      // Setting platform velocity was discussed in the second article in this series

      this.platformVelocity = this.bgVelocity * this.PLATFORM_VELOCITY_MULTIPLIER; 
   },

   setTranslationOffsets: function () {
      // Setting the background translation offset was discussed
      // in the second article in this series

      this.setBackgroundTranslationOffset();
      this.setSpriteTranslationOffsets();
   },
   
   setSpriteTranslationOffsets: function () {
      var i, sprite;

      this.spriteOffset += this.platformVelocity / this.fps; // In step with platforms
   
      for (i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];
      
         if ('runner' !== sprite.type) {
            sprite.offset = this.platformOffset; // In step with platforms
         }
      }
   },
   ...
};

The draw() method sets the platform velocity and subsequently sets translation offsets for all sprites except the runner. (The runner's horizontal location is fixed; it does not move in concert with the platforms.)

After setting translation offsets and drawing the background, the draw() method updates and draws the game's sprites with updateSprites() and drawSprites(). Those methods are shown in Listing 16:

Listing 16. Updating and drawing sprites
SnailBait.prototype = {
   ...
   updateSprites: function (now) {
      var sprite;
   
      for (var i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];

         if (sprite.visible && this.spriteInView(sprite)) {
            sprite.update(now, this.fps);
         }
      }
   },
   
   drawSprites: function() {
      var sprite;
   
      for (var i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];
   
         if (sprite.visible && this.spriteInView(sprite)) {
            this.context.translate(-sprite.offset, 0);

            sprite.draw(this.context);

            this.context.translate(sprite.offset, 0);
         }
      }
   },
   
   spriteInView: function(sprite) {
      return sprite === this.runner || // runner is always visible
         (sprite.left + sprite.width > this.platformOffset &&
          sprite.left < this.platformOffset + this.canvas.width);   
   },

When sprites are not in view

Snail Bait's final version has a playing field that is four times as wide as the game's canvas. (The width is arbitrary; it could be much wider.) At any given time, therefore, three-quarters of Snail Bait's landscape isn't in view. There is no need to update or draw sprites that lie within that three-quarters of the landscape, so Snail Bait does not do so. Strictly speaking, it's not necessary to exclude those sprites when drawing, because the Canvas context would exclude them anyway.

Both updateSprites() and drawSprites() iterate over all the game's sprites and, respectively, update and draw the sprites — but only if the sprites are visible and in the section of the canvas that is currently displayed.

Before drawing the sprites, the drawSprites() method translates the context by the sprite offset calculated in setTranslationOffsets() and afterwards translates the context back to its original position, thereby giving the sprites their apparent horizontal motion.


Next time

In this article, I've shown you how to implement sprites and sprite artists and how to incorporate sprites into a game loop. In the next article in the HTML5 2D game development series, you'll learn how to implement sprite behaviors and attach them to specific sprites. See you next time.


Download

DescriptionNameSize
Sample codej-html5-game4.zip3.9MB

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=848419
ArticleTitle=HTML5 2D game development: Sprites
publish-date=12042012