游戏案例研究:Onslaught! Arena
2010年6月,我们注意到当地出版的杂志 Boing Boing 正在 举办游戏开发比赛 。 我们认为这是一个很好的借口,可以用 JavaScript 制作一个快速、简单的游戏,并且 <canvas>
,所以我们开始工作。 比赛结束后我们还有很多想法,想完成我们已经开始的事情。 这是结果的案例研究,一个名为 Onslaught 的小游戏竞技场 。
复古的像素化外观
重要的是,我们的游戏外观和感觉要像复古的 任天堂娱乐系统 游戏,因为 竞赛前提 是开发基于芯片音乐的 游戏 。 大多数游戏没有此要求,但它仍然是一种常见的艺术风格(尤其是在独立开发者中),因为它易于创建资产并且对怀旧游戏玩家具有自然吸引力。
增加像素大小可以减少图形设计工作。
考虑到这些精灵有多小,我们决定将像素加倍,这意味着 16x16 精灵现在将变成 32x32 像素,依此类推。 从一开始我们就在资产创建方面加倍努力,而不是让浏览器来做繁重的工作。 这更容易实现,但也有一些明确的外观优势。
这是我们考虑的一个场景:
<style> canvas { width: 640px; height: 320px; } </style> <canvas width="320" height="240"> Sorry, your browser is not supported. </canvas>
此方法将由 1x1 精灵组成,而不是在资产创建端将它们加倍。 从那里, CSS 将接管并调整画布本身的大小。 我们的基准测试表明,这种方法的速度大约是渲染更大(放大)图像的两倍,但不幸的是,CSS 大小调整包括抗锯齿,我们无法找到一种方法来防止这种情况。
左:像素完美的资产在 Photoshop 中翻倍。 右:CSS 调整大小添加了模糊效果。
这对我们的游戏来说是一个大问题,因为单个像素非常重要,但如果您需要调整画布大小并且抗锯齿适合您的项目,出于性能原因,您可以考虑这种方法。
有趣的画布技巧
我们都知道 <canvas>
是新热点,但有时 开发人员仍然推荐使用 DOM 。 如果您对使用哪个犹豫不决,这里有一个示例说明如何 <canvas>
为我们节省了大量时间和精力。
当敌人在 猛攻中被击中时! Arena ,它闪烁红色并短暂显示“痛苦”动画。 为了限制我们必须创建的图形数量,我们只显示朝下方向处于“痛苦”状态的敌人。 这在游戏中看起来是可以接受的,并且节省了大量创建精灵的时间。 然而,对于 Boss 怪物来说,看到一个大的精灵(64x64 像素或更大)从面向左侧或向上突然突然变为面向下方的痛苦帧是令人震惊的。
一个明显的解决方案是在八个方向的每个方向上为每个 boss 绘制一个痛苦框架,但这会非常耗时。 谢谢 <canvas>
,我们能够在代码中解决这个问题:
使用 context.globalCompositeOperation 可以产生有趣的效果。
首先我们把怪物画到一个隐藏的“缓冲区” <canvas>
,用红色覆盖它,然后将结果渲染回屏幕。 代码看起来像这样:
// Get the "buffer" canvas (that isn't visible to the user) var bufferCanvas = document.getElementById("buffer"); var buffer = bufferCanvas.getContext("2d"); // Draw your image on the buffer buffer.drawImage(image, 0, 0); // Draw a rectangle over the image using a nice translucent overlay buffer.save(); buffer.globalCompositeOperation = "source-in"; buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red buffer.fillRect(0, 0, image.width, image.height); buffer.restore(); // Copy the buffer onto the visible canvas document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);
游戏循环
游戏开发与网络开发有一些显着差异。 在 Web 堆栈中,通过事件侦听器对发生的事件做出反应是很常见的。 所以初始化代码除了监听输入事件外什么都不做。 游戏的逻辑是不同的,因为它需要不断地自我更新。 例如,如果一个玩家没有移动,那不应该阻止地精抓住他!
这是一个游戏循环的例子:
function main () { handleInput(); update(); render(); }; setInterval(main, 1);
第一个重要的区别是 handleInput
功能实际上并没有 立即做 任何事情。 如果用户在典型的网络应用程序中按下一个键,立即执行所需的操作是有意义的。 但在游戏中,事情必须按时间顺序发生才能正确流动。
window.addEventListener("mousedown", function(e) { // A mouse click means the players wants to attack. // We don't actually do that yet, but instead tell the rest // of the program about the request. buttonStates[e.button] = true; }, false); function handleInput() { // Here is where we respond to the click if (buttonStates[LEFT_BUTTON]) { player.attacking = true; delete buttonStates[LEFT_BUTTON]; } };
现在我们知道输入并可以在 update
功能,知道它将遵守其余的游戏规则。
function update() { // Check for collisions, states, whatever else is needed // If after that the player can still attack, do it! if (player.attacking && player.canAttack()) { player.attack(); } };
最后,一旦计算完所有内容,就该重绘屏幕了! 在 DOM 领域,浏览器会处理这种繁重的工作。 但是在使用的时候 <canvas>
有必要在发生某些事情时手动重绘(通常是每一帧)。
function render() { // First erase everything, something like: context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); // Draw the player (and whatever else you need) context.drawImage( player.getImage(), player.x, player.y ); };
基于时间的建模
基于时间的建模是基于自上次帧更新以来经过的时间量移动精灵的概念。 这种技术允许您的游戏尽可能快地运行,同时确保精灵以一致的速度移动。
为了使用基于时间的建模,我们需要捕获自上一帧绘制以来经过的时间。 我们需要增加我们的游戏循环 update()
功能来跟踪这个。
function update() { // NOTE: You'll need to initially seed this.lastUpdate // with the current time when your game loop starts // this.lastUpdate = Date.now(); // Calculate elapsed time since last frame var now = Date.now(); var elapsed = (now - this.lastUpdate); this.lastUpdate = now; // Do stuff with elapsed };
现在我们有了经过的时间,我们可以计算给定的精灵每帧应该移动多远。 首先,我们需要跟踪精灵对象的一些信息:当前位置、速度和方向。
var Sprite = function() { // The sprite's position relative to the top left of the game world this.position = {x: 0, y: 0}; // The sprite's direction. A positive x value indicates moving to the right this.direction = {x: 1, y: 0}; // How many pixels the sprite moves per second this.speed = 50; };
考虑到这些变量,我们将如何使用基于时间的建模来移动上述精灵类的实例:
// Determine how far this sprite will move this frame var distance = (sprite.speed / 1000) * elapsed; // Apply the movement distance to the sprite's current position // taking into account its direction sprite.position.x += (distance * sprite.direction.x); sprite.position.y += (distance * sprite.direction.y);
请注意, direction.x
和 direction.y
值应该 标准化 ,这意味着它们应该始终介于 -1
和 1
.
控件
控制可能是最大的绊脚石 在开发Onslaught ! 竞技场 。 第一个演示只支持键盘; 玩家用箭头键在屏幕上移动主角,然后用空格键朝他面对的方向开火。 虽然有些直观且易于掌握,但这使得游戏在更困难的级别上几乎无法玩。 坏人之间穿梭。 在任何给定时间都有数十个敌人和射弹向玩家飞来,因此必须能够在向任何方向 射击时在
为了与同类游戏进行比较,我们添加了鼠标支持来控制瞄准十字线,角色将使用它来瞄准他的攻击。 角色仍然可以用键盘移动,但在这个改变之后他可以同时向任何 360 度方向开火。 铁杆玩家很喜欢这个功能,但不幸的是,它有让触控板用户感到沮丧的副作用。
Onslaught 中的旧控件或 如何玩 模式! 竞技场。
为了适应触控板用户,我们带回了箭头键控件,这次允许在按下的方向上开火。 虽然我们觉得我们正在迎合所有类型的玩家,但我们也在不知不觉中为我们的游戏引入了太多的复杂性。 令我们惊讶的是,我们后来听说一些玩家不知道用于攻击的可选鼠标(或键盘!)控件,尽管有教程模式,但在很大程度上被忽略了。
玩家大多忽略教程覆盖; 他们宁愿玩耍和玩得开心!
我们也很幸运有一些欧洲粉丝,但我们从他们那里听到了挫败感,他们可能没有典型的 QWERTY 键盘并且无法使用 WASD 键进行方向移动。 左撇子球员也表达了类似的抱怨。
通过我们实施的这种复杂的控制方案,还存在在移动设备上播放的问题。 事实上,我们最常见的要求之一就是制作《 猛攻! Arena 在 Android、iPad 和其他触摸设备(没有键盘)上可用。 HTML5 的核心优势之一是其可移植性,因此将游戏移植到这些设备上绝对是可行的,我们只需要解决许多问题(最显着的是控制和性能)。
为了解决这些问题,我们开始玩一种仅涉及鼠标(或触摸)交互的单输入游戏方法。 玩家点击或触摸屏幕,主角走向按下的位置,自动攻击最近的坏人。 代码看起来像这样:
// Find the nearest hostile target (if any) to the player var player = this.getPlayerObject(); var hostile = this.getNearestHostile(player); if (hostile !== null) { // Found one! Shoot in its direction var shoot = hostile.boundingBox().center().subtract( player.boundingBox().center() ).normalize(); } // Move towards where the player clicked/touched var move = this.targetReticle.position.clone().subtract( player.boundingBox().center() ).normalize(); var distance = this.targetReticle.position.clone().subtract( player.boundingBox().center() ).magnitude(); // Prevent jittering if the character is close enough if (distance < 3) { move.zero(); } // Move the player if ((move.x !== 0) || (move.y !== 0)) { player.setDirection(move); }
消除必须瞄准敌人的额外因素可以使游戏在某些情况下更容易,但我们认为让玩家的事情变得更简单有很多好处。 出现了其他策略,例如必须将角色定位在靠近危险敌人的位置以瞄准他们,并且支持触摸设备的能力是无价的。
声音的
时面临的最大问题之一 在控制和性能方面,这是我们在开发《猛攻》 ! Arena 是 HTML5 的 <audio>
标签。 最糟糕的方面可能是延迟:在几乎所有浏览器中,调用之间都有延迟 .play()
和实际播放的声音。 这可能会破坏游戏玩家的体验,尤其是在玩像我们这样的快节奏游戏时。
其他问题包括 未能触发进度事件 ,这可能会导致游戏的加载流程无限期挂起。 出于这些原因,我们采用了所谓的 fall-forward 方法,如果 Flash 加载失败,我们将切换到 HTML5 音频。 代码看起来像这样:
/* This example uses the SoundManager 2 library by Scott Schiller: http://www.schillmania.com/projects/soundmanager2/ */ // Default to sm2 (Flash) var api = "sm2"; function initAudio (callback) { switch (api) { case "sm2": soundManager.onerror = (function (init) { return function () { api = "html5"; init(callback); }; }(arguments.callee)); break; case "html5": var audio = document.createElement("audio"); if ( audio && audio.canPlayType && audio.canPlayType("audio/mpeg;") ) { callback(); } else { // No audio support :( } break; } };
对于游戏来说,支持不能播放 MP3 文件的浏览器(例如 Mozilla Firefox)也可能很重要。 如果是这种情况,可以检测到支持并切换到类似 Ogg Vorbis 的东西,代码如下:
/* Note: you could instead use "new Audio()" here, but the client will throw an error if it doesn't support Audio, which makes using "document.createElement" a safer approach. */ var audio = document.createElement("audio"); if (audio && audio.canPlayType) { if (!audio.canPlayType("audio/mpeg;")) { // Here you know you CANNOT use .mp3 files if (audio.canPlayType("audio/ogg; codecs=vorbis")) { // Here you know you CAN use .ogg files } } }
保存数据
没有高分就无法进行街机式射击! 我们知道我们需要一些游戏数据来保存,虽然我们可以使用 cookie 之类的老东西,但我们想深入研究有趣的新 HTML5 技术。 当然不乏选项,包括本地存储、会话存储和 Web SQL 数据库。
保存高分,以及击败每个老板后您在游戏中的位置。
我们决定使用 localStorage
因为它是新的、很棒的并且易于使用。 它支持保存基本的键/值对,这是我们简单游戏所需要的。 这是一个如何使用它的简单示例:
if (typeof localStorage == "object") { localStorage.setItem("foo", "bar"); localStorage.getItem("foo"); // Value is "bar" localStorage.removeItem("foo"); localStorage.getItem("foo"); // Value is now null }
有一些陷阱需要注意。 无论您传入什么,值都存储为字符串,这可能会导致一些意想不到的结果:
localStorage.setItem("foo", false); typeof localStorage.getItem("foo"); // Value is "false" (a string literal) if (localStorage.getItem("foo")) { // It's true! } // Don't pass objects into setItem localStorage.setItem("bar", {"key": "value"}); localStorage.getItem("bar"); // Value is "[object Object]" (a string literal) // JSON stringify and parse when dealing with localStorage localStorage.setItem("json", JSON.stringify({"key": "value"})); typeof localStorage.getItem("json"); // string JSON.parse(localStorage.getItem("json")); // {"key": "value"}
概括
HTML5 非常好用。 大多数实现处理游戏开发人员需要的一切,从图形到保存游戏状态。 虽然有一些成长的烦恼(例如 <audio>
tag woes),浏览器开发人员正在迅速行动,并且随着事情的发展,基于 HTML5 的游戏的未来看起来一片光明。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 提高 HTML5 应用程序的性能
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论