案例研究:SONAR HTML5 游戏开发
去年夏天,我担任了一款名为 SONAR 。 该项目花了大约三个月的时间完成,并且完全是用 JavaScript 从头开始完成的。 在 SONAR 的开发过程中,我们不得不在新的和未经测试的 HTML5 水域中找到许多问题的创新解决方案。 特别是,我们需要一个看似简单的问题的解决方案:当玩家开始游戏时,我们如何下载和缓存 70+ MB 的游戏数据?
对于这个问题,其他平台有现成的解决方案。 大多数游戏机和 PC 游戏从本地 CD/DVD 或硬盘驱动器加载资源。 Flash 可以将所有资源打包为包含游戏的 SWF 文件的一部分,Java 可以对 JAR 文件执行相同的操作。 Steam 或 App Store 等数字分发平台可确保在玩家开始游戏之前下载并安装所有资源。
HTML5 没有为我们提供这些机制,但它确实为我们提供了构建自己的游戏资源下载系统所需的所有工具。 构建我们自己的系统的好处是我们获得了我们需要的所有控制和灵活性,并且可以构建一个完全符合我们需求的系统。
恢复
在我们有资源缓存之前,我们有一个简单的链式资源加载器。 该系统允许我们通过相对路径请求单个资源,这反过来又可以请求更多资源。 我们的加载屏幕显示了一个简单的进度表,用于衡量需要加载多少数据,并且仅在资源加载队列为空后才转换到下一个屏幕。
该系统的设计使我们能够轻松地在打包资源和通过本地 HTTP 服务器提供的松散(未打包)资源之间切换,这确实有助于确保我们可以快速迭代游戏代码和数据。
以下代码说明了我们的链式资源加载器的基本设计,其中删除了错误处理和更高级的 XHR/图像加载代码以保持可读性。
function ResourceLoader() { this.pending = 0; this.baseurl = './'; this.oncomplete = function() {}; } ResourceLoader.prototype.request = function(path, callback) { var xhr = new XmlHttpRequest(); xhr.open('GET', this.baseurl + path); var self = this; xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { callback(path, xhr.response, self); if (--self.pending == 0) { self.oncomplete(); } } }; xhr.send(); };
该接口的使用非常简单,但也非常灵活。 初始游戏代码可以请求一些描述初始游戏关卡和游戏对象的数据文件。 例如,这些可能是简单的 JSON 文件。 然后,用于这些文件的回调检查该数据并可以对依赖项发出额外的请求(链式请求)。 游戏对象定义文件可能会列出模型和材质,然后材质的回调可能会请求纹理图像。
这 oncomplete
附加到主要的回调 ResourceLoader
只有在加载所有资源后才会调用实例。 游戏加载屏幕可以在转换到下一个屏幕之前等待调用该回调。
当然,这个界面可以做更多的事情。 作为读者练习,一些值得研究的附加功能是添加进度/百分比支持、添加图像加载(使用 Image 类型)、添加 JSON 文件的自动解析,当然还有错误处理。
本文最重要的特性是baseurl字段,它可以让我们轻松切换我们请求的文件的来源。 很容易设置核心引擎以允许 URL 中的 ?uselocal 类型的查询参数从同一本地 Web 服务器提供的 URL 请求资源(例如 python -m SimpleHTTPServer
) 为游戏提供主 HTML 文档,如果未设置参数,则使用缓存系统。
包装资源
资源链式加载的一个问题是无法获得所有数据的完整字节数。 这样做的结果是没有办法为下载创建一个简单、可靠的进度对话框。 由于我们将下载所有内容并对其进行缓存,而对于大型游戏来说这可能需要相当长的时间,因此为玩家提供一个不错的进度对话框非常重要。
解决这个问题的最简单方法(这也给我们带来了一些其他不错的优势)是将所有资源文件打包到一个包中,我们将通过单个 XHR 调用下载它,这为我们提供了我们需要显示的进度事件一个不错的进度条。
构建自定义捆绑文件格式并不难,甚至可以解决一些问题,但需要创建一个用于创建捆绑格式的工具。 另一种解决方案是使用已经存在工具的现有存档格式,然后只需要编写一个解码器即可在浏览器中运行。 我们不需要压缩存档格式,因为 HTTP 已经可以使用 gzip 或 deflate 算法压缩数据就好了。 由于这些原因,我们选择了 TAR 文件格式。
TAR 是一种相对简单的格式。 每条记录(文件)都有一个 512 字节的标题,然后是填充到 512 字节的文件内容。 对于我们的目的,标头只有几个相关或有趣的字段,主要是文件类型和名称,它们存储在标头中的固定位置。
TAR 格式的标头字段以固定大小存储在标头块中的固定位置。 例如,文件的最后修改时间戳存储在从头开始的 136 个字节处,长度为 12 个字节。 所有数字字段都被编码为以 ASCII 格式存储的八进制数。 然后,要解析字段,我们只需从数组缓冲区中提取字段,对于数字字段,我们调用 parseInt() 确保传入第二个参数以指示所需的八进制基数。
最重要的字段之一是类型字段。 这是一个八进制数字,它告诉我们记录包含什么类型的文件。 对我们而言,仅有的两种有趣的记录类型是常规文件('0')和目录('5')。 如果我们正在处理任意 TAR 文件,我们可能还会关心符号链接 ('2') 和可能的硬链接 ('1')。
每个标头后面紧跟标头描述的文件内容(除了没有自己的内容的文件类型,如目录)。 然后在文件内容之后进行填充,以确保每个标头都以 512 字节边界开始。 因此,要计算 TAR 文件中文件记录的总长度,我们首先必须读取文件的标题。 然后,我们将标头的长度(512 字节)与从标头中提取的文件内容的长度相加。 最后,我们添加任何必要的填充字节以使偏移量与 512 字节对齐,这可以通过将文件长度除以 512,取数字的上限,然后乘以 512 来轻松完成。
// read a string out of an array buffer with a maximum string length of 'len'. // state is an object containing two fields: the array buffer in 'buffer' and // the current input index in 'index'. function readString(state, len) { var str = ''; // we read out the characters one by one from the array buffer view. // this actually is a lot faster than it looks, at least on Chrome. for (var i = state.index, e = state.index + len; i != e; ++i) { var c = state.buffer[i]; if (c == 0) { // at NUL byte, there's no more string break; } str += String.fromCharCode(c); } state.index += len; return str; } // read the next file header out of a tar file stored in an array buffer. // state is an object containing two fields: the array buffer in 'buffer' and // the current input index in 'index'. function readTarHeader(state) { // the offset of the file this header describes is always 512 bytes from // the start of the header var offset = state.index + 512; // the header is made up of several fields at fixed offsets within the // 512 byte block allocated for the header. fields have a fixed length. // all numeric fields are stored as octal numbers encoded as ASCII // strings. var name = readString(state, 100); var mode = parseInt(readString(state, 8), 8); var uid = parseInt(readString(state, 8), 8); var gid = parseInt(readString(state, 8), 8); var size = parseInt(readString(state, 12), 8); var modified = parseInt(readString(state, 12), 8); var crc = parseInt(readString(state, 8), 8); var type = parseInt(readString(state, 1), 8); var link = readString(state, 100); // the header is followed by the file contents, then followed // by padding to ensure that the next header is on a 512-byte // boundary. advanced the input state index to the next // header. state.index = offset + Math.ceil(size / 512) * 512; // return the descriptor with the relevant fields we care about return { name : name, size : size, type : type, offset : offset }; };
我四处寻找现有的 TAR 阅读器,发现了一些,但没有一个没有其他依赖项,或者很容易融入我们现有的代码库。 出于这个原因,我选择自己写。 我还花时间尽可能优化加载,并确保解码器轻松处理存档中的二进制和字符串数据。
我必须解决的第一个问题是如何实际从 XHR 请求中获取数据。 我最初是从“二进制字符串”方法开始的,这使得解析 TAR 标头非常容易。 不幸的是,从二进制字符串转换为更容易使用的二进制形式,如 ArrayBuffer
不是直截了当的,这种转换也不是特别快。 转换为 Image
对象同样痛苦。
我决定将 TAR 文件加载为 ArrayBuffer
直接来自 XHR 请求并添加一个小便利功能,用于从 ArrayBuffer
到一个字符串。 目前我的代码只处理基本的 ANSI/8 位字符,但是一旦浏览器中提供了更方便的转换 API,就可以解决这个问题。
该代码只是扫描 ArrayBuffer
解析记录头,其中包括所有相关的 TAR 头字段(和一些不那么相关的)以及文件数据在 ArrayBuffer
. 该代码还可以选择将数据提取为 ArrayBuffer
查看并将其存储在返回的记录标题列表中。
的友好、宽松的开源许可下免费获得 https://github.com/subsonicllc/TarReader.js 。
文件系统 API
为了实际存储文件内容并在以后访问它们,我们使用了 FileSystem API。 API 是相当新的,但已经有一些很棒的文档,包括优秀的 HTML5 Rocks FileSystem 文章 。
FileSystem API 并非没有注意事项。 一方面,它是一个事件驱动的接口; 这既使 API 成为非阻塞的,这对 UI 来说非常有用,但也使使用起来很痛苦。 使用 WebWorker 中的 FileSystem API 可以缓解这个问题,但这需要将整个下载和解包系统拆分为 WebWorker。 这甚至可能是最好的方法,但由于时间限制(我还不熟悉 WorkWorkers),这不是我采用的方法,因此我必须处理 API 的异步事件驱动性质。
我们的需求主要集中在将文件写入目录结构。 这需要对每个文件执行一系列步骤。 首先,我们需要获取文件路径并将其转换为列表,这很容易通过在路径分隔符上拆分路径字符串(始终是正斜杠,如 URL)来完成。 然后我们需要遍历结果列表中的每个元素保存最后一个,递归地在本地文件系统中创建一个目录(如果需要)。 然后我们可以创建文件,然后创建一个 FileWriter
,最后写出文件内容。
代码有点迟钝,但不是特别棘手,也不是很难编写。
要考虑的第二个重要事项是 FileSystem API 的文件大小限制 PERSISTENT
贮存。 我们想要持久存储,因为可以随时清除临时存储,包括当用户正在玩我们的游戏时,就在它尝试加载被驱逐的文件之前。
对于以 Chrome 网上应用店为目标的应用,使用 unlimitedStorage
应用程序清单文件中的权限。 但是,常规 Web 应用程序仍然可以通过实验性配额请求界面请求空间。
function allocateStorage(space_in_bytes, success, error) { webkitStorageInfo.requestQuota( webkitStorageInfo.PERSISTENT, space_in_bytes, function() { webkitRequestFileSystem(PERSISTENT, space_in_bytes, success, error); }, error ); }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 制作世界奇观 3D 地球仪
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论