实现游戏对象、暂停、冻结、解冻和键盘输入
在本系列文章中,HTML5 专家 David Geary 将向您展示如何逐步实现一个 HTML5 2D 视频游戏。在本期文章中,您将学习如何将游戏代码封装在一个对象中,如何实现暂停和取消暂停,以及如何使用 CSS3 过渡来实现游戏重启的倒计时。
David Geary, 作者和演讲家, Clarity Training, Inc.
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
大幅节流的帧速率可能对碰撞检测算法造成破坏,该算法通常基于帧速率确定是否发生了(或将发生)碰撞。您可以在游戏的窗口失去焦点时暂停游戏,并在窗口重新获得焦点时重新启动它,避免因大幅节流的帧速率而造成碰撞检测的崩溃。在清单 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 本身,Snail Bait toast 是使用 HTML、CSS 和 JavaScript 的组合来实现的,如接下来的三个清单所示。
清单 7 显示一个 toast 的 HTML 代码:
清单 7. toast:HTML
…
<div id='toast'></div>
...
</div>
...
实现 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 的上面或下面,而不是前面或后面。toastDIV 的 z-index 值也是 1,这意味着它始终显示在游戏画布的上面,其 z-index 的默认值为 0。最后,toast 元素的 CSS 定义一个绑定到 opacity 属性的 0.5 秒的过渡,当更改该属性时,CSS 用 0.5 秒时间将 DIV 从之前的不透明度平滑过渡到新的值。
在 清单 9 的 splashToast() 方法中,事情变得更加有趣,toast 会在一段指定的时间内显示。当 Snail Bait 调用 splashToast() 时,默认的显示时间为 3 秒,toast 淡入 0.5 秒,短暂地显示 2.5 秒,然后淡出 0.5 秒。下面是它的工作原理:
splashToast() 方法首先将 toastDIV 的 display 属性设置为 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() 的讨论之后,您可能想知道为什么该方法要这么麻烦地操作 toastDIV 的 display 属性。为什么不直接操纵的 DIV 的不透明度使其变得可见或不可见呢?答案是,除非您明确地有意这样做,让不可见的 DIV 悬停在那里,否则这并不是一个好主意,因为它们很可能以其他令人惊讶的方式(如拦截事件)来显示它们的存在。
如果 splashToast() 同时设置 toastDIV 的 display 和 opacity 属性,CSS3 会忽略不透明度的过渡,因此,在设置 display 属性之后,该方法会将不透明度设置为 1.0,更确切地说,在大约 50ms 之后会执行该操作。
最后,当所需的显示时间过去后,splashToast() 会将 toastDIV 的 opacity 属性重新设置为 0,这会再次触发一个 0.5 秒的 CSS3 动画。在 CSS3 动画开始两秒钟后之后,splashToast() 方法会将 display 属性重新设置为 0。
解冻 Snail Bait
当 Snail Bait 在暂停后恢复播放时,它通过三秒钟的倒计时让玩家有时间做好准备,如图 3 所示:
图 3. 解冻过程中的倒计时
清单 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 中,玩家可以使用键盘来控制跑步小人,所以我会对该游戏如何处理键盘输入做一个简短的讨论,并以此来结束这篇文章。d 和 k 键可以让移动的跑步小人向左和向右移动,而 j 和 f 则分别让跑步小人跳跃和下落。图 4 显示了跳上第三个平台的轨道之后的跑步小人:
图 4. 在轨道之间跳跃之后的奔跑
您只能将键盘事件侦听器添加到 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 的现有代码中。下次再见!