HTML5 2D 游戏开发: 设置舞台

实现游戏对象、暂停、冻结、解冻和键盘输入

在本系列文章中,HTML5 专家 David Geary 将向您展示如何逐步实现一个 HTML5 2D 视频游戏。在本期文章中,您将学习如何将游戏代码封装在一个对象中,如何实现暂停和取消暂停,以及如何使用 CSS3 过渡来实现游戏重启的倒计时。

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 月 21 日

游戏开发的许多方面都和玩游戏没有关系。显示说明、暂停游戏、级别之间的过渡和滚动游戏分数,这些都是游戏开发人员必须在游戏本身以外实现的一些特性。

当游戏的灵感来临时,这些灵感中通常不包括显示高分数或级别之间的过渡的巧妙方式,开发人员会很自然地深入研究如何实现游戏机制,但对于游戏的基础架构却没有太多的想法。但在大多数项目中,如果想在开发后添加功能,所需的工作量比从一开始就添加功能要大得多。

在本系列的 上一期文章 中,我讨论了图形和动画,这些是 Snail Bait 游戏的基础内容。在本文中,我将临时转向去实现该游戏的一些基础架构。首先,我将 Snail Bait 的代码封装在一个 Game 对象中。最初实现该游戏时,我就是从这一步开始的,但在上一期 文章中,我不想在对象中实现它们而混淆了对图形和动画的讨论,所以我将对 Game 对象的介绍推迟到了现在。

我还将告诉您如何暂停和冻结 Snail Bait,以及随后如何利用动画倒计时解冻并重启游戏。在文章的结尾,我会回到游戏机制的主题,向您展示如何通过处理键盘事件来控制跑步小人的垂直位置。

在本文中,您将学习以下内容:

  • 将游戏函数封装在一个对象中。
  • 暂停和恢复游戏。
  • 当窗口失去焦点时自动暂停游戏。
  • 当窗口重新获得焦点时,利用动画的倒计时继续游戏。
  • 暂时显示给用户的消息(被称为 toast)。
  • 处理键盘输入。

在本文中,您将学习如何定义和实例化 JavaScript 对象,如何使用 CSS3 过渡,以及如何结合使用 setTimeout() 和这些过渡来实现分步动画。

游戏对象

在本系列文章中,到现在为止,我已经实现了所有 Snail Bait 函数,并将它们的几个变量定义为全局变量。当然,我们以后不会再这样做。如果您尚未了解全局变量的可恶之处,请参阅 参考资料,获得来自 Douglas Crockford 和 Nicholas Zakas 等 JavaScript 名人的支持论据。

从现在开始,我不再使用全局变量,而是将所有 Snail Bait 函数和变量封装在一个对象中。该对象由两部分组成,如清单 1 和清单 2 所示。(本文的完整样例代码请参见 下载。)

清单 1 是本游戏的构造函数,它定义了对象的属性:

清单 1. 本游戏的构造函数(部分清单)
var SnailBait = function (canvasId) {
   this.canvas  = document.getElementById(canvasId);
   this.context = this.canvas.getContext('2d');

   // HTML elements 

   this.toast = document.getElementById('toast'),
   this.fpsElement = document.getElementById('fps');

   // Constants

   this.LEFT = 1;
   this.RIGHT = 2;
   ...

   // Many more attributes are defined in the rest of this function
};

清单 2 是本游戏的原型,它定义了对象的方法:

清单 2. 是本游戏的原型(部分清单)
SnailBait.prototype = {
   // The draw() and drawRunner() methods were
   // discussed in the second article in this series.


   draw function (now) {
      this.setPlatformVelocity(); 
      this.setOffsets();
   
      this.drawBackground();
   
      this.drawRunner();
      this.drawPlatforms();
   },
   
   drawRunner: function () {
      this.context.drawImage(this.runnerImage,
         this.STARTING_RUNNER_LEFT,
         this.calculatePlatformTop(this.runnerTrack) - this.RUNNER_HEIGHT);
   },
   ...

   // Many more methods are defined in the rest of this object
};

