使用 TogetherJS 和 CreateJS 创建多人游戏

Bubble Hell Duel 是一款多人 HTML5 空战游戏。游戏的目的是躲避对手发射的泡泡,同时还击。这款游戏主要作为学习的原型编写,源代码可以在 GitHub 上获取。你可以试着在单人或多人模式下玩这个游戏 这里。目前游戏没有声音效果,但使用了 CreateJSTogetherJS

screenshot

在这篇文章中,我想分享一些我在开发游戏时的经验。如果你同意或有其他建议,请在评论中分享你的想法。

游戏引擎

在开发 2D 游戏时,你可以自己编写引擎,也可以使用一些很棒的库。在花了一些时间研究各种选择之后,我决定使用 CreateJS。由于我有一些 Flash 的经验,CreateJS 很适合我的需求,因为学习曲线并不陡峭。我还想使用一些 Flash 动画,CreateJS 支持此功能。我将在本文后面详细介绍动画。

作为一名 C++ 开发人员,我认为 emscripten 也是一个不错的选择。它允许将 C/C++ 代码编译为 JavaScript,可以在浏览器中执行。我认为静态类型检查和编译时优化在开发大型代码库时非常有用。我之前使用过 emscripten,它运行良好,但是对于这个项目,我想要 JavaScript 快速便捷的原型设计功能。我还想扩展我的 JavaScript 知识。

我想提几个其他看起来非常有趣的库:Cocos2d-x 正在进行 emscripten 移植,并且他们已经支持 HTML5 绑定。我还喜欢 pixi.js,因为它提供 webGL 渲染器,但当浏览器不支持 webGL 时也支持 Canvas 回退。

C++ 与 JavaScript

起初,我对 JavaScript 的性能有点担心,这就是为什么我难以决定是使用 CreateJS 还是 emscripten。幸运的是,一个简单的基准测试表明,带有约 400 个球的屏幕上的简单碰撞检测算法仍然可以达到 40 多帧/秒,这足以满足我的简单实验。

作为一个使用 C++ 比 JavaScript 多的人,我喜欢我能够快速地将我的想法转化为代码并在多个浏览器上进行测试。另一方面,调试 JavaScript 并不是很舒服。C++ 编译器非常擅长指出拼写错误和其他导致运行时问题的错误。虽然“use strict”指令和其他机制(如闭包编译器)有其目的,但它们对我来说并没有多大帮助,尤其是在变量变得未定义时。相比之下,寻找错误原因可能有点困难。

例如,我遇到了以下调试问题。我使用浮点数表示坐标和其他几何值(如角度)。这些值通过 TogetherJS.send 方法传递给另一个玩家,以进行同步。

var player = { x: 10.0, y: 10.0 };
TogetherJS.send({type:'sync',x:player.x,y:player.y});
TogetherJS.hub.on('sync', function(msg){
    enemy.x = msg.x;
    enemy.y = msg.y;
});

这有效,但以这种方式发送了大量的十进制数,因此我决定降低精度。

TogetherJS.send({type:'sync', x:Math.round(player.x), y:Math.round(player.y) });

然后我认为整数对于碰撞检测可能不够准确,因此我在消息中添加了更多位数。

TogetherJS.send({type:'sync', x:player.x.toFixed(2), y:player.y.toFixed(2) });

虽然这似乎是一个合理的解决方案,但它实际上引发了一个很难找到的错误,直到我在实现了一些其他功能之后测试游戏时才注意到它。我注意到在玩游戏时,对手永远不会移动。

我花了几个小时调试才找到原因。我认为如果使用 C++,我不会犯这个错误。

如果你想看看这个错误是如何发生的,可以查看这个 jsFiddle 项目。看一下三个 canvas 标签的输出,你会注意到第三个 canvas 包含错误。这个问题的发生是因为 toFixed 返回的是字符串表示形式。

我不确定使用闭包编译器是否能避免这个问题,但我确实在 另一个项目 中发现它对优化确实有所帮助。

使用 Flash 进行动画制作

