HTML5 2D 游戏开发: 实现 Sprite 行为

为 Snail Bait 人物配备行为

在本系列中,HTML5 专家 David Geary 将向您展示如何逐步实现一个 HTML5 2D 视频游戏。在这一期的文章中,您将学习如何实现任何视频游戏的精华部分: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 年 3 月 11 日

优秀的故事离不开优秀的人物角色。就像图书和电影一样,视频游戏也需要具有有趣行为的人物角色。例如,Braid(一直是最畅销的平台游戏)中的主角可操纵时间。精巧的行为使该游戏脱颖而出。

行为是任何视频游戏的灵魂,将行为添加到 前一期文章 中为 Snail Bait 实现的惰性 sprite 中,就会立刻使该游戏变得更加有趣,如图 1 所示:

图 1. 完成本文后 Snail Bait 的状态
完成本文后 Snail Bait 状态的屏幕截图

回想一下前一篇文章中的 Sprite 对象 一节,Snail Bait 的 sprite 没有实现自己的活动,比如跑、跳或发怒。相反,sprite 依靠其他对象(称为行为)来控制操作。

图 1 显示了蜗牛射出蜗牛炸弹的行为。其他无法在 图 1 的静态图像中看到的行为包括:

  • 跑步小人跑动
  • 纽扣在平台上来回移动
  • 红宝石和蓝宝石闪耀光芒

表 1 总结了这些行为:

表 1. 本文中讨论的行为
Sprite行为说明
纽扣和蜗牛 paceBehavior 在一个平台中来回移动
跑步小人 runBehavior 循环跑步小人的图像,看起来像是跑步小人正在跑动
蜗牛 snailShootBehavior 从蜗牛的口中射出一个蜗牛炸弹
蜗牛 cycleBehavior 循环显示一个 sprite 的图像
蜗牛炸弹 snailBombMoveBehavior 在画布显示范围中水平地将蜗牛炸弹向左移动

操作时间

在 Braid 中,主角 Tim 可以操纵时间,但每个视频游戏都擅长操纵时间。在本文中,您会看到潜在的时间流动行为。在本系列接下来的两篇文章中,我将介绍如何通过让时间弯曲来实现非线性运动,这是跑动和跳跃等真实运动的基本特征。

表 1 中列出的行为不足该游戏行为的一半,在本系列第一篇文章中的 Snail Bait 的 sprite 和行为 表中,可以看到全部行为。它们也是 sprite 的行为基础;举例而言,跳跃要复杂得多,您将在以后的文章中看到。尽管如此,对于本文中的简单行为的实现,仍有许多要学习的地方,包括如何:

  • 实现行为并将它们分配给 sprite
  • 循环显示一个 sprite 的一系列图像
  • 创建轻量级行为以节省内存消耗
  • 组合行为
  • 使用行为发射炮弹

行为基础

Replica Island 的行为

行为的概念来自一个流行的开源 Android 游戏 Replica Island。Snail Bait 的许多图形也来自 Replica Island。请参阅 参考资料,获得该游戏以及该游戏的创建者探讨行为的博客帖子的链接。

任何对象都可以是一个行为,只要它拥有一个 execute() 方法。该方法接受 3 个参数:sprite、时间和游戏动画的帧率。行为的 execute() 方法依据时间和动画帧率来修改 sprite 的状态。

行为很强大,因为:

  • 它们将 sprite 与其行为分离开来。
  • 可在运行时更改 sprite 行为。
  • 可实现用于任何 sprite 的行为。
  • 无状态行为可用作 轻量级行为

在讨论 表 1 中列出的行为在实现之前,我将通过跑步小人的总体行为大致概括一下 — 如何实现它们并将它们与 sprite 相关联。


跑步小人行为

Snail Bait 的跑步小人有 4 种行为,如表 2 所示:

表 2. 跑步小人的行为
行为说明
runBehavior 循环显示 sprite 表单中的跑步小人,以产生跑步小人正在跑动的效果
jumpBehavior 控制跳跃的所有方面:上升、下降和着地
fallBehavior 控制跑步小人在下落时的垂直移动
runnerCollideBehavior 检测跑步小人与其他 sprite 之间的碰撞并做出反应

我通过一个对象数组来指定跑步小人的行为,并将这个数组传递给 Sprite 构造函数,如清单 1 所示:

清单 1. 创建 SnailBait 的跑步小人
var SnailBait = function () {
   ...

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

                           ]);
};

跑步小人的行为如清单 2 中所示,其中删除了实现细节:

清单 2. 跑步小人行为对象
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
         ...
      }
   };
};

在每个动画帧中,Snail Bait 都会迭代它的 sprite 数组,调用每个 sprite 的 update() 方法,如清单 3 所示:

清单 3. 执行行为
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);
      }
   }
};

Sprite.update() 方法迭代了 sprite 的行为,调用每个行为的 execute() 方法。Snail Bait 持续(每个动画帧一次)调用与所有可视 sprite 有关联的行为。因此,一个行为的 execute() 方法与其他大多数方法可能有所不同,大多数方法的调用频率相对较低;而每个 execute() 方法就像一个不停运转的小型马达。