为了在整个系列中添加一些新特性,我需要添加和删除一些方法,以及修改一些方法来实现。表 1 列出了 Snail Bait 的方法,因为它们会在本文结尾处出现:

表 1. Snail Bait 在此开发阶段的方法(按调用顺序列出)
方法描述
initializeImages()初始化游戏的图像。背景图像的 onload 事件处理器调用 start()
start()通过调用 requestAnimationFrame() 启动游戏,在可以绘制第一个动画帧的时候,它调用了 animate() 方法。
splashToast() [1]向玩家显示一个临时消息。
animate() [2]如果游戏没有暂停,此方法将会绘制下一个动画帧,并调用 requestNextAnimationFrame() 来安排 animate() 的另一次调用。如果游戏暂停,那么 animate() 会等待 200 毫秒,然后调用 requestNextAnimationFrame()
calculateFps()根据自最后一个动画帧起所经过的时间,计算帧速率。
draw()绘制一个动画帧。
setTranslationOffsets()为背景和平台设置过渡偏移。
setBackgroundTranslationOffset()根据当前时间设置背景过渡偏移。
setPlatformTranslationOffset()根据当前时间设置平台过渡偏移。
setPlatformVelocity()将平台速度设置为背景速度的倍数,以产生轻微的视差效果。
drawBackground()平移 Canvas 坐标系统,绘制背景两次,并将坐标系平移回它的原始位置。
drawRunner() [3]使用 drawImage() 绘制跑步小人。
drawPlatforms() [3]使用 2D 上下文的 strokeRect()fillRect() 绘制矩形平台。
calculatePlatformTop()针对给定轨道计算平台顶部的 Y 坐标(平台在三个水平轨道之一上移动)。
turnLeft()将背景和平台向右滚动。
turnRight()将背景和平台向左滚动。
togglePaused() [1]切换游戏的暂停状态。

[1] 在本文中介绍
[2] 由浏览器调用
[3] 在本系列的下一期文章中将被替换

函数与方法

作为某个对象的成员 JavaScript 函数被称为方法,而独立的函数被简单地称为函数

在本系列前两期文章中,我介绍过在 表 1 中列出的大部分方法,它们在当时只是函数。在本文中,我会讨论两个新的方法:togglePaused()splashToast(),还会修改其他方法,比如 animate()

清单 1清单 2 中的 JavaScript 定义了一个函数和一个原型,但没有实例化一个 SnailBait 对象。我会在下一节中完成此操作。


启动游戏

SnailBait 的全局对象

清单 1清单 3 所示,Snail Bait 只有两个全局对象:SnailBait 函数和 snailBait 对象。

清单 3 显示了启动游戏的 JavaScript。该清单的开头定义了三个 SnailBait 方法的实现:animate()start()initializeImages()

清单 3. 启动
SnailBait.prototype = {
   ...

   // The 'this' variable in the animate() method is
   // the window object, so the method uses snailBait instead

   animate: function (now) { 
      snailBait.fps = snailBait.calculateFps(now); 
      snailBait.draw(now);

      requestNextAnimationFrame(snailBait.animate);
   },

   start: function () {
      this.turnRight();                     // Sets everything in motion
      this.splashToast('Good Luck!', 2000); // "Good Luck" is displayed for 2 seconds

      requestNextAnimationFrame(this.animate);
   },

   initializeImages: function () {
      this.background.src = 'images/background_level_one_dark_red.png';
      this.runnerImage.src = 'images/runner.png';
   
      this.background.onload = function (e) {

         // ...the 'this' variable is the window object,
         // so this function uses snailBait instead.
     
         snailBait.start();
      };
   },
}; // End of SnailBait.prototype


// Launch game

var snailBait = new SnailBait(); // Note: By convention, the object
                                     // reference starts with lowercase, but
                                     // the function name starts with uppercase

