Emscripten 是一个开源编译器,它将 C/C++ 源代码编译为高度可优化的 asm.js JavaScript 子集。这使得可以在网页浏览器中运行最初为桌面环境编写的程序。
将您的游戏移植到 Emscripten 有几个优势。最重要的是它能够覆盖更广泛的潜在用户群。Emscripten 游戏可以在任何现代网页浏览器中运行。无需安装程序或设置 - 用户只需打开一个网页。游戏数据在浏览器缓存中的本地存储意味着游戏只需在更新后重新下载。如果您实现了一个基于云的用户数据存储系统,用户可以在任何有浏览器的计算机上无缝地继续游戏。
更多信息请访问
虽然 Emscripten 对可移植 C/C++ 代码的支持非常好,但也有一些需要考虑的地方。我们将在本文中讨论这些问题。
第一部分:准备工作
将我的游戏移植到 Emscripten 是否可行?如果可行,难度如何?首先考虑 Emscripten 施加的以下限制
- 禁止使用闭源第三方库
- 禁止使用线程
然后,已经拥有以下内容中的某些内容
- 使用 SDL2 和 OpenGL ES 2.0 进行图形渲染
- 使用 SDL2 或 OpenAL 进行音频处理
- 现有的跨平台支持
将使移植任务变得更加轻松。接下来我们将更详细地研究这些要点。
需要检查的第一件事
如果您正在使用任何没有源代码的第三方库,那么您基本上就无能为力了。您必须重写代码以不再使用它们。
大量使用线程也会成为问题,因为 Emscripten 目前不支持它们。虽然有 Web Workers,但它们与其他平台上的线程不同,因为没有共享内存。因此,您必须禁用多线程。
SDL2
甚至在接触 Emscripten 之前,您可以在正常的开发环境中做一些事情。首先,您应该使用 SDL2。SDL 是一个库,它负责处理特定于平台的事情,例如创建窗口和处理输入。一个不完整的 SDL 1.3 移植版本与 Emscripten 一起提供,并且有一个 完整的 SDL2 移植版本 正在开发中。它将很快合并到上游。
FTL 中的太空战斗。
OpenGL ES 2.0
第二件事是使用 OpenGL ES 2.0。如果您的游戏使用 SDL2 渲染接口,那么这已经为您完成了。如果您使用的是 Direct3D,您首先需要创建一个 OpenGL 版本的游戏。这就是从一开始就进行跨平台支持是一个好主意的原因。
一旦您拥有了桌面 OpenGL 版本,您就需要创建一个 OpenGL ES 版本。ES 是完整 OpenGL 的一个子集,其中一些功能不可用,并且还有一些额外的限制。至少 NVidia 驱动程序,以及可能是 AMD 也支持在桌面上创建 ES 上下文。这样做的好处是您可以使用现有的环境和调试工具。
您应该尽可能避免使用已弃用的 OpenGL 固定函数管道。虽然 Emscripten 对此有一些支持,但它可能无法很好地工作。
在这个阶段,您可能会遇到一些问题。第一个是缺乏扩展支持。着色器可能也需要针对 Emscripten 进行重写。如果您使用的是 NVidia,请添加 #version 行以触发更严格的着色器验证。
GLSL ES 需要为浮点数和整数变量指定精度限定符。NVidia 在桌面上接受这些限定符,但大多数其他 GL 实现不接受,因此您最终可能会得到两组不同的着色器。
OpenGL 入口点名称在 GL ES 和桌面之间有所不同。GL ES 不需要 GLEW 这样的加载器,但如果您使用任何扩展,您可能仍然需要手动检查它们。还要注意,桌面上的 OpenGL ES 比 WebGL 更宽松。例如,WebGL 对 glTexImage 参数和 glTexParameter 采样模式更加严格。
GL ES 可能不支持多个渲染目标。如果您使用的是模板缓冲区,那么您也必须使用深度缓冲区。您必须使用顶点缓冲区对象,而不是用户模式数组。此外,您不能将索引缓冲区和顶点缓冲区混合到同一个缓冲区对象中。
对于音频,您应该使用 SDL2 或 OpenAL。一个潜在的问题是,Emscripten OpenAL 实现可能比桌面需要更多更大的声音缓冲区,以避免声音断断续续。
跨平台支持
如果您的项目具有跨平台支持,特别是对于移动平台(Android、iOS),那就更好了。有两个原因。首先,WebGL 本质上是 OpenGL ES 而不是桌面 OpenGL,因此您的大部分 OpenGL 工作已经完成了。其次,由于移动平台使用 ARM 架构,因此大多数特定于处理器的問題已经解决了。特别重要的是内存对齐,因为 Emscripten 不支持从内存中进行未对齐的加载。
在您整理好 OpenGL 之后(或者如果您有多个人,甚至可以与之同时进行),您应该将您的游戏移植到 Linux 和/或 OS X。同样,也有几个原因。第一个原因是 Emscripten 基于 LLVM 和 Clang。如果您的代码是用 MSVC 编写的并经过测试,那么它可能包含非标准结构,MSVC 会接受这些结构,但其他编译器不会接受。此外,不同的优化器可能会暴露错误,这些错误在桌面上的调试要比在浏览器上容易得多。
FTL Emscripten 版本主菜单。请注意缺少的“退出”按钮。UI 与 iPad 版本的 UI 类似。
Ryan Gordon 在 Steam Dev Days 演讲中 提供了将 Windows 游戏移植到 Linux 的良好概述。
如果您使用的是 Windows,您也可以使用 MinGW 进行编译。
有用的调试工具
UBSan
将游戏移植到 Linux 的第二个原因是能够使用一些有用的工具。其中首当其冲的是未定义行为分析器(UBSan)。它是 Clang 编译器的一项功能,它在您的代码中添加运行时检查以捕获 C/C++ 未定义的行为。其中最有用的是未对齐加载检查。C/C++ 标准规定,在访问指针时,它必须正确对齐。不幸的是,基于 x86 的处理器会执行未对齐的加载,因此大多数现有代码都没有针对此进行检查。基于 ARM 的处理器通常会在发生这种情况时使您的程序崩溃。这就是为什么移动移植版本好的原因。在 Emscripten 中,未对齐的加载不会导致崩溃,而是会静默地返回错误的结果。
UBSan 也在 GCC 4.9 及更高版本中可用,但不幸的是,未对齐加载分析器仅包含在即将发布的 5.0 版本中。
AddressSanitizer
Clang(和 GCC)中的第二个有用工具是 AddressSanitizer。这是一个运行时检查器,它验证您的内存访问。在任何平台上,读取或写入分配的缓冲区之外都会导致崩溃,但在 Emscripten 上,这个问题要严重一些。本机二进制文件具有较大的地址空间,其中包含大量的空闲空间。无效读取,尤其是仅仅略微偏离的读取,可能会命中一个有效地址,因此不会立即崩溃,甚至根本不会崩溃。在 Emscripten 中,地址空间更加“密集”,因此任何无效访问都可能命中某个关键位置,甚至可能完全超出分配的地址空间。这将触发一个不显眼的崩溃,并且可能非常难以调试。
Valgrind
第三个工具是 Valgrind。它是一个运行时工具,它运行未经工具化的二进制文件,并检查它们的各种属性。对于我们的目的,最有用的是 memcheck 和 massif。Memcheck 是一个类似于 AddressSanitizer 的内存验证器,但它捕获了一组略有不同的问题。它还可以用来查明内存泄漏。Massif 是一个内存分析器,它可以回答“为什么我使用这么多内存?”这个问题。这很有用,因为 Emscripten 也是一个比桌面甚至移动设备更加受内存限制的平台,并且没有内置的内存分析工具。
Valgrind 还有一些其他检查器,例如 DRD 和 Helgrind,它们检查多线程问题,但由于 Emscripten 不支持线程,因此我们在这里不讨论它们。尽管如此,它们非常有用,因此如果您在桌面进行多线程,您确实应该使用它们。
Valgrind 在 Windows 上不可用,而且可能永远不会可用。这本身就应该是将您的游戏移植到其他平台的原因。
第三方库
大多数游戏都使用许多第三方库。希望您已经摆脱了所有闭源库。但即使是开源库通常也以预先编译的库的形式提供。大多数这些库在 Emscripten 上并不容易获得,因此您需要自己编译它们。此外,Emscripten 对象格式基于 LLVM 字节码,该字节码不能保证稳定。任何预编译的库在未来的 Emscripten 版本中可能不再有效。
虽然 Emscripten 对动态链接有一些支持,但它并不完整或没有得到很好的支持,应该避免使用。
解决这些问题的最佳方法是在标准构建过程中构建库并静态链接它们。虽然将库捆绑到存档文件中并在链接步骤中包含这些存档文件有效,但您可能会遇到意想不到的问题。此外,如果您将所有源代码作为构建系统的一部分,那么更改编译器选项会变得更加容易。
完成所有这些后,您应该实际尝试使用 Emscripten 进行编译。如果您使用的是 MS Visual Studio 2010,则可以使用一个集成模块。如果您使用的是 cmake,Emscripten 附带一个包装器(emcmake),它应该自动配置您的构建。
如果您使用的是其他构建系统,则需要自行进行设置。通常,CC=emcc
和 CXX=em++
应该可以解决问题。您可能还需要删除特定于平台的选项,例如 SSE 等。
第二部分:Emscripten 本身
所以现在它可以链接了,但是当您在浏览器中加载它时,它只是挂起,过了一会儿,浏览器会告诉您脚本已挂起并将其杀死。
出了什么问题?
桌面游戏通常有一个事件循环,它会轮询输入,模拟状态,绘制场景并运行直到终止。在浏览器中,则使用回调函数来执行这些操作,并且由浏览器调用该函数。因此,为了使您的游戏正常运行,您需要将循环重构为回调函数。在Emscripten中,可以使用函数 emscripten_set_main_loop 来设置回调函数。幸运的是,在大多数情况下,这非常简单。最简单的方法是将循环主体重构为辅助函数,然后在桌面版本中循环调用该函数,在浏览器版本中将其设置为回调函数。或者,如果您使用的是C++11,可以使用lambda表达式并将该表达式存储在std::function
中。然后,您可以添加一个小的包装器来调用该函数。
如果您有多个独立的循环,例如加载屏幕,则会出现问题。在这种情况下,您需要将它们重构为单个循环,或者依次调用它们,设置一个新的循环,并使用emscripten_cancel_main_loop
取消上一个循环。这两种方法都非常复杂,并且高度依赖于您的代码。
现在,游戏可以运行了,但是您会收到大量错误消息,提示无法找到您的资源。下一步是将您的资源添加到包中。最简单的方法是预加载它们。在链接标志中添加开关--preload-file <filename>
将导致Emscripten将指定的文件添加到.data文件中,该文件将在调用main之前预加载。然后,可以使用标准的C/C++ I/O调用来访问这些文件。Emscripten将负责必要的魔术。
但是,当您有大量资源时,这种方法会变得很麻烦。整个包需要在程序启动之前加载,这会导致过长的加载时间。为了解决这个问题,您可以流式传输一些资源,例如音乐或视频。
如果您已经在桌面代码中使用了异步加载,那么您可以重复使用它。Emscripten提供函数emscripten_async_wget_data
用于异步加载数据。需要注意的一点区别是,Emscripten异步调用只在加载完成后才知道资源大小,而桌面通常在文件打开后就知道大小。为了获得最佳效果,您应该将代码重构为类似于“加载此文件,然后在您获得文件后执行此操作”的形式。C++11 lambda表达式在这里很有用。无论如何,您确实应该在桌面版本中编写匹配的代码,因为这样调试起来容易得多。
您应该在主循环的末尾添加一个调用来处理异步加载。您不应该异步加载太多内容,因为这可能会很慢,尤其是当您加载多个小文件时。
现在,游戏可以运行一段时间了,但它会崩溃并显示超出内存限制的消息。由于Emscripten使用JavaScript数组来模拟内存,因此这些数组的大小至关重要。默认情况下,它们很小,无法增长。您可以通过使用-s ALLOW_MEMORY_GROWTH=1
链接来启用增长,但这很慢,可能会禁用asm.js优化。这在调试阶段很有用。对于最终发布,您应该找到一个有效的内存限制,并使用-s TOTAL_MEMORY=<number>
。
如上所述,Emscripten没有内存分析器。在Linux上使用Valgrind massif工具来找出内存的消耗情况。
如果您的游戏仍然崩溃,您可以尝试使用JavaScript调试器和源代码映射,但它们不一定能很好地工作。这就是为什么消毒剂很重要的原因。printf
或其他日志记录也是一种很好的调试方法。此外,在链接阶段使用-s SAFE_HEAP=1
可以发现一些内存错误。
Osmos测试版本在Emscripten测试html页面上。
保存和偏好设置
保存数据不像在桌面平台上那样简单。您应该做的第一件事是找到您保存或加载用户生成数据的全部位置。所有这些数据应该放在一个地方,或者通过一个包装器进行处理。如果它没有这样做,您应该在继续之前在桌面版本中进行重构。
最简单的方法是设置本地存储。Emscripten已经包含了必要的代码来执行此操作,并模拟标准的C风格文件系统接口,因此您无需更改任何内容。
您应该将以下类似的代码添加到html中的preRun
中,或者将其添加到您的main函数的开头。
FS.createFolder('/', 'user_data', true, true)
FS.mount(IDBFS, {}, '/user_data');
FS.syncfs(true, function(err) {
if(err) console.log('ERROR!', err);
console.log('finished syncing..');
}
然后,在您写入文件后,您需要告诉浏览器将其同步。添加一个包含以下类似代码的新方法
static void userdata_sync()
{
EM_ASM(
FS.syncfs(function(error) {
if (error) {
console.log("Error while syncing", error);
}
});
);
}
并在关闭文件后调用它。
虽然这可以正常工作,但它存在一个问题,即文件存储在本地。对于桌面游戏来说,这不是问题,因为用户了解保存数据存储在他们的计算机上。对于基于Web的游戏,用户希望他们的保存数据在所有计算机上都存在。对于Mozilla Bundle,Humble Bundle构建了CLOUDFS
库,它与Emscripten的IDBFS
功能相同,并且具有可插拔的后端。您需要使用emscripten的GET
和POST
API构建自己的后端。
Osmos演示在Humble Mozilla Bundle页面上。
提高速度
现在您的游戏可以运行了,但速度并不快。如何提高速度?
在Firefox中,首先要检查的是是否启用了asm.js。打开Web控制台,查找消息“Successfully compiled asm.js”。如果它不存在,错误消息会告诉您发生了什么错误。
接下来要检查的是您的优化级别。Emscripten在编译和链接时都需要使用正确的-O
选项。很容易忘记链接阶段的-O
,因为桌面通常不需要它。测试不同的优化级别,并阅读Emscripten文档了解其他构建标志。特别是,OUTLINING_LIMIT
和PRECISE_F32
可能会影响代码速度。
您还可以通过添加--llvm-lto <n>
选项来启用链接时优化。但请注意,这存在已知的错误,可能会导致代码生成错误,只有在Emscripten升级到更新的LLVM时才会修复,这将在未来某个时间进行。您也可能会在普通优化器中遇到错误,因为Emscripten仍然是一个正在开发中的项目。因此,请仔细测试您的代码,如果您遇到任何错误,请将其报告给Emscripten开发人员。
Emscripten的一个奇怪特性是,任何预加载的资源都会被浏览器解析。我们通常不希望这样做,因为我们没有使用浏览器来显示它们。通过添加以下代码作为--pre-js
来禁用它
var Module;
if (!Module) Module = (typeof Module !== 'undefined' ? Module : null) || {};
// Disable image and audio decoding
Module.noImageDecoding = true;
Module.noAudioDecoding = true;
接下来:不要猜测时间花在哪里,进行分析!使用--profiling
选项(编译和链接阶段)编译您的代码,以便编译器会发出命名符号。然后,使用浏览器的内置JavaScript分析器查看哪些部分速度很慢。请注意,某些版本的Firefox无法分析asm.js代码,因此您要么必须升级浏览器,要么必须通过手动删除生成的JavaScript中的use asm
语句来临时禁用asm.js。您还应该使用Firefox和Chrome进行分析,因为它们具有不同的性能特征,并且它们的分析器工作方式略有不同。特别是,Firefox可能不会考虑速度较慢的OpenGL函数。
像glGetError
和glCheckFramebuffer
这样的函数在桌面平台上速度很慢,在浏览器中可能会非常糟糕。此外,调用glBufferData
或glBufferSubData
的次数过多也会很慢。您应该重构代码以避免调用这些函数,或者尽可能使用一次调用来完成操作。
需要注意的另一点是,您的游戏使用的脚本语言可能非常慢。实际上,没有简单的方法来解决这个问题。如果您的语言提供分析工具,您可以使用它们来尝试提高速度。另一个选择是用本地代码替换脚本,这些代码将被编译为asm.js。
如果您正在执行物理模拟或其他可以利用SSE
优化的操作,您应该注意,目前asm.js不支持它,但它应该很快就会出现。
为了在最终构建中节省一些空间,您还应该检查您的代码和第三方库,并禁用您实际上没有使用的所有功能。特别是像SDL2和freetype这样的库包含了很多大多数程序都不会使用的功能。请查看库的文档,了解如何禁用未使用的功能。Emscripten目前没有办法找出哪些代码部分最大,但如果您有Linux构建(再次,您应该有),您可以使用
nm -S --size-sort game.bin
来查看它。请注意,Emscripten上的大小与本地平台上的大小可能不同。一般来说,它们应该非常一致。
在Dustforce中扫落叶。
总结
总而言之,将现有游戏移植到Emscripten包括删除任何闭源第三方库和线程,使用SDL2进行窗口管理和输入,使用OpenGL ES进行图形处理,以及使用OpenAL或SDL2进行音频处理。您还应该首先将您的游戏移植到其他平台,例如OS X和移动平台,但至少要移植到Linux平台。这使得查找潜在问题变得更容易,并提供了使用一些有用的调试工具的机会。Emscripten移植本身最少需要修改主循环、资源文件处理和用户数据存储。此外,您还需要特别注意优化代码,使其在浏览器中运行。
关于 Turo Lamminen
Alternative Games游戏移植公司的程序员,专门专注于Linux环境。
关于 Tuomas Närväinen
Alternative Games移植公司的程序员,主要负责OS X和Windows平台。
关于 Robert Nyman [荣誉编辑]
Mozilla Hacks的技术布道者和编辑。他经常发表演讲和撰写关于HTML5、JavaScript和开放网络的博客文章。Robert是HTML5和开放网络的坚定支持者,自1999年以来一直致力于Web前端开发——在瑞典和纽约市。他也会定期在http://robertnyman.com上发表博客文章,并且喜欢旅行和结识新朋友。
2 条评论