与大多数游戏一样,我想使用大量的动画。我对在 Flash 中创建动画非常熟悉,并且发现 CreateJS 支持多种方法来使用 Flash 动画并将它们呈现在 HTML5 中。CreateJS 是一套用于创建交互式 HTML5 内容的库和工具。因此,通过使用 CreateJS,我可以使用我的动画以及使用其他库进行循环处理、资源管理以及将来的声音操作。要快速了解 CreateJS,请查看这个 视频

CreateJS(现在由 Mozilla 赞助)为 Flash 动画提供了很好的支持。

使用 CreateJS 在 HTML5 中使用 Flash 动画有两种方法。第一种选择是直接以一种可以访问所有元素原始形式(包括路径、转换和补间动画)的方式导出 Flash 动画。这种方法的优点是它会生成更小的文件,CreateJS 允许你将它们传输到客户端的精灵表中,以实现更快的渲染。Adobe Flash CS6 提供了 CreateJS Toolkit 插件,允许设计人员将动画的所有内容导出到 HTML5 文件。这通常会生成一个包含所有图形和补间动画的 JavaScript 文件,一个 HTML 文件和一组图像文件。你可以在浏览器中打开 HTML 文档,并查看动画。

另一种选择是将动画导出到精灵表中,即包含所有帧的图像,以及描述每个帧的位置和大小的 JavaScript 文件。这些文件可以通过 CreateJS 中的 SpriteSheet 类轻松地集成到基于 HTML 的游戏或应用程序中。这是我在这个游戏中使用的方法。要查看我在其中使用 SpriteSheet 的代码,请查看这个 链接。如果你想了解更多关于这种方法的信息,可以查看这个 视频

我还应该注意,你可以使用名为 Zoë 的工具,直接将 Flash 动画导出到精灵表或 JSON 文件中。

marisa

上面的图像是我在游戏中使用的一个精灵表的示例,它是按照上述方法生成的。原始图像来自游戏《东方非想天则 ~ Scarlet Weather Rhapsody》,可在 http://www.spriters-resource.com 获取。

使用 TogetherJS 实现多人游戏

在我代码的第一个版本中,游戏不是多人游戏。最初,它是一个单人弹幕游戏,有一个随机在屏幕上移动的 Boss 敌人。在我屈服于毁灭性的火力之前,我撑不过 30 秒。这很有趣,我认为多人游戏会很刺激。

Together.js 发布后不久,我就听说过它。jsFiddle 项目由 Together.js 提供支持,并提供了一个令人印象深刻的协作模式。这促使我在我的游戏中使用 Together.js。Mozilla 还提供了一个默认的集线器服务器,简化了创建基于 Web 的多人游戏的过程。要详细了解 Together.js,请务必查看这篇文章 文章

将 Together.js 集成到我的游戏中既简单又舒适,因为它与其他事件分派器/侦听器框架的工作方式类似。

使用 Together.js,我能够在游戏中实现随机匹配和仅限邀请的多人模式。在设计通信协议时,我确实遇到了一些设计挑战,我必须克服它们。

首先,我没有添加代码来防止使用双向通信作弊,并假设玩家之间存在一定程度的信任。在当前的游戏设计中,所有玩家的碰撞检测都在本地完成。理论上,如果你阻止相应的訊息,就可以掩盖你受到伤害的事实。

另一个我略作处理的方面是,敌方化身的泡泡是在本地随机生成的。这意味着你的人物化身看到的泡泡不一定与你的对手看到的一样。

实际上,这些捷径都不会影响游戏的乐趣。
我确实遇到了几个关于 Together.JS 的问题或注意事项。

  • 我没有找到在 Together.js 中禁用光标更新的方法。虽然这在协作工具中很有用,但我并不需要在我的游戏中使用它。
  • 我以不对称的方式使用 Together.js,其中两个玩家都将自己视为红色的裙子化身(灵梦)。这使得将玩家更容易放置在屏幕底部,将对手放置在顶部。这也意味着,当你在对手的游戏视图中移动主要玩家时,你的移动会被视为对手的移动,反之亦然。

