HTML5 2D 游戏开发: Sprites

实现 Snail Bait 人物角色

在本系列中,HTML5 专家 David Geary 将向您展示如何逐步实现一个 HTML5 2D 视频游戏。Sprites(图形化对象,您可以为其分配行为)是视频游戏最基本最重要的方面之一。在这一期的文章中,您将学习如何实现构成 Snail Bait 人物角色的 sprite。

David Geary, 作者和演讲家, Clarity Training, Inc.

David GearyDavid Geary 是 Core HTML5 Canvas 的作者,也是 HTML5 Denver User's Group 的合著者和其他 8 本 Java 图书的作者,其中包括畅销书 Swing 和 JavaServer Faces。David 经常在各大会议发表演讲,包括 JavaOne、Devoxx、Strange Loop、NDC 和 OSCON,他曾三次当选为 JavaOne 之星。他还为 developerWorks 撰写了 JSF 2 fuGWT fu 系列文章。您可以通过 @davidgeary 在 Twitter 上关注他。



2013 年 1 月 01 日

和其他艺术形式(比如电影、戏剧和小说)一样,游戏也有一系列的人物,每个人物都扮演着特定的角色。例如,Snail Bait 有跑步小人(游戏主角)、硬币、红宝石、蓝宝石、蜜蜂、蝙蝠、纽扣和一个蜗牛,图 1 中展示了其中的大多数角色。在本系列的第 1 篇文章中(参阅 sprite:演员表 小节),已经介绍了这些人物以及它们在游戏中的角色。

图 1. Snail Bait 的人物
Snail Bait 的 sprites 的屏幕截图

Snail Bait 中每个人物都是一个 sprite。Sprite 是可以赋予行为 的图形化对象,例如跑步小人可以奔跑、跳跃、坠落并与游戏中的其他 sprite 相撞,而红宝石和蓝宝石可以闪耀光芒、上下跳动,与跑步小人相撞后消失。

发明术语 sprite

美国德州仪器公司视频显示处理器的一个实现者是第一个使用术语 sprite 作为动画角色的人。(在标准英语中,这个单词源自于拉丁语 spiritus,意思是小精灵。)Sprite 已经在软件和硬件中得以实现;Commodore Amiga 在 1985 年就已支持高达 8 个硬件 sprite。

对于任何游戏来说,sprite 都是最基本的部分,因为游戏通常有很多 sprite,因此将其基本功能封装到可重用对象中是非常有意义的。在本文中,您将学习如何执行以下操作:

  • 实现一个在任何游戏中均可重用的 Sprite 对象
  • 从绘制 sprite 的对象(称之为 sprite artists)中解耦 sprite,以便在运行时灵活应用。
  • 使用 sprite 表单 减少启动时间和内存需求
  • 使用元数据创建 sprite
  • 将 sprite 整合成为一个游戏循环

参阅 下载 部分,获取本文的完整样例代码。

Sprite 对象

我将 Snail Bait 的 sprite 实现为可在任何游戏中使用的 JavaScript 对象,因此,sprite 保存在游戏本身的文件中。我只是在 Snail Bait 的 HTML 中包含了该文件,如下所示:<script src='js/sprites.js'></script>

表 1 列出了 Sprite 属性:

表 1. Sprite 属性
属性描述
artist绘制 sprite 的对象。
behaviors一个行为数组,每一个都以某种方式操纵其 sprite。
left 左上角 sprite 的 X 坐标。
top 左上角 sprite 的 Y 坐标
width sprite 的宽度,用像素表示。
height sprite 的高度,用像素表示。
opacity sprite 是不透明的还是透明的,或者介于两者之间。
type一个代表 sprite 类型的字符串,比如 batbeerunner
velocityX sprite 的水平速度,单位为像素/秒。
velocityY sprite 的垂直速度,单位为像素/秒。
visible sprite 的可见度。如果值为 false,则不需要绘制 sprite。

Sprite 是比较简单的对象,一些属性定义了其位置和大小(即 sprite 的边界框)、速度和可见度。还有一个属性,可用于将一个 sprite 与另一个 sprite 以及不透明 sprite 区分开,这意味着 sprite 可以是半透明的。

Sprite 将它们的绘制方式和行为方式分配给了其他属性,分别称为 artists 和 behaviors

清单 1 展示了 Sprite 构造函数,该函数将 sprite 的属性设置为初始值:

清单 1. Sprite 构造函数
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;
};

表现与行为

Sprite 方法的签名对表现和行为之间的关注点进行了分离:draw() 使用 Canvas 上下文绘制一个 sprite,而 update() 被设计为只根据当前时间以及动画帧速率更新 sprite 状态。行为不应该绘制 sprite 状态,而 artist 不应该操控 sprite 状态。