snailBait.initializeImages();

JavaScript 的挑剔的 this 对象

如果您曾使用过经典的面向对象的语言(如 Java),那么您应该预料到,某个对象的 this 变量总是指向与方法相关联的对象。

JavaScript 最麻烦的一件事情是,this 变量是变化的。在清单 2 中,animate() 方法和背景图像的 onload 事件处理器中的 this 变量涉及 window 对象,而不是 snailBait 对象,所以这些方法可以直接访问 snailBait 对象。

清单 3 中的 JavaScript 实例化一个 SnailBait 对象,并调用了它的 initializeImages() 方法,该方法实现背景图像的 onload 事件处理器。当图像加载时,该事件处理器调用了 start() 方法。

start() 方法调用 turnRight(),它设置移动中的背景和平台。还调用了 splashToast(),以显示 Good Luck! 两秒。最后,start() 调用了 requestNextAnimationFrame() polyfill(我们曾在本系列第二期文章中讨论过它,请参阅那篇文章的 A requestAnimationFrame() polyfill 部分),它最终调用该游戏的 animate() 方法。

animate() 方法将绘制当前帧,然后调用 requestNextAnimationFrame()(指定它本身作为回调函数)来保持动画。

这就是游戏开始的方式。接下来,我将告诉您在开始游戏之后如何暂停它。


暂停游戏

HTML5 游戏(尤其是视频游戏)必须能够暂停。在清单 4 中,我已经修改了 Snail Bait 的游戏循环,以便暂停和取消暂停游戏:

清单 4. 暂停和取消暂停
var SnailBait = function (canvasId) {
   ...
   this.paused = false,
   this.PAUSED_CHECK_INTERVAL = 200; // milliseconds
   ...
};

SnailBait.prototype = {
   animate: function (now) { 
      if (snailBait.paused) {

         // Check again in snailBait.PAUSED_CHECK_INTERVAL milliseconds

         setTimeout( function () {

            requestNextAnimationFrame(snailBait.animate);

         }, snailBait.PAUSED_CHECK_INTERVAL);
      }
      else {

         // The game loop from Listing 1

         snailBait.fps = snailBait.calculateFps(now); 
         snailBait.draw(now);
         requestNextAnimationFrame(snailBait.animate);
      }
   },

   togglePaused: function () {
      this.paused = !this.paused;
   },
};

togglePaused() 方法简单地切换该游戏的 paused 变量。当该变量为 true (意味着游戏已暂停)时,animate() 方法不会执行游戏循环。

每秒检查 60 次(假设帧速率为 60fps),查看是否应该恢复某个暂停的帧,这是没必要的,且效率低下,因此,清单 4 中的 animate() 方法只会等待 200ms,然后调用 requestNextAnimationFrame() polyfill,在到时间绘制下一个动画帧时,它就会安排对 animate() 的另一次调用。

当窗口失去焦点时自动暂停

W3C 的 Timing control for script-based animations(基于脚本的动画时序控制) 规范对于使用 requestAnimationFrame() 实现的动画具有下列规定:

如果页面目前不可见,可以大幅裁剪该页面上的动画,那么就不会经常更新这些页面,因此只会占用极少的 CPU 资源。

术语大幅节流意味着,浏览器以极低的帧速率(通常介于 1 到 10 fps 之间)调用您的动画回调,如图 1 所示,帧速率在窗口重新获得焦点后立即达到 6fps:

图 1. 在失去焦点和重新获得焦点后的 Snail Bait
重新获得焦点后的 Snail Bait 的屏幕截图

大幅节流的帧速率可能对碰撞检测算法造成破坏,该算法通常基于帧速率确定是否发生了(或将发生)碰撞。您可以在游戏的窗口失去焦点时暂停游戏,并在窗口重新获得焦点时重新启动它,避免因大幅节流的帧速率而造成碰撞检测的崩溃。在清单 5 中,您可以看到如何做到这一点:

清单 5. 自动暂停
window.onblur = function () { // window looses focus
   if (!snailBait.paused) {
      snailBait.togglePaused();
   }
};

window.onfocus = function () { // window regains focus
   if (snailBait.paused) {
      snailBait.togglePaused();
   }
};

当窗口失去焦点时,您不仅要暂停游戏,还应该在暂停游戏时冻结 它。


冻结游戏

暂停游戏所涉及的不仅仅是简单地中止动画。游戏应该恢复到离开它们时的确切位置清单 4 的出现满足了这一要求。毕竟,在游戏暂停后,什么也不会发生,就好像游戏已经恢复为和它暂停之前一样。但实际情况并非如此,因为所有动画(包括 Snail Bait 的动画)的主要参数是时间。

如我在第二期文章中所讨论的(请参阅这篇文章的 requestAnimationFrame() 部分),requestAnimationFrame() 将时间传递给指定的回调函数。在使用 Snail Bait 的情况下,该回调函数是 animate() 方法,它随后会将时间传递给 draw() 方法。

当游戏暂停时,即使动画没有运行,时间依然有增无减地继续前进。而且,由于 Snail Bait 的 draw() 方法根据从 animate() 接收的时间绘制下一个动画帧,所以 清单 4 中实现的 togglePaused() 会导致在恢复暂停游戏时,游戏的时间寸步难行。

清单 6 显示了 Snail Bait 如何避免暂停的游戏在恢复时出现时间的变化:

清单 6. 冻结游戏
var SnailBait = function (canvasId) {
   ...
   this.paused = false,
   this.pauseStartTime = 0,
   this.totalTimePaused = 0,
   this.lastAnimationFrameTime = 0,
   ...
};

SnailBait.prototype = {
   ...
   calculateFps: function (now) {
      var fps = 1000 / (now - this.lastAnimationFrameTime);
      this.lastAnimationFrameTime = now;
   
      if (now - this.lastFpsUpdateTime > 1000) {
         this.lastFpsUpdateTime = now;
         this.fpsElement.innerHTML = fps.toFixed(0) + 'fps';

      }

      return fps; 
   },

   togglePaused: function () {
      var now = +new Date();

      this.paused = !this.paused;
   
      if (this.paused) {
         this.pauseStartTime = now;
      }
      else {
         this.lastAnimationFrameTime += (now - this.pauseStartTime);
      }
   },
};

清单 6 中,我修改了 Snail Bait 的 togglePaused()calculateFps() 方法,以计算游戏被暂停(如果有的话)的时间量。

为了计算前一个动画帧的帧速率,我从当前时间减去我绘制最后一帧的时间,并用 1000 被除以该值,这使得我获得的帧速率是以秒为单位,而不是以毫秒为单位。(请参阅第二期文章的 以 fps 计算动画速率 部分,了解关于计算帧速率的更多信息。)

当游戏恢复时,我将游戏暂停的时间量加到最后一个动画帧的时间。这个加法有效地清除了暂停,游戏恢复到在暂停开始时它的确切位置。


当窗口获得焦点时解冻游戏

当游戏恢复时,玩家会喜欢平稳地过渡到操作,为他们提供一些时间来重新获得控制。在这段时间里,在恢复游戏之前提供有关剩余时间量的反馈,这会是一个好主意。Snail Bait 通过在 toast 中显示倒计时来实现该反馈,所以我从 toast 的概述开始这个讨论。

Toast

toast 是游戏暂时向玩家显示的某些消息,像图 2 中的 Good Luck! toast:

图 2. toast
Snail Bait 中的 Good Luck! toast 的屏幕截图

像 Snail Bait 本身,Snail Bait toast 是使用 HTML、CSS 和 JavaScript 的组合来实现的,如接下来的三个清单所示。

清单 7 显示一个 toast 的 HTML 代码:

清单 7. toast:HTML
<!DOCTYPE html>
<html>
   <head>
      ...
   </head>

   <body>
      <div id='wrapper'>
         <!-- Toast...................................................-->

         <div id='toast'></div>
         ...
   
      </div>
      ...
  </body>
</html>

实现 Snail Bait 的 Good Luck! toast 的 CSS 如清单 8 所示:

清单 8. toast:CSS
#toast {
   position: absolute;
   ...

   -webkit-transition: opacity 0.5s;
   -moz-transition: opacity 0.5s;
   -o-transition: opacity 0.5s;
   transition: opacity 0.5s;

   opacity: 0;
   z-index: 1;
   display: none;
}

清单 9 显示 Good Luck! toast 的 JavaScript:

清单 9. toast:JavaScript
var SnailBait =  function () {
   ...
   this.toast = document.getElementById('toast'),
   this.DEFAULT_TOAST_TIME = 3000, // 3 seconds
   ...
};

SnailBait.prototype = {
   ...
   start: function () {
      ...
      snailBait.splashToast('Good Luck!');
   },

   splashToast: function (text, howLong) {
      howLong = howLong || this.DEFAULT_TOAST_TIME;

      toast.style.display = 'block';
      toast.innerHTML = text;

      setTimeout( function (e) {
         toast.style.opacity = 1.0; // After toast is displayed
      }, 50);

      setTimeout( function (e) {
         toast.style.opacity = 0; // Starts CSS3 transition

         setTimeout( function (e) {
            toast.style.display = 'none'; // Just before CSS3 animation concludes
         }, 480);
      }, howLong);
   },
   ...
}

正如前面这三个清单中的实现,toast 只是 DIV,您可以在 清单 7 中看到。事情在 清单 8 中变得更为有趣,其中列出了 DIV 的 CSS。DIV 的位置是 absolute,这意味着它可以显示在其他 DIV 的上面或下面,而不是前面或后面。toastDIVz-index 值也是 1,这意味着它始终显示在游戏画布的上面,其 z-index 的默认值为 0。最后,toast 元素的 CSS 定义一个绑定到 opacity 属性的 0.5 秒的过渡,当更改该属性时,CSS 用 0.5 秒时间将 DIV 从之前的不透明度平滑过渡到新的值。

清单 9splashToast() 方法中,事情变得更加有趣,toast 会在一段指定的时间内显示。当 Snail Bait 调用 splashToast() 时,默认的显示时间为 3 秒,toast 淡入 0.5 秒,短暂地显示 2.5 秒,然后淡出 0.5 秒。下面是它的工作原理:

splashToast() 方法首先将 toastDIVdisplay 属性设置为 block,这通常会使得 DIV 变得可见。但是,因为它的 opacity 属性的初始值为 0,所以 toastDIV 仍保持不可见。然后 splashToast()toastDIV 的内部 HTML 设置为您传递给方法的文本,但不透明度设置保持不变,所以设置文本也不会使得 DIV 可见。

为了使得 toastDIV 可见,我将它的不透明度设置为 1.0。该设置触发因我在 清单 8 中指定的过渡而产生的 CSS3 动画,但是,仅当不透明度设置稍后(在本例中是 50 毫秒)产生看起来很奇怪的 setTimeout() 的结果时,才会修改不透明度设置,在该函数中它是封闭的。这是因为:

只能对有中间状态的元素属性指定 CSS3 过渡。例如,如果您将不透明度从 0.2 修改为 0.3(随机选择的两个数字),中间不透明度为 0.21、0.22 等。

过渡需要中间状态,这是有一定道理的;如果没有中间状态,就没有一个明确的方法来指定过渡的动画。例如,这就是为什么您不能为 display 属性指定一个过渡的原因,它没有中间状态。不仅如此,如果您修改了 display 属性,那么 CSS3 将不再接受您为其他任何属性指定的任何过渡。这也是有一定道理的,因为您在让 CSS3 执行互相冲突的两件事:例如,通过修改 display 属性让元素立即可见,但又使用 opacity 属性的过渡使其慢慢地淡入视图。CSS3 不能两件事同时做,所以它选择了修改 display 属性。