现在您已经大致理解了 sprite 和行为,我将分别介绍它们的具体实现。

Strategy 设计模式

行为是 Strategy 设计模式的一种实现,Strategy 设计模式将算法封装在对象中(参阅 参考资料)。在运行时,您可混搭这些算法,将一个行为集合分配给一个 sprite。行为比将算法直接硬编码到各个 sprite 中更灵活。

跑动

Snail Bait 通过两种操作来让跑步小人看起来正在跑动。首先,正如本系列第二篇文章中 滚动背景 一节所述,游戏不停滚动背景,让它看起来像是跑步小人正在水平移动。第二,跑步小人跑动行为的循环显示来自游戏的 sprite 表单的一系列图像,如图 2 所示:

图 2. 跑动序列
Snail Bait 跑步小人的跑动顺序的屏幕截图

清单 4 中的代码实现了跑动行为:

清单 4. 跑步小人的 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;
         }
      }
   },
   ...
};

runBehavior 对象的 execute() 方法定期将跑步小人的 artist 前进到 sprite 表单中的跑步小人序列中的下一个图像。(可以在本系列第 4 篇文章中的 Sprite artists 和 sprite 表单 一节中看到 Snail Bait 的 sprite 表单)。

runBehavior 前进到下一个图像的频率决定了跑步小人跑动的速度。该时间间隔使用跑步小人的 runAnimationRate 属性设置。在游戏启动时,跑步小人并没有跑动,所以它的 runAnimationRate 最初为 0。但是,当玩家向左转或向右转时,Snail Bait 将该属性设置为 17 帧/秒,如清单 5 所示,跑步小人开始跑动:

清单 5. 开始播放跑动动画
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;
   },

};

时间流

像跑步小人的跑动行为一样,几乎所有行为都以时间为基础。而且因为一个游戏的动画不断在运行,所以许多修改游戏行为的函数(比如 清单 5 中的 turnLeft()turnRight())会设置简单的游戏变量来完成任务。当游戏绘制下一个动画帧时,这些变量会影响游戏的行为。

前面已经讨论过,turnLeft()turnRight() 方法(由游戏的键盘事件处理函数调用)使用 runAnimationRate 属性控制跑步小人循环其图像序列的速度。这些方法还通过设置 bgVelocity 属性来控制跑步小人从左移动到右的速度,该属性表示背景滚动的速度。


轻量型行为

前一节中讨论的跑步小人的跑动行为需要维持状态,也就是说,行为持续的时间会推动 sprite 的图像移动。这种状态将跑步状态与行为紧密联系在一起。所以,如果您希望让另一个 sprite 跑动,则需要拥有另一个跑动行为。

不需要维持状态的行为更加灵活;例如,可将这些行为用作轻量型行为。轻量型行为是对象的一个实例,可供其他许多对象同时使用。图 3 演示了一种无状态的移动行为,它让 sprite 在一个平台上来回移动。该行为一个实例被用于游戏的纽扣和蜗牛,所有这些对象都在平台上来回移动,如图 3 所示:

图 3. 纽扣移动顺序
Snail Bait 纽扣移动顺序的屏幕截图

清单 6 给出了 Snail Bait 的 createButtonSprites() 方法,它将单独的移动行为添加到每个纽扣:

清单 6. 创建移动纽扣
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);
      }
   },
   ...
};

清单 7 给出了 paceBehavior 对象:

清单 7. 移动行为
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);
      }
   },

移动行为修改了一个 sprite 的水平位置。该行为实现基于时间的运动,通过将 sprite 的速度(以像素/秒为单位)除以动画的帧率(以帧/秒为单位),可以计算出当前动画帧中的移动 sprite 的像素数,从而获得像素/帧的值。(请参阅本系列第二篇文章中的 基于时间的运动 一节,了解基于时间的运动的更多信息)。


非游戏独有的行为

轻量型行为和状态

paceBehavior 可用作一个轻量型行为,因为它是无状态的。它没有状态是因为它将状态(每个 sprite 的位置和方向)存储在 sprite 自身中。

我们在本文中讨论的第一个行为( runBehavior )是一种有状态行为,它与一个 sprite 紧密关联。接下来要讨论的 paceBehavior 是一个无状态行为,该行为与各个 sprite 是分离开的,所以一个实例可供多个 sprite 使用。

行为可进一步一般化:不仅可以将它们与各个 sprite 分离,还可以将它们与游戏本身分离。Snail Bait 使用了 3 种可用在任何游戏中使用的行为:

  • bounceBehavior
  • cycleBehavior
  • pulseBehavior

弹跳行为上下弹跳一个 sprite,这个周期性的行为循环显示 sprite 的一组图像,心跳行为操纵了 sprite 的不透明度,使它显示为好像是 sprite 正在心跳。

弹跳和心跳行为都涉及到非线性动画,我将在未来的文章中探讨这一主题。这个周期性行为线性循环显示了一个 sprite 的行为,所以我将使用该行为的实现来演示如何实现可在任何游戏中使用的行为。

闪耀的红宝石