清单 1 中的所有构造函数参数都是可选的。如果没有指定行为,构造函数将会创建一个空数组,如果您创建一个 sprite 而没有指定类型,那么该类型将是一个空字符串。如果没有指定 artist,那么它的值将是 undefined。

除了这些属性之外,sprite 还有两个方法,表 2 中列出了这两个方法:

表 2. Sprite 方法
方法描述
draw(context)如果 sprite 是可见的并且有一个 artist 对象,则调用 sprite artist 的 draw() 方法
update(time, fps) 对于每个 sprite 行为,应该调用 update() 方法。

表 2 中列出的方法的实现如清单 2 所示:

清单 2. Sprite 方法的实现
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 方法( draw()update()),这些方法是对 Canvas 2D 上下文的引用,这些方法会分别传递给 sprite 的 artist 和 behaviors 对象。

Sprite 速度:以像素/秒为单位

正如您在本系列第 2 篇文章(参阅 基于时间的运动 小节)所看到的,sprite 运动必须独立于游戏动画的底层帧速率。请根据该需求指定 sprite 速度,以像素/秒为单位。

正如您在 清单 1清单 2 中所看到的,sprite 并不复杂,大多数复杂的 sprite 都封装在 sprite 的 artist 和 behaviors 对象中。了解在运行时 可以更改 sprite 的 artist 和 behavior 很重要,因为这样您可以从这些对象中解耦 sprite。您可在本系列的下一篇文章中看到,实现多个 sprite 使用的一般行为是可行的,而且是十分可取的。

您已经了解了 sprite 的实现方法了,现在可以了解一下如何实现 sprite artists 了。


Sprite artists 和 sprite 表单

Sprite artists 可使用以下 3 种方法之一实现:

  • 描边和填充 artist:绘制基本图形,比如线条、弧形和曲线
  • 图像 artist:使用 2D 上下文的 drawImage() 方法绘制一个图像
  • Sprite 表单 artist:从 sprite 表单绘制一个图像(也可使用 drawImage()

不管 artist 是什么类型,清单 2 所有 sprite artist 都必须满足以下条件:该对象必须执行一个 draw() 方法,此方法接受一个 sprite 和一个 Canvas 2D 上下文作为参数。

接下来,我们将讨论每个 artist 类型,先检查一下 sprite 表单。

描边和填充 artist

描边和填充 artist 没有标准实现,您需要使用 Canvas 2D 上下文的图形化功能,以实现它们。清单 3 显示了描边和填充 artist 的实现,绘制 Snail Bait 平台 sprite:

清单 3. 描边和填充 artist
// 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();
      }
   },
};

正如您从 图 1 中所看到的,平台只不过是一些矩形,清单 3 中列出的平台 artist 使用 Canvas 2D 上下文的 strokeRect()fillRect() 方法来绘制这些矩形。本系列第 2 篇文章(参阅这篇文章的 HTML5 Canvas 概述 小节)提供了关于这些方法的更多信息。矩形的位置和大小是由平台 sprite 边界框决定的。

图像 artist

与描边和填充 artist 不一样,图像 artist 有一个标准实现,如清单 4 所示:

清单 4. 图像 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);
   }
};

您可以使用一个图像 URL 构造一个图像 artist,该 artist 的 draw() 方法在其 sprite 位置绘制整个图像。

Snail Bait 没有使用图像 artist 对象,因此从 sprite 表单绘制图像效率更高一些。

Sprite 表单

确保网站快速加载的一个最有效的方法是将您发出的 HTTP 请求数量减至最小。大多数游戏都使用了大量图像,如果对每个图像都发出一个 HTTP 请求,则会影响您的启动时间。鉴于这个原因,HTML5 游戏开发人员创建了一个包含所有游戏图像的大型图像,该图像称为 sprite 表单。图 2 显示了 Snail Bait 的 sprite 表单:

图 2. Snail Bait 的 sprite 表单
Snail Bait 的 sprite 表单的屏幕截图

如果给定一个 sprite 表单,那么您需要使用一个方法将该 sprite 表单中的特定矩形绘制到一个画布上。幸运的是,Canvas 2D 上下文的 drawImage() 方法使您能够轻松地实现此操作。该技术被 sprite 表单 artists 对象所用。

Sprite 表单 artist

sprite 表单 artists 对象的实现如清单 5 所示:

清单 5. Sprite 表单 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
   }
};

您可引用一个 sprite 表单和一个边界框数组(称为单元格)实例化 sprite 表单。这些单元格表示 sprite 表单的矩形区域,每个表单封装一个 sprite 图像。

