在本系列中,HTML5 专家 David Geary 将向您展示如何逐步实现一个 HTML5 2D 视频游戏。第一期的文章将向您展示一个已完成的游戏,然后引导您从头开始实现它。如果您曾经想要创建一个 HTML5 游戏,但又没时间掌握所有细节,那么本系列非常适合您。
查看本系列更多内容 1 评论:
2012 年 10 月 18 日
软件开发的优势在于:在合理的范围内让任何您能想象到的事物在屏幕上变成现实。不像其他领域受一些物理条件的束缚,软件开发人长期以来一直使用图形化 API 和 UI 工具包来实现富有创造性和引人注目的应用程序。最富有创造性的软件开发大概是游戏编程;比起让游戏景愿变成现实,优秀的创意就可使您通过少许努力便可获得更多的收益。
然而,有益的并不意味着容易的;事实上,恰恰相反。实现游戏(特别是视频游戏)需要对编程有着非凡的理解,能够很好地把握抓图和动画,还需要掌握大量数学知识、艺术和创造力的结合使用。而这仅仅只是一个开头,成功的游戏开发人员需要花费大量时间来雕琢其游戏,改良游戏玩法和图像,除此之外,还要实现很多与游戏玩法无关的方面,比如记分牌、指令、生命和级别之间的动画以及残局序列 (endgame sequences)。
本系列旨在向您显示如何实现一个 HTML5 视频游戏,这样您就可以开始创建自己的游戏了。
在本系列中,我首先会向您展示如何使用 HTML5 Canvas API 实现一个平台视频游戏。该游戏是 Snail Bait,如图 1 所示。您可以在线播放该游戏;请参阅 参考资料,以获取该游戏的链接。确保您的浏览器中有 Canvas 硬件加速器(最近刚刚在大多数浏览器上实现,包括 Chrome V18 以上的版本);否则,Snail Bait 性能会大幅降低(参阅 HTML5 Canvas 性能 侧栏,获取有关的详细信息。)
Snail Bait 是一款经典的平台游戏,游戏的主人公,我通常将其简称为跑步小人,在水平移动的浮动平台奔跑跳跃。跑步小人的最终目标是到达一个有规律地跳动的平台,并在该级别结束时获得一枚黄金纽扣。图 1 中显示了跑步小人、有规律地跳动的平台和黄金纽扣。
玩家使用键盘控制跑步小人:d 向左移动,k 向右移动,j 或 f 跳起,p 暂停游戏。
游戏开始时,您有 3 条生命。游戏画布左上方会显示代表剩余生命数的跑步小人图标,正如您在 图 1 中所看到的那样。在跑步小人达到该级别终点的过程中,她必须避开一些坏家伙(蜜蜂、蝙蝠和蜗牛),同时试着抓住贵重物品,比如硬币、红宝石和蓝宝石。如果跑步小人碰到坏家伙,就会流血并失去一条生命,并且您必须回到该级别的起点。当跑步小人碰到 “好人” 时,您的积分将会增加,并会听到一个令人愉悦的声音。
坏家伙通常只是在附近徘徊,等待跑步小人撞上它。但是,蜗牛会定期发射蜗牛炸弹(图 1 中心显示的银球),炸弹和其他坏家伙一样,碰到跑步小人就时会炸毁她。
结束游戏的方法有两种:您失去 3 次生命,或者您到达有规律地跳动的平台(击中黄金纽扣就会有奖励积分)。不管采用哪种方式,游戏结束积分均如图 2 所示:
您在 图 1 中无法看到的是:所有一切都在不停的滚动,除了跑步小人,她的运动由您来控制。因为有滚动,所以将 Snail Bait 归类为 side-scroller 平台游戏。虽然这不是游戏中惟一的动作,但它引导我发现了 sprite 及其行为。
回页首
除了背景之外,Snail Bait 中的一切都是 sprite,一个 sprite 是一个您可以在游戏画布上绘画的对象。sprite 不是 Canvas API 的一部分,但是易于实现,该游戏的 sprite 是:
除了从右向左滚动之外,几乎所有的游戏 sprite 都有其自己独立的行为。例如,红宝石和蓝宝石以不同的速率上下跳动,纽扣和蜗牛沿着它们所在的平台来回徘徊。
独立运动是诸多 sprite 行为之一。Sprite 可以具有与运动无关的其他行为;例如,除了上下跳动之外,红宝石和蓝宝石还会闪闪发光。
每个 sprite 都有一个行为阵列。行为只是一个具有 execute()
方法的对象。在每个动画帧中,游戏都会调用每个行为的 execute()
方法。在该方法中,行为以某种方式(具体情况视游戏条件而定)操控其相关 sprite。例如,您按下 k 键让跑步小人向右移动,跑步小人的横向移动行为随后会在每个动画帧中将跑步小人向右移动,直至更改其方向。另一个行为是在正确位置跑动,该行为会定期改变跑步小人图像,使得跑步小人看起来像是在正确位置上跑动。这两个行为组合在一起使得跑步小人看起来似乎正在向左或向右跑动。
表 1 列出了游戏的 sprite 及其相应行为:
Sprite | 行为 |
---|---|
平台 |
|
跑步小人 |
|
蜜蜂和蝙蝠 |
|
纽扣 |
|
硬币、红宝石和蓝宝石 |
|
蜗牛 |
|
蜗牛炸弹 |
|
本系列的后续文章将深入研究 sprite 和 sprite 行为。现在,为了向您提供一个高度概述,清单 1 展示了该游戏如何创建 runner
sprite:
var runInPlace = { // Just an object with an execute method execute: function (sprite, time, fps) { // Update the sprite's attributes based on the time and frame rate } }; var runner = new Sprite('runner', // name runnerPainter, // painter [ runInPlace,... ]); // behaviors
runInPlace
对象是在包含其他行为的数组中定义的,并给传递给跑步小人 sprite 的构造函数。在运行的时候,该游戏调用每个动画帧的 runInPlace
对象的 execute()
方法。
回页首
我将在本系列中讨论游戏开发最佳实践,在这里,从特定于 HTML5 的五个最佳实践开始:
我将在本系列的后续文章中对这 5 个最佳实践进行详细讨论;现在我们来快速浏览其中的每个最佳实践。
如果在浏览器中运行某个 HTML5 游戏,当您将焦点转移到另一个选项卡或者浏览器窗口时,大多数浏览器会固定游戏动画运行的帧速率,以节省 CPU 和电池源等资源。反过来,帧速率固定总是对碰撞检测算法造成严重干扰,该算法可以使得该游戏以最小帧速率运行。为了避免帧速率限制以及随之而来的碰撞检测崩溃,当窗口中失去焦点时,应该自动停止游戏。
当您的游戏窗口再次获得焦点时,给用户几秒钟的准备时间来重启游戏是一个不错的主意。窗口再次获得焦点时,Snail Bait 使用了 3 秒钟的倒计时,如 图 3 所示:
图 4 是加载游戏后的屏幕截图:
图 4 中有两件事需要注意。第一个值得注意的是一个 toast,它是一些向玩家临时显示的事物,是可见的,比如说 Good luck!。当加载游戏时,该 toast 会先淡入,然后在五秒后淡出。第二,注意游戏画布之下的复选框(用于声音和音乐)和操作说明(告诉您哪些击键执行哪些功能)。游戏开始时,复选框和操作说明都是完全透明的,正如 图 4 所示;玩家开始之后,这些元素慢慢淡出,最后几乎看不见(如 图 3 所示),这样就不会分散玩家的注意力。
Snail Bait 通过 CSS3 变换使元素变暗并使 toasts 消失。
和那些运行在严格控制环境下的控制台游戏不一样,HTML5 游戏在一个高度可变的、不可预测的混乱环境中运行。对于您的游戏而言,当玩家在另一个选项卡上播放 YouTube 视频时,或者在过度使用 CPU 或 GPU 时,游戏运行会变得异常慢,这屡见不鲜。或者也有可能您的玩家使用了一个跟不上节奏的浏览器。
作为一个游戏开发者,您必须能够预见这些不幸的事件并作出相应的反应。Snail Bait 不断监控帧速率,当它检测到在很多秒内帧速率多次跌破特定阈值的时候,会显示运行太慢,如图 5 所示:
几乎所有的成功游戏都融合了一些社交因素,比如在 Twitter 或 Facebook 上发布得分。当 Snail Bait 玩家单击游戏结尾出现的 Tweet my score 链接时(参见 图 2),Snail Bait 会在另一个选项卡上打开 Twitter 链接并自动创建一条微博发布得分情况,如图 7 所示:
现在,您对该游戏有了一个更高层次的理解,是时候看一些代码了。
回页首
Snail Bait 是通过 HTML、CSS 和 JavaScript 实现的;但是,正如您从 Snail Bait code statistics 侧栏中看到的,大多数代码是 JavaScript。事实上,该系列的其余部分主要关注 JavaScript,只是偶尔谈及 HTML 和 CSS3。
图 8 显示了适合该游戏的 HTML 元素及其相应 CSS,忽略了其他元素的 HTML 和 CSS,比如 toasts 和积分:
点击查看大图
CSS 大多都很不起眼,除了我在 图 8 中高亮显示的那几个关注的属性。首先,我将 wrapper
元素的 margin
属性设置为 0 auto
,这意味着包装器(以及其中包含的一起)在窗口中水平居中。其次,lives
和 sound-and-music
元素有一个 absolute
位置。如果采用了默认位置,即 relative
,那么这些 relative
会扩展到与画布一样宽,它们的邻居(分别是记分和操作说明)将在它们之下运动。最后,keys
和 explanation
CSS 类有一个 display
属性 inline
,用于将相关元素放在同一行上。
清单 2 显示了 图 8 中的 CSS:
#arena { text-align: center; padding: 5px; width: 805px; height: 445px; } #copyright { margin-top: -35px; float: right; margin-right: 12px; padding: 2px; color: blue; text-shadow: 1px 1px 1px rgba(255,255,255,0.7); font-size: 0.8em; } .explanation { color: #ff0; text-shadow: 1px 1px 1px rgba(0,0,0,1.0); display: inline; margin-top: 5px; padding-right: 5px; padding-left: 5px; padding-bottom: 2px; } #game-canvas { border: 2px inset rgba(0,0,80,0.62); -webkit-box-shadow: rgba(0,0,0,0.5) 8px 8px 16px; -moz-box-shadow: rgba(0,0,0,0.5) 8px 8px 16px; -o-box-shadow: rgba(0,0,0,0.5) 8px 8px 16px; box-shadow: rgba(0,0,0,0.5) 8px 8px 16px; } #instructions { height: 30px; margin-right: 8px; padding-top: 6px; padding-left: 25px; -webkit-transition: opacity 2s; -moz-transition: opacity 2s; -o-transition: opacity 2s; transition: opacity 2s; color: #ff0; font-size: 1.05em; opacity: 1.0; } .keys { color: blue; text-shadow: 1px 1px 1px rgba(255,255,0,1.0); background: rgba(0,0,0,0.1); border: thin solid rgba(0,0,0,0.20); border-radius: 5px; margin-left: 10px; padding-right: 10px; padding-left: 10px; padding-bottom: 5px; display: inline; } #sound-and-music { position: absolute; top: 495px; margin-left: 10px; color: #ff0; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); background: rgba(0,0,0,0.1); border-radius: 5px; border: thin solid rgba(0,0,0,0.20); padding-top: 2px; padding-bottom: 2px; z-index: 1; } #wrapper { margin: 0 auto; margin-top: 20px; padding: 5px; width: 817px; height: 520px; }
正如您在清单 3 中看到的(清单 3 列出了 图 8 中显示的 HTML),该游戏的 HTML 是一堆 DIV
和画布,以及少许图像和几个复选框:
<!DOCTYPE html> <html> <!-- Head........................................................--> <head> <title>Snail Bait</title> </head> <!-- Body........................................................--> <body> <!-- Wrapper..................................................--> <div id='wrapper'> <!-- Header.................................................--> <div id='header'> <div id='lives'> <img id='life-icon-left' src='images/runner-small.png'/> <img id='life-icon-middle' src='images/runner-small.png'/> <img id='life-icon-right' src='images/runner-small.png'/> </div> <div id='score'>0</div> <div id='fps'></div> </div> <!-- Arena..................................................--> <div id='arena'> <!-- The game canvas.....................................--> <canvas id='game-canvas' width='800' height='400'> Your browser does not support HTML5 Canvas. </canvas> <!-- Sound and music.....................................--> <div id='sound-and-music'> <div class='checkbox-div'> Sound <input id='sound-checkbox' type='checkbox' checked/> </div> <div class='checkbox-div'> Music <input id='music-checkbox' type='checkbox' checked/> </div> </div> <!-- Instructions........................................--> <div id='instructions'> <div class='keys'> d / k <div class='explanation'> move left/right </div> </div> <div class='keys'> f / j <div class='explanation'> jump </div> </div> <div class='keys'> p <div class='explanation'> pause </div> </div> </div> <!-- Copyright...........................................--> <div id='copyright'> ©2012 David Geary</div> </div> </div> <!-- JavaScript................................................--> <script src='js/stopwatch.js'></script> <script src='js/animationTimer.js'></script> <script src='js/sprites.js'></script> <script src='js/requestNextAnimationFrame.js'></script> <script src='js/behaviors/bounce.js'></script> <script src='js/behaviors/cycle.js'></script> <script src='js/behaviors/pulse.js'></script> <script src='game.js'></script> </body> </html>
canvas
元素是所有活动发生的地方。此画布有一个 2D 环境和一个功能强大的 API 来实现 2D 游戏,以及其他。canvas
元素中的文本是可退回文本,只有当该浏览器不支持 HTML5 Canvas 才显示。
关于该游戏 HTML 和 CSS 最后一个注意事项:注意画布的宽度和高度是通过 canvas
元素的 width
和 height
属性来指定的。这些属性都是与 canvas
元素的大小以及 包含在此元素中的绘图表面的大小相关的。
另一方面,使用 CSS 设置 canvas
元素的宽度和高度,仅设置该元素的大小。绘图表面仍然保持其默认宽度和高度,分别为 300 和 150 像素。这意味着很可能 canvas
元素大小与绘图表面大小不匹配,而那时浏览器可能会调整绘图表面大小以适应该元素。大多数时间这会产生不利影响,因此绝不使用 CSS 设置 canvas
元素大小将是一个不错的主意。
正如热映的电影 Pulp Fiction,您已经看到了故事结局。现在我们回到开始。
回页首
图 9 显示了该游戏的起点,仅绘制了背景、平台和跑步小人。从一开始,平台和跑步小人都不是 sprite,相反是游戏直接绘制的。参阅 下载 部分获取创建背景和跑步小人的代码。
清单 3 列出了该游戏 HTML 的起点,这只是 清单 2 的一个缩小版:
<!DOCTYPE html> <html> <!-- Head.........................................................--> <head> <title>Snail Bait</title> <link rel='stylesheet' href='game.css'/> </head> <!-- Body.........................................................--> <body> <!-- Wrapper...................................................--> <div id='wrapper'> <!-- Header.................................................--> <div id='header'> <div id='score'>0</div> </div> <!-- Arena..................................................--> <div id='arena'> <!-- The game canvas.....................................--> <canvas id='game-canvas' width='800' height='400'> Your browser does not support HTML5 Canvas. </canvas> </div> </div> <!-- JavaScript................................................--> <script src='game.js'></script> </body> </html>
清单 4 显示了 JavaScript:
// --------------------------- DECLARATIONS ---------------------------- var canvas = document.getElementById('game-canvas'), context = canvas.getContext('2d'), // Constants............................................................ PLATFORM_HEIGHT = 8, PLATFORM_STROKE_WIDTH = 2, PLATFORM_STROKE_STYLE = 'rgb(0,0,0)', STARTING_RUNNER_LEFT = 50, STARTING_RUNNER_TRACK = 1, // Track baselines // // Platforms move along tracks. The constants that follow define // the Y coordinate (from the top of the canvas) for each track. TRACK_1_BASELINE = 323, TRACK_2_BASELINE = 223, TRACK_3_BASELINE = 123, // Images background = new Image(), runnerImage = new Image(), // Platforms // // Each platform has its own fill style, but the stroke style is // the same for each platform. platformData = [ // One screen for now // Screen 1....................................................... { left: 10, width: 230, height: PLATFORM_HEIGHT, fillStyle: 'rgb(255,255,0)', opacity: 0.5, track: 1, pulsate: false, }, { left: 250, width: 100, height: PLATFORM_HEIGHT, fillStyle: 'rgb(150,190,255)', opacity: 1.0, track: 2, pulsate: false, }, { left: 400, width: 125, height: PLATFORM_HEIGHT, fillStyle: 'rgb(250,0,0)', opacity: 1.0, track: 3, pulsate: false }, { left: 633, width: 100, height: PLATFORM_HEIGHT, fillStyle: 'rgb(255,255,0)', opacity: 1.0, track: 1, pulsate: false, }, ]; // ------------------------- INITIALIZATION ---------------------------- function initializeImages() { background.src = 'images/background_level_one_dark_red.png'; runnerImage.src = 'images/runner.png'; background.onload = function (e) { startGame(); }; } function drawBackground() { context.drawImage(background, 0, 0); } function calculatePlatformTop(track) { var top; if (track === 1) { top = TRACK_1_BASELINE; } else if (track === 2) { top = TRACK_2_BASELINE; } else if (track === 3) { top = TRACK_3_BASELINE; } return top; } function drawPlatforms() { var pd, top; context.save(); // Save context attributes on a stack for (var i=0; i < platformData.length; ++i) { pd = platformData[i]; top = calculatePlatformTop(pd.track); context.lineWidth = PLATFORM_STROKE_WIDTH; context.strokeStyle = PLATFORM_STROKE_STYLE; context.fillStyle = pd.fillStyle; context.globalAlpha = pd.opacity; // If you switch the order of the following two // calls, the stroke will appear thicker. context.strokeRect(pd.left, top, pd.width, pd.height); context.fillRect (pd.left, top, pd.width, pd.height); } context.restore(); // Restore context attributes } function drawRunner() { context.drawImage(runnerImage, STARTING_RUNNER_LEFT, calculatePlatformTop(STARTING_RUNNER_TRACK) - runnerImage.height); } function draw(now) { drawBackground(); drawPlatforms(); drawRunner(); } function startGame() { draw(); } // Launch game initializeImages();
JavaScript 访问 canvas
元素,随后获取画布的 2D 上下文的引用。然后,该代码使用上下文的 drawImage()
方法来绘制背景和跑步小人图像。在本例中,我将使用 drawImage()
的由 3 个参数组成的变体,在画布中的特定 (x,y)
目的地上绘制图像。
在设置上下文的行宽度、描边风格、填充风格和全局 alpha 属性之后 ,drawPlatforms()
函数会通过对矩形进行描边和填充来绘制平台。请注意对 context.save()
和 context.restore()
的调用:这些调用之间的属性设置都是临时的。我会在本系列的下一篇文章中讨论这些方法。
游戏从加载背景图像时开始。现在,开始开发游戏只需绘制背景、sprite 和跑步小人即可。下一个挑战是让这些静态图像动起来。
回页首
在本系列的下一篇文章中,我将从画布上下文的 2D API 的概述开始,然后讨论动画,通过滚动背景让一切都动起来。您将了解如何实现视差,让平台看起来比背景更近一些,还将了解如何确保您的 sprite 以固定速率运动,而不管动画的帧速率如何。下次再见!
回页首
描述 | 名字 | 大小 |
---|---|---|
Snail Bait 背景和跑步小人的代码 | j-html5-game1.zip | 718KB |