Snail Bait 的红宝石和蓝宝石会不停闪耀,如图 4 所示:

图 4. 闪耀的红宝石序列
Snail Bait 的闪耀红宝石序列的屏幕截图

Snail Bait 的 sprite 表单包含红宝石和蓝宝石的图形序列;循环显示这些图像会带来闪耀的效果。

清单 8 给出了创建红宝石的 Snail Bait 方法。可以采用几乎相同的方法(未给出)创建蓝宝石。createRubySprites() 方法还创建一种每隔 500 毫秒显示红宝石闪耀序列中的下一个图像 100 毫秒的周期性行为。

清单 8. 创建红宝石
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
         ...
      }
   },
   ...
};

清单 9 列出了这个周期性行为:

清单 9. CycleBehavior 行为
// 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;
      }
   }
};

使行为一般化

寻找机会让行为一般化,使它们可用在更多的环境中,这是一个不错的想法。

这个周期性行为将适用于任何一个具有 sprite 表单 artist 的 sprite,表明该行为不是 Snail Bait 所独有的,因此可在不同游戏中重用它。清单 4 中特定于 sprite 的跑动行为与 清单 9 中非游戏特定的周期性行为具有很多共同点;事实上,周期性行为源于跑动行为。(跑动行为可能是一种更加一般化的周期性行为,但跑动行为还会考虑跑步小人的动画速率。)


组合行为

各个行为会封装特定的操作,比如跑步、移动或闪耀。也可组合行为来实现更复杂的效果;例如,在蜗牛在其平台上来回移动时,会定期射出蜗牛炸弹,如图 5 所示:

图 5. 蜗牛射击序列
Snail Bail 的蜗牛射击序列的屏幕截图

蜗牛射击序列是 3 种行为的组合:

  • paceBehavior
  • snailShootBehavior
  • snailBombMoveBehavior

paceBehaviorsnailShootBehavior 与蜗牛相关联,snailBombMoveBehavior 与蜗牛炸弹相关联。当 Snail Bait 创建 sprite 时,它在 Sprite 构造函数中指定前两个行为,如清单 10 中所示:

清单 10. 创建蜗牛
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
      }
   },
};

每隔 1.5 秒,蜗牛的 CycleBehavior 就会循环显示 sprite 表单中的图像,如图 6 所示,显示每个图像 300 毫秒,这使它看起来像蜗牛在定期张开和闭上嘴巴。蜗牛的 paceBehavior 方法使它在平台上来回移动。

图 6. 蜗牛射击序列的 Sprite 表单图像
Snail Bait 的蜗牛射击序列的 sprite 表单图像的屏幕截图

蜗牛炸弹由 armSnails() 方法创建,如清单 11 所示,Snail Bait 会在游戏开始时调用。该方法迭代游戏的蜗牛,为每个蜗牛创建一个蜗牛炸弹,为每个炸弹配备一个 snailBombMoveBehavior 行为,将对蜗牛的引用存储在蜗牛炸弹中。

清单 11. 武装蜗牛
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);
      }
   },
};

蜗牛的 snailShootBehavior 射出蜗牛炸弹,如清单 12 所示:

清单 12. 射出蜗牛炸弹
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;
         }
      }
   },

};

基于行为的游戏

对于一个基于行为的游戏,在实现了基本的架构之后,完善游戏的主要任务就是实现行为。无需担忧游戏的力学基础,比如动画、帧率、滚动背景等,要让游戏充满生机,可将大部分精力集中在实现行为上。而且因为行为可在运行时混搭,所以您可以通过组合行为快速地设计场景原型。

因为 snailShootBehavior 与蜗牛有关联,所以传递给行为的 execute() 方法的 sprite 就是蜗牛。

一个蜗牛维持着对其蜗牛炸弹的引用,所以 snailShootBehavior 通过蜗牛访问该炸弹。然后,snailShootBehavior 会检查蜗牛的当前图像是否位于 图 6 中的最右端,也就是说,检查蜗牛是否马上就会张开它的嘴巴;如果是,那么该行为会将炸弹放入蜗牛口中并显示它。

因此,射出蜗牛炸弹涉及在正确的条件下放置炸弹并让它可见。移动炸弹是 snailBombMoveBehavior 的职责,如清单 13 所示:

清单 13. 蜗牛炸弹移动行为
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;
         }
      }
   },

只要蜗牛炸弹出现在视图中,snailBombMoveBehavior 就会以速率 snailBait.SNAIL_BOMB_VELOCITY (450) 像素/秒向左侧移动炸弹。一旦炸弹移出视图,该行为就会使炸弹变得不可见。


结束语

在本系列的下一篇文章中,我将进一步分析时间和行为,探讨跑步小人的跳跃行为。您会看到如何实现一个 JavaScript 秒表对跳跃的计时。这种基本技术(计时动画)将会在您自己的游戏中大量使用。


下载

描述名字大小
样例代码j-html5-game5-code.zip1.2MB

参考资料

学习

获得产品和技术

  • 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=860606
ArticleTitle=HTML5 2D 游戏开发: 实现 Sprite 行为
publish-date=03112013