sprite 表单 artist 也包含一个指向其单元格的指针。sprite 表单的 draw() 方法使用该指针访问当前单元格,然后使用 Canvas 2D 上下文的 drawImage() 方法的 9 个参数的版本将该单元格的内容绘制到 sprite 所在位置的画布上。

sprite 表单 artist 的 advance() 方法使单元格指针推进到下一个单元格,当指针指向最后一个单元格时返回第一个单元格。sprite 表单 artist 的 draw() 方法的后续调用绘制相应的图像。通过反复向前推进指针并绘制图像,sprite 表单 artist 可从 sprite 表单连续绘制一组图像。

正如您从 清单 5 所看到的,sprite 表单 artist 很容易实现。也容易使用它们,只需使用一个 sprite 表单和单元格实例化 artist 对象,然后根据需要调用 advance()draw() 方法即可。麻烦的是如何定义单元格。

定义 sprite 表单单元格

清单 6 显示了 Snail Bait 游戏中蝙蝠、蜜蜂和蜗牛的 sprite 表单的单元格定义:

清单 6. Snail Bait sprite 表单的单元格定义
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 },
    ];

定义单元格边界框是一个很繁琐的任务,需要投入一些时间来设计一个可帮您实现这些任务的工具。图 3 展示了这样一个工具,该工具可在 Core HTML Canvas 网站在线运行(参阅 参考资料):

图 3. 一个简单的 sprite 表单检查器
简单的 sprite 表单检查器的屏幕截图

游戏开发人员的工具箱

游戏开发人员的生活并不都是快乐而轻松的。他们需要在繁琐的工作中花费大量的时间,比如确定 sprite 表单单元格以及设计游戏级别。因此,大多数游戏开发人员需要花费相当多的时间来实现相关的工具,比如 图 3 中的表单检查器,帮助他们完成这些繁琐的任务。

图 3 所示的应用程序展示一个图像并跟踪该图像中的鼠标移动。当您移动鼠标时,应用程序将绘制引导线,并更新应用程序左上角显示鼠标所在位置的读数。该工具使得确定每个图像和 sprite 表单的边界框变得非常容易。

现在关于如何实现 sprite 及其 artist 已经有一个不错的方式,可以看看 Snail Bait 如何创建和初始化其 sprite。


创建和初始化 Snail Bait sprite

Snail Bait 定义了最终包含 sprite 的数组,如清单 7 所示:

清单 7. 在游戏构造函数中定义 sprite 数组
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
   ...
};

清单 7 中的每个数组都包含相同类型的 sprite,bats 数组包含蝙蝠 sprite,bees 数组包含蜜蜂 sprite 等。该游戏也有一个包含所有游戏 sprite 的数组 。蜜蜂、蝙蝠等单个数组并不是必要的,事实上它们是多余的,但是它们可以提升性能,例如,游戏检查跑步小人是否位于一个平台上时,在 platforms 数组上进行迭代,比在 sprites 数组上进行迭代更为有效。

清单 7 还展示了该游戏如何创建跑步小人 sprite,以及如何将该 sprite 添加到 sprites 数组。因为这个游戏只有一个跑步小人,所以没有跑步小人数组。请注意,该游戏使用一个类型 runner 和一个 artist 来实例化跑步小人,但实例化时没有指定任何行为。这些行为(将在本系列下期文章中讨论)稍后将会添加到 代码中。

游戏开始时,Snail Bait(以及其他操作)会调用一个 createSprites() 方法,正如您在清单 8 中所看到的那样。

清单 8. 开始游戏
SnailBait.prototype = { // methods
   ...
   start: function () {
      this.createSprites();
      this.initializeImages();
      this.equipRunner();
      this.splashToast('Good Luck!');
   },
};

createSprites() 方法,创建除跑步小人之外的所有游戏 sprite,如清单 9 所示:

清单 9. 创建和初始化 Snail Bait sprite
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() 调用可以帮助函数创建其他类型的 sprite,然后调用初始化 sprite 的方法,并将它们添加到 sprites 数组,这些函数的实现如清单 10 所示:

清单 10. 创建和初始化单个 sprite
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.
  },
};

清单 10 中展示的这些方法值得关注,原因有 3 个。第一,这些方法都非常简单:每个方法都创建了 sprite,设置其宽度和高度,并将它们添加到单个 sprite 数组中。第二,createBatSprites()createButtonSprites() 使用多个 artist 创建类型相同的 sprite,createBatSprites() 方法可替代 artist,使一半蝙蝠的眼睛呈红色,另一半蝙蝠的眼睛呈白色,如图 4 所示。createButtonSprites() 方法使用 artist 方法绘制蓝色或金色纽扣。

图 4. 红眼蝙蝠和白眼蝙蝠
Chrome 中运行的 Snail Bait 的屏幕截图