犯错的乐趣

游戏中有两个视觉效果,它们是意想不到的惊喜。

  • 当一轮结束并出现“你赢了”或“你输了”的消息时,时间会暂停几秒钟。这就像戏剧性的暂停。
  • 当释放蓄力攻击时,子弹会被固定,然后逐渐吹向敌人。

这些效果都没有按这种方式设计。我并不想要暂停,我希望子弹在释放后继续绕着玩家旋转。但是,我犯了错误,结果似乎比我预期的要好得多,因此它们最终被保留了下来。

结论和未来计划

学习新事物总是有趣的。我喜欢能够快速地进行原型设计和可视化。将来,我可能会为子弹幕添加更多模式,以及一些声音效果。此外,我可能还会绘制更多背景图像,或者可能对它们进行动画处理。

在开发游戏时,我确实意识到,为了获得自然直观的体验,需要比我预期的付出更多努力。这正是我在玩游戏时一直理所当然的事情。

代码是开源的,所以请随意fork和玩耍。如果您有任何关于改进游戏或现有代码的建议,请务必发表评论。

关于 Lu Wang

pdf2htmlEX 和 scanmem/GameConqueror 背后的快乐黑客。

更多由 Lu Wang 撰写的文章……


16 条评论

  1. Simon

    你说你不舒服调试你的 JavaScript,并且编译器在指出拼写错误和其他导致运行时问题的错误方面做得很好。你使用什么 IDE 来编写 JS 代码(如果有)?我发现使用像 webStorm/phpStorm 这样的好的 IDE 真的很有帮助。它们确实可以在你编码时指出很多错误。

    Simon

    不,我不为 Jetbrains 工作,我只是喜欢他们的产品 phpStorm,并且一直对它在(更好)编码方面的帮助印象深刻。

    2014 年 5 月 22 日 在 02:58

    1. Lu Wang

      嗨,Simon,感谢你的推荐!
      我碰巧有一份 IntelliJ IDEA 的副本,所以我相信 webStorm 也应该是最好的 IDE 之一。
      它看起来很有前途,我会试一试!

      大多数时候我使用 vim 进行舒适的编辑,然后严重依赖编译器来检查错别字和其他错误。它对于 C/C++ 或任何其他具有类型检查的语言都运行良好。
      但这次在这个项目中我使用的是 brackets,只是为了尝试一下新事物。它确实集成了 JSLint,但大多数时候它只是抱怨格式和空格问题,所以实际上没有太大帮助。

      请允许我描述一个我发现难以调试 JS 的常见场景。
      假设我们正在处理矩形,可以有不同的方法来表示它们。
      var rect1 = { x: 10, y: 20, width: 50, height: 100 }; var rect2 = [ 10, 20, 50, 100]; var rect3 = [ [10, 20], [60, 120] ]; var rect4 = { x1: 10, y1: 20, x2: 60, y2: 120 };

      在设计算法和框架时,我会忙于将我的想法转化为(伪)代码,并且不会过多关注实际的数据类型。
      所以我会写
      var x1 = rect1.x; var x2 = rect2.x; // 这是错误的,应该是 rect2[0]

      现在最烦人的部分来了。在 C/C++ 中,错误可以在编译阶段被检测到,在 Python 中,当我尝试访问 rect2.x 时会抛出异常。
      但在 JS 中,在 x2 实际用于渲染对象之前什么都不会发生。我发现很难找到这种问题的原因。

      想知道 IDE 如何帮助调试或防止这种情况。
      有时我认为我可能以一种受到 C++ 背景影响的错误/低效的方式编写 JS,但不确定如何防止这种情况。
      我想知道你在你的开发中是否经常遇到这种情况,你能分享一些你处理它的方法吗?
      谢谢!

      2014 年 5 月 22 日 在 04:03

    2. Luke

      webStorm/phpStorm 有很好的警告吗?

      Netbeans 在 JavaScript 方面做得很好,因为它用紫色标记意外的全局变量 - 我还没有见过其他 IDE 这样做。如果你不小心输入了“var thing = 4 [enter] another=4”而不是“var thing=4, another=5”,它会标记意外的全局变量。

      不幸的是,它似乎不喜欢“let”语句,而大多数浏览器现在都支持它。

      2014 年 5 月 22 日 在 17:51

      1. Simon Speich

        是的,它会标记全局变量,它还会标记未使用的变量。

        2014 年 5 月 22 日 在 23:46

        1. Simon Speich

          …它还会告诉你是否重新定义了变量以及更多检查:http://blog.jetbrains.com/webide/2012/11/new-javascript-inspections-and-intentions/

          现在我真的听起来像个营销人员了…

          2014 年 5 月 22 日 在 23:53

          1. Lu Wang

            哈哈,我想一个好的 IDE 应该值得拥有。

            2014 年 5 月 24 日 在 07:28

  2. Simon

    phpStorm/webStorm 肯定会对你有所帮助。当你输入 rect1. 时,它会自动建议你所有的对象属性,当你输入 rect2. 时,不会显示任何属性,但会显示数组方法。当然 rect.x 不会被报告为错误,因为它只会返回 undefined。

    你也可以使用 jsdoc 来注释你的对象,它们也会出现在自动建议中。如果你的项目很大,你也可以考虑编写测试用例。

    2014 年 5 月 22 日 在 04:24

    1. Lu Wang

      注释在一定程度上有效,我已经尝试过使用 closure compiler。但如果我必须编写完整的注释,我可能会选择 C++ + emscripten。

      通过矩形的例子,我不是说在声明之后立即访问数据字段,而是在函数调用中更可能发生。
      function getArea(rect) { return rect.width * rect.height; // 或 // return rect[2] * rect[3]; }

      注释可能是唯一的解决方案,我不确定。

      测试用例确实很有意义,但我还尝试在那里编写断言,这在一定程度上有所帮助。

      2014 年 5 月 22 日 在 06:14

  3. simon

    webstorm 真的非常智能(我猜其他 IDE 也是),你可以通过 jsdoc 做很多事情。如果你这样做

    /**
    * 一个矩形对象。
    * @class Rect // 或者有一个工厂/模块等。
    * @property {number} x
    * @property {number} y
    * @property {number} width
    * @property {number} height
    */
    var rect1 = {};

    那么它会在函数中进行自动建议,它还会标记例如 rect.rad 未定义

    /**
    *
    * @param {Rect} rect
    * @returns {number}
    */
    function getArea(rect) {
    return rect.width * rect.height;
    }

    或者你做

    /**
    * 计算面积。
    * @param {Object} rect.width
    * @param {Object} rect.height
    */
    function getArea(rect) {
    return rect.width * rect.height;
    }

    2014 年 5 月 22 日 在 08:14

    1. Lu Wang

      是的,确实如此。
      对于我这样的懒人来说,一种可能的方法是逐渐添加更多注释,直到我修复了 bug。

      2014 年 5 月 22 日 在 10:36

      1. Simon Speich

        如果你预先编写 jsdoc 似乎工作量更大,但你也会节省打字时间,因为 IDE 可以自动完成你的代码。而且我经常不记得函数参数,而 IDE 可以告诉你它们是什么,最终你也会少犯一些以后难以找到的错误。

        2014 年 5 月 22 日 在 23:44

        1. Lu Wang

          我明白了。
          也许我需要尝试一下 CoffeScript、TypeScript 和类似的语言。

          2014 年 5 月 22 日 在 23:49

  4. Ablar

    这个游戏确实为我创建了幽灵窗口。

    2014 年 5 月 22 日 在 16:32

    1. Lu Wang

      你使用什么浏览器?

      2014 年 5 月 22 日 在 23:48

      1. Ablar

        Firefox 29.0.1
        启动单人游戏并关闭标签/窗口会在 memory.ghost_window_timeout_seconds 后显示幽灵。

        2014 年 5 月 24 日 在 14:14

  5. niks

    谢谢 :D

    2014 年 5 月 24 日 在 04:01

这篇文章的评论已关闭。