Docstoc

HTML5 2D game development Sprites

Document Sample
HTML5 2D game development  Sprites Powered By Docstoc
					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 charactersEach 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 gameDecouple sprites
from the objects that draw them (known as sprite artists) for flexibility
at run timeUse sprite sheets to reduce startup time and memory
requirementsCreate sprites with metadataIncorporate sprites into a game
loopSee 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 attributesAttributeDescriptionartist
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 constructorvar 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 methodsMethodDescriptiondraw(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 implementationsSprite.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.
Back to top
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 curvesImage artist: Draws an image with the 2D context's drawImage()
methodSprite 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 sheetGiven 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 definitionsvar 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 inspectorThe 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.
Back to top
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 constructorvar 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 gameSnailBait.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
spritesSnailBait.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 spritesSnailBait.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 batsThe third and most interesting aspect
of the methods in Listing 10 is that they all create sprites from arrays
of sprite metadata.
Back to top
Creating sprites with metadata
Listing 11 shows some of Snail Bait's sprite metadata:
Listing 11. Sprite metadatavar 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 spritesSnailBait.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 platformsSnailBait.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
spritesSnailBait.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.
Back to top
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 offsetsSnailBait.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 spritesSnailBait.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.
Back to top
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.
Back to top
Download
DescriptionNameSizeDownload methodSample code
j-html5-game4.zip
3.9MB
HTTP
Information about download methods

				
DOCUMENT INFO
Shared By:
Categories:
Tags:
Stats:
views:1
posted:1/31/2013
language:English
pages:16
About