清单 10 中的这些方法的值得关注的第三个原因(也是最重要原因)是,它们都使用 sprite 元数据创建了 sprite。


使用元数据创建 sprite

清单 11 显示了一些 Snail Bait 的 sprite 元数据:

清单 11. Sprite 元数据
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
};

从元数据创建 sprite 是一个不错的主意,因为:

  • Sprite 元数据位于某个地方,而不是遍布在代码中。
  • 当从元数据中解耦这些创建 sprite 的方法时,这些方法更简单一些。
  • Metadata 可从任何地方获取。

因为 sprite 元数据在代码中位于一个地方,因此很容易发现和修改它们。另外,由于元数据是在创建 sprite 的方法之外定义的,而这些方法比较简单,更容易理解和修改。最后,尽管 Snail Bait 元数据直接嵌入在代码中,但是 sprite 元数据还是可以从任何地方获取,比如可在运行时创建元数据的级别编辑器。总而言之,与直接在创建 sprite 的方法中指定 sprite 数据相比,元数据易于修改,更为灵活。

回忆一下 清单 9,创建游戏 sprite 之后,Snail Bait 的 createSprites() 方法调用了两个方法:initializeSprites()addSpritesToSpriteArray()。清单 12 展示了 initializeSprites() 方法:

清单 12. 初始化 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;
         }
      }
   },
};

对于该游戏的 sprite 数组,initializeSprites() 调用了 positionSprites() 方法。该方法将 sprite 放到 sprite 元数据指定的位置。请注意,一些 sprite(比如纽扣和蜗牛)驻留在平台顶部。putSpriteOnPlatform() 方法如清单 13 所示。

清单 13. 将 sprite 放到平台上
SnailBait.prototype = { // methods
   ...

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

给定一个 sprite 和一个平台,putSpriteOnPlatform() 方法将 sprite 放置在平台顶部,并在 sprite 中存储该平台的引用,以便进一步引用它们。

您可能会怀疑,但是清单 14 已经证实,将单个 sprite 添加到所有封装 sprites 的数组中是一件非常简单的事情:

清单 14. 创建和初始化 Snail Bait sprite
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]);
      }
   },
};

现在,您已经了解了如何实现 sprite 和 sprite artists,以及 Snail Bait 如何创建和初始化其 sprite,接下来我将向您介绍如何将 sprite 合并到 Snail Bait 游戏循环中。


将 sprites 合并到游戏循环中

回顾一下本系列第二篇文章(参阅其 滚动背景 小节),Snail Bait 中几乎所有水平运动都是平移 Canvas 2D 上下文的结果。Snail Bait 总是将大多数 sprite 绘制在同一水平位置,表面上的水平运动完全是平移的结果。大多数 Snail Bait 的 sprite 与游戏平台同时水平移动,如清单 15 所示:

清单 15. 更新 sprite 位移
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
         }
      }
   },
   ...
};

draw() 方法为所有 sprite(除了跑步小人)设置平台速率以及平移位移。(跑步小人的水平位置是固定的,不会随着平台的移动而移动。)

设置平移位移以及绘制背景之后,draw() 方法使用 updateSprites()drawSprites() 更新并绘制游戏 sprite。这些方法如清单 16 所示:

清单 16. 更新和绘制 sprite
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);   
   },

当 sprite 不在视野范围内时

Snail Bait 最终版本有一个游戏场,其宽度是游戏画布的 4 倍(宽度是任意的,可以更宽)。在任何给定时间内,Snail Bait 背景的四分之三都不在视野范围内。因此不需要更新或绘制这四分之三背景中的 sprite,Snail Bait 也不需要这样做。严格地说,绘图时不需要排除那些 sprite,因为 Canvas 上下文会排除它们。

updateSprites()drawSprites() 对所有游戏 sprite 都可进行迭代,然后分别更新和绘制 sprite,但只有 sprite 是可见的,或者出现在目前显示的画布中。

在绘制 sprite 之前,drawSprites() 方法使用 setTranslationOffsets() 方法中计算的 sprite 位移来平移背景,然后将背景平移回原始位置,使 sprite 看起来像是在进行水平运动。


结束语

在本文中,我们向您展示了如何实现 sprites 和 sprite artist,以及如何将 sprite 合并到游戏循环中,在下一期 HTML5 2D 游戏开发 系列文章中,您将学习如何实现 sprite 行为,以及将它附加到特定 sprite 中。下期见!


下载

描述名字大小
样例代码j-html5-game4.zip3.9MB

参考资料

学习

获得产品和技术

  • Replica Island:您可以下载这个面向 Android 的流行开源平台视频频游的资源。

讨论

  • 加入 developerWorks 社区。探索由开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户进行交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=853610
ArticleTitle=HTML5 2D 游戏开发: Sprites
publish-date=01012013