半透明的 DIV 和事件

经过前面关于 splashToast() 的讨论之后,您可能想知道为什么该方法要这么麻烦地操作 toastDIVdisplay 属性。为什么不直接操纵的 DIV 的不透明度使其变得可见或不可见呢?答案是,除非您明确地有意这样做,让不可见的 DIV 悬停在那里,否则这并不是一个好主意,因为它们很可能以其他令人惊讶的方式(如拦截事件)来显示它们的存在。

如果 splashToast() 同时设置 toastDIVdisplayopacity 属性,CSS3 会忽略不透明度的过渡,因此,在设置 display 属性之后,该方法会将不透明度设置为 1.0,更确切地说,在大约 50ms 之后会执行该操作。

最后,当所需的显示时间过去后,splashToast() 会将 toastDIVopacity 属性重新设置为 0,这会再次触发一个 0.5 秒的 CSS3 动画。在 CSS3 动画开始两秒钟后之后,splashToast() 方法会将 display 属性重新设置为 0

解冻 Snail Bait

当 Snail Bait 在暂停后恢复播放时,它通过三秒钟的倒计时让玩家有时间做好准备,如图 3 所示:

图 3. 解冻过程中的倒计时
Snail Bait 解冻过程中的倒计时屏幕截图

清单 10 显示了倒计时的 JavaScript:

清单 10. 倒计时:JavaScript
var SnailBait = function (canvasId) {
   ...
   this.toast = document.getElementById('toast'),
};


window.onblur = function (e) {  // Pause if unpaused
   if (!snailBait.paused) {
      snailBait.togglePaused();
   }
};

window.onfocus = function (e) {  // unpause if paused
   var originalFont = snailBait.toast.style.fontSize;

   if (snailBait.paused) {
      snailBait.toast.style.font = '128px fantasy';

      snailBait.splashToast('3', 500); // Display 3 for one half second

      setTimeout(function (e) {
         snailBait.splashToast('2', 500); // Display 2 for one half second

         setTimeout(function (e) {
            snailBait.splashToast('1', 500); // Display 1 for one half second

            setTimeout(function (e) {
               snailBait.togglePaused();

               setTimeout(function (e) { // Wait for '1' to disappear
                  snailBait.toast.style.fontSize = originalFont;
               }, 2000);
            }, 1000);
         }, 1000);
      }, 1000);
   }
};

当 Snail Bait 窗口重新获得焦点时,它会使用 splashToast() 方法启动倒计时。每个数字淡入 0.5 秒,然后淡出 0.5 秒。一旦倒计时为零,onfocus 处理器就会重新启动游戏。

然而,如果玩家在倒计时过程中激活了另一个窗口或选项卡,清单 10 中的代码就会无法正常工作,因为无论窗口是否获得焦点,游戏都会在倒计时结束时重新开始。这很容易解决,利用一个 windowHasFocus 标实即可,如清单 11 所示:

清单 11. 在倒计时期间失去焦点的处理
var SnailBait = function (canvasId) {
   ...
   this.windowHasFocus = true,
   ...
};
...

SnailBait.prototype = {
   ...

   splashToast: function (text, howLong) {
      howLong = howLong || this.DEFAULT_TOAST_TIME;

      toast.style.display = 'block';
      toast.innerHTML = text;

      setTimeout( function (e) {
         if (snailBait.windowHasFocus) {
            toast.style.opacity = 1.0; // After toast is displayed
         }
      }, 50);

      setTimeout( function (e) {
         if (snailBait.windowHasFocus) {
            toast.style.opacity = 0; // Starts CSS3 transition
         }

         setTimeout( function (e) { 
            if (snailBait.windowHasFocus) {
               toast.style.display = 'none'; 
            }
         }, 480);
      }, howLong);
   },
   ...
};
...

