体验现代 Web 技术总是很有趣的。在我的最新项目中,我提出了以下需求
- 不是一个复杂的游戏,而是一个概念验证
- 星球大战射击游戏主题
- 基于 Canvas 的渲染,但没有使用 WebGL
- 重用现有的精灵(我不是艺术家)
- 基本的音频支持
- AI/机器人
- 工作的网络多人游戏
我仅在业余时间从事这个项目;大约两周后,工作完成了:Just Spaceships! 本文描述了我开发过程中的一些决策和方法。如果您对代码感兴趣,可以查看相关的 Google Code 项目。
动画和计时
维护动画有两种基本方法:requestAnimationFrame
和 setTimeout/setInterval
。它们的优缺点是什么?
- requestAnimationFrame 指示浏览器在合适的时间(用于动画)执行回调函数。在大多数浏览器中,这意味着 60fps,但也使用其他值(某些平板电脑上的 30fps)。浏览器负责决定正确的计时;开发人员无法控制它。当页面处于后台(用户切换到另一个选项卡)时,动画可以自动减慢甚至完全停止。这种方法非常适合流畅的动画任务。
- setTimeout 指示浏览器在给定时间过去后执行下一个(动画)步骤;浏览器将尽力尽可能精确地满足此请求。当页面处于后台时,不会执行减速,这意味着此方法非常适合物理模拟。
为了解决与动画相关的事情,我创建了一个 HTML5 动画框架 (HAF),它将这两种方法结合在一起。使用两个独立的循环:一个用于模拟,另一个用于渲染。我们的对象(在 HAF 术语中称为actor)必须实现两种基本方法
/* simulation: this is called in a setTimeout-based loop */
Ship.prototype.tick = function(dt) {
var oldPosition = this._position;
this._position += dt * this._velocity;
return (oldPosition != this._position);
}
/* animation: this is called in a requestAnimationFrame loop */
Ship.prototype.draw = function(context) {
context.drawImage(this._image, this._position);
}
请注意,tick
方法返回一个布尔值:它对应于对象在模拟过程中其(视觉)位置可能会(或可能不会)发生变化的事实。HAF 会处理这个问题——在渲染时,它只会重新绘制在其 tick
方法中返回 true
的那些 actor。
渲染
Just Spaceships 广泛使用精灵;预渲染的图像,使用其 drawImage
方法绘制到画布上。我创建了一个 基准测试页面,您可以在线尝试;事实证明,使用精灵比通过 beginPath
或 putImageData
绘制东西要快得多。
大多数精灵都是动画化的:它们的源图像包含所有动画帧(例如在 CSS 精灵 中),并且一次只绘制一个帧(完整源图像的矩形部分)。因此,动画精灵的核心绘制部分如下所示
Ship.prototype.draw = function(context) {
var fps = 10; /* frames per second */
var totalFrames = 100; /* how many frames the whole animation has? */
var currentFrame = Math.floor(this._currentTime * fps) % totalFrames;
var size = 16; /* size of one sprite frame */
context.drawImage(
/* HTML <img> or another canvas */
this._image,
/* position and size within source sprite */
0, currentFrame * size, size, size,
/* position and size within target canvas */
this._x, this._y, size, size
);
}
顺便说一句:Just Spaceships! 的精灵取自 Space Rangers 2,我最喜欢的游戏。
渲染的黄金法则很直观:仅重新绘制实际更改的屏幕部分。虽然听起来很简单,但遵循它可能会变得相当具有挑战性。在 Just Spaceships 中,我采用了两种不同的方法来重新绘制精灵
- 飞船、激光和爆炸 使用称为“脏矩形”的技术;当对象发生变化(移动、动画等)时,我们仅重新绘制(
clearRect
+drawImage
)其边界框覆盖的区域。必须执行一些更深入的边界框分析,因为多个对象的边界框可能会重叠;在这种情况下,必须重新绘制所有重叠的对象。 - 星场背景 有自己的图层(画布),位于其他对象下方。完整的背景图像首先预渲染到一个大的(3000×3000px)隐藏画布中;当视口发生变化时,此大画布的子集将绘制到背景图层中。
声音
使用 HTML5 音频非常简单——至少对于桌面浏览器而言。只有两个问题需要解决
- 文件格式——格式之战 远未结束;最通用的方法是提供 MP3 和 OGG 版本。
- 在某些较慢的配置上同时播放多个声音时出现的性能问题。更具体地说,我的 Linux 机器出现了一些减速;最好提供一个选项来关闭所有音频(尚未实现)。
归根结底,音频代码看起来像这样
/* detection */
var supported = !!window.Audio && !audio_disabled_for_performance_reasons;
/* format */
var format = new Audio().canPlayType("audio/ogg") ? "ogg" : "mp3";
/* playback */
if (supported) { new Audio(file + "." + format).play(); }
多人游戏和网络模型
选择合适的网络模型对于用户体验至关重要。对于实时游戏,我决定使用客户端-服务器架构。每个客户端都维护与中央服务器的 WebSocket 连接,该服务器控制游戏流程。
在理想情况下,客户端只会发送按键,服务器会将它们重复发送给其他客户端。不幸的是,这是不可能的(由于延迟);为了维护玩家之间的一致性和同步性,有必要在服务器上运行完整的模拟,并定期通知客户端有关飞船和其他实体的各种物理属性。这种方法被称为权威服务器。
最后,客户端不能仅仅等待服务器数据包来更新其状态;即使在定期的服务器消息之间,玩家也需要游戏能够工作。这意味着浏览器也会运行其版本的模拟——并通过服务器发送的数据更正其内部状态。这被称为客户端预测。这些原则的示例实现如下所示
/* physical simulation step - client-side prediction */
Ship.prototype.tick = function(dt) {
/*
assume only these physical properties:
acceleration, velocity and position
*/
this._position += dt * this._velocity;
this._velocity += dt * this._acceleration;
}
/* "onmessage" event handler for a WebSocket data connection;
used to override our physical attributes by server-sent values */
Ship.prototype.onMessage = function(e) {
var data = JSON.parse(e.data);
this._position = data.position;
this._velocity = data.velocity;
this._acceleration = data.acceleration;
}
您可以在 Glenn Fiedler 的网站 上找到非常有用的相关阅读。
多人游戏和服务器
选择服务器端解决方案非常容易:我决定使用 v8cgi,一个基于 V8 的多用途服务器端 JavaScript 环境。它不仅比 Node 更老,而且(最重要的是)它是我自己创建和维护的;-)。
使用服务器端 JavaScript 的优势显而易见:游戏的服务器运行与在浏览器中执行的相同代码。即使 HAF 也在服务器端工作;我只是关闭了它的渲染循环,模拟按预期工作。这是客户端-服务器代码共享的一个很好的演示;在未来几年中,我们可能会越来越多地看到这种情况。
模数
为了使游戏更有趣和更具挑战性,我决定整个游戏区域应该环绕——将游戏宇宙视为一个大型环形表面。当宇宙飞船飞到最左边时,它将从右边出现;其他方向也是如此。我们如何实现这一点?让我们看看一个典型的模拟时间步长
/* Variant #1 - no wrapping */
Ship.prototype.tick = function(dt) {
this._position += dt * this._velocity;
}
为了创造环绕表面的错觉,我们需要飞船保持在给定大小的固定区域内。模数运算符可以帮助我们实现这一点
/* Variant #2 - wrapping using modulus operator */
Ship.prototype.tick = function(dt) {
var universe_size = 3000; // some large constant number
this._position += (dt * this._velocity) % universe_size;
}
但是,存在一个故障。要看到它,我们必须解决这个(小学)公式
(-7) % 5 = ?
JavaScript 的答案是 -2
;Python 的答案是 3
。谁是赢家?结果的正确性取决于定义,但就我的目的而言,正值肯定更有用。需要一个简单的技巧来更正 JavaScript 的行为
/* Returned value is always >= 0 && < n */
Number.prototype.mod = function(n) {
return ((this % n) + n) % n;
}
/* Variant #3 - wrapping using custom modulus method */
Ship.prototype.tick = function(dt) {
var universe_size = 3000; // some large constant number
this._position += (dt * this._velocity).mod(universe_size);
}
经验教训
以下是我在开发 JS 游戏(特别是 Just Spaceships 开发)时收集的一些一般技巧和提示的简短总结
- 了解这门语言!如果没有正确理解编程语言,就很难创建任何东西。
- 缺乏艺术(精灵、音乐、音效等)不应阻止您进行开发。有大量的资源可以获取这些资产;对原创艺术的需求仅在项目的后期阶段才相关。
- 使用纸和笔,卢克!你知道我最喜欢的开发工具是什么吗?一支笔和一张方格纸。
- 如果您不确定整个游戏架构,请从较小的(工作)部分开始。稍后将它们重构以形成更大的项目是自然、有用且容易的。
- 尽快收集反馈——归根结底,用户的意见才是最重要的。
待办事项
如前所述,Just Spaceships 不是一个完整的项目。肯定有改进的空间,最值得注意的是
- 通过减少发送的数据量来优化网络协议。
- 提供更多选项来优化 Canvas 性能(降低模拟 FPS、关闭背景、关闭音频等)。
- 通过实现不同的行为模型来改进 AI。
即使存在这些未完成的问题,我认为这款游戏也达到了可玩状态。我们在测试其多人游戏组件方面玩得很开心;希望您也能喜欢玩它!
关于 Ondřej Žára
Ondřej Žára 喜欢尝试与 JavaScript、HTML5 和其他 Web 技术相关的所有内容。他在 http://ondras.zarovi.cz/ 展示了他的许多项目。他目前在 Seznam.cz, a.s. 工作,主要专注于流行的制图服务 Mapy.cz 以及 HTML5 布道。他偶尔会在 Twitter 上以 @0ndras 的身份发布有关 JS 的内容。
关于 Robin Hawkes
Robin 热衷于通过代码解决问题。他是一位数字修补匠、Pusher 的开发者关系主管、Mozilla 前布道师、书籍作者,还是一位英国人。
4 条评论