window.onblur = function (e) {  // pause if unpaused
   snailBait.windowHasFocus = false;
   
   if (!snailBait.paused) {
      snailBait.togglePaused();
   }
};

window.onfocus = function (e) {  // unpause if paused
   var originalFont = snailBait.toast.style.fontSize;

   snailBait.windowHasFocus = true;

   if (snailBait.paused) {
      snailBait.toast.style.font = '128px fantasy';

      snailBait.splashToast('3', 500); // Display 3 for one half second

      setTimeout(function (e) {
         snailBait.splashToast('2', 500); // Display 2 for one half second

         setTimeout(function (e) {
            snailBait.splashToast('1', 500); // Display 1 for one half second

            setTimeout(function (e) {
               if ( snailBait.windowHasFocus) {
                  snailBait.togglePaused();
               }

               setTimeout(function (e) { // Wait for '1' to disappear
                  snailBait.toast.style.fontSize = originalFont;
               }, 2000);
            }, 1000);
         }, 1000);
      }, 1000);
   }
};

键盘输入

在 Snail Bait 中,玩家可以使用键盘来控制跑步小人,所以我会对该游戏如何处理键盘输入做一个简短的讨论,并以此来结束这篇文章。dk 键可以让移动的跑步小人向左和向右移动,而 jf 则分别让跑步小人跳跃和下落。图 4 显示了跳上第三个平台的轨道之后的跑步小人:

图 4. 在轨道之间跳跃之后的奔跑
在轨道之间跳跃之后的 Snail Bait 跑步小人的屏幕截图

您只能将键盘事件侦听器添加到 focusable HTML 元素。canvas 元素是不能获得焦点的,所以 Snail Bait 将一个 onkeydown 事件处理器添加到 window 对象,如清单 12 所示:

清单 12. 对键盘输入的反应
var runnerTrack = 1,
    BACKGROUND_VELOCITY = 42;

function turnLeft() {
   bgVelocity = -BACKGROUND_VELOCITY;
}

function turnRight() {
   bgVelocity = BACKGROUND_VELOCITY;
}

window.onkeydown = function (e) {
   var key = e.keyCode;

   if (key === 80 || (paused && key !== 80)) {  // p
      togglePaused();
   }

   if (key === 68 || key === 37) { // d or left arrow
      turnLeft();
   }
   else if (key === 75 || key === 39) { // k or right arrow
      turnRight();
   }
   else if (key === 74) { // j
      if (runnerTrack === 3) {
         return;
      }
      runnerTrack++;
   }
   else if (key === 70) { // f
      if (runnerTrack === 1) {
         return;
      }
      runnerTrack--;
   }
};

重要的是要认识到,Snail Bait 的游戏循环在不断运行。在浏览器已经准备好画下一个动画帧时,浏览器就会调用 animate() 函数,而 animate() 反过来又不断地调用 draw()(在清单 2 中列出)。

由于游戏循环在不断运行,键盘事件处理器只需设置游戏的变量即可。例如,当您按下 k 将跑步小人向右移动,事件处理器会将 bgVelocity 设置为 BACKGROUND_VELOCITY = 42(像素/秒),当您按下 d 将跑步小人向左移动,事件处理器会将 bgVelocity 设置为 -42 像素/秒。一直到后来,当游戏绘制下一个动画帧时,那些设置才会生效。


结束语

在本系列的下一篇文章中,我将向您演示如何将 Snail Bait 的插入图形转换成被称为精灵 (sprite) 的动画对象。您将看到如何采用几种不同的方式绘制精灵,包括从一个精灵表 (spritesheet) 绘制它们,并且您将看到如何将它们纳入 Snail Bait 的现有代码中。下次再见!


下载

描述名字大小
样例代码j-html5-game3.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=855645
ArticleTitle=HTML5 2D 游戏开发: 设置舞台
publish-date=01212013