« Lua 的 C 模块之间如何传递内存块 | 返回首页 | 游戏引擎中的可视化编辑器 »

游戏数据包的补丁和更新

我们的游戏引擎是基于虚拟文件系统,可以通过网络把开发机上的目录映射到手机上。这对开发非常方便,开发者只需要在自己的开发机上修改资源,立刻就能反应到手机上。

但当游戏发行(也就是我们正在准备的工作),我们还是需要把所有资源打包,并当版本更新时,一次性的下载更新补丁更好。

之前一直没时间做这方面的工作,直到最近才考虑这个问题。我们到底应该设计一个怎样的补丁更新系统。

我不是第一次设计这玩意,早在 20 多年前我就为大话西游设计过一个。但我这次想重新思考这个问题,用一些更标准的技术来做,比如,使用标准的 zip 包格式,而不是自己重新设计。

当然,怎么把文件打包是次要问题,主要问题是怎么解决版本间的差异更新。用户可能停留在不同的版本上,都应该可以正确更新到最新的版本。如有可能,还应该支持版本回滚。

传统的方法是用一个递增的版本号,打包时,仅打包版本间的差异。用户要更新版本时,下载从本地版本到最新版本间的所有 patch 文件,按严格的次序依次打包。我觉得这个方法固然没什么大问题,但不是特别好。因为它不够健壮,缺失一个 patch 就会让升级无法完成。而频繁的版本更迭会导致太多的 patch 。虽然可以定期打包一个全量的包来阻止太多的 patch 文件,但也只是个不太干净的补救手段。版本回滚和分支版本发布都会比较麻烦。


我们的 vfs 系统其实是一棵 Merkle tree 。每个文件的文件名就是它内容的 hash 值。而整棵树的根的 hash 值就是一个天然的版本号。(btw, 它天然是防篡改的。)所谓打包,就是把当前版本的整棵树的文件打包为一个包文件。这个文件的文件名可以就是它的根的 hash ,也就是版本号。

所以,版本号不需要是递增的数字,这样,从一个版本切到另一个版本,也不用区分是更新、还是回滚、亦或是分叉。git 就是这样管理版本的,我们的 vfs 也一样,只不过现在要处理如何打包补丁的问题。

所谓补丁,我们是为了减少更新的带宽,减少用户设备上的存储空间。因为 vfs 中文件的文件名就是内容的 hash 。所以找到补丁和上个版本的差异,只是找到那些新增的文件即可。假设在打包机器上已经有很多历史版本的包,那么,我们需要做的就是用当前版本的完整列表和历史版本包文件内列表相比较,找到新增文件数量最少的那个,并打包新增加的文件即可。

在包里面,可以在补上一点元信息:这个包是补丁包,它的完整版本还依赖另一个版本 hash 。

用户在更新时,一旦需要切到某个特定版本(更新服务器上有所有版本的列表以及建议的最新版本),就下载那个版本的 hash 名的文件即可。下载后,检查元信息,看看所依赖的版本 hash 本地是否存在,如果不存在,再重复前面的过程。

这样更新的好处是,完全兼容平时开发中的 vfs 同步。如果我们用开发版本同步过某些历史版本(这些版本未必发布过更新补丁),再下载更新补丁的话,也能顺利的找到需要的补丁文件,把本地资源补全到完整版本。


这个方案中,不再区分完整版本包和补丁包。它们都代表了某个特定版本,只不过包内数据全或不全。我们在包的元信息中记录三样信息:

  1. 这个版本的根 hash 是哪个文件。一般同时是包自己的文件名,但这个信息不应该依赖包的文件名,所以也记录在包内的元信息里。这样,包文件名就可以任意发挥。

  2. 这个包的数据不完整的话,数据还依赖哪(几)个 hash 版本。

  3. 这个包依赖哪个版本的二进制执行文件。这个通常是源代码的 git hash 版本号。因为执行文件是不打包在资源包里的,所以需要单独注明,已便运行时校验。

Comments

还有个更古老的版本控制系统,叫CVS,大约几万行C代码。
之前做过类似的游戏项目, 参考steam pipe (stream 平台的游戏更细机制,大概8年前版本), 跟云风大佬类似, 不过要求更多做一步,对文件按1M进行切割,,好处如下: 1. 对CDN友好。小文件有利于更快传播到边缘节点,(10年前的CDN来说,现在不清楚CDN是否依旧有效) 2. 有利于减少更新包大小,因为很多文件并不是每次都全部更新,可能只更新部分(所以官方给出的打包建议是尽量在尾部加内容)
为什么不直接用 git 的实现 ? 因为,我们只需要实现 git 一个非常小的功能集合。目前的实现也只有几百行代码,并没有任何其它库的依赖。 我们使用 Lua 实现的,方便更新。
git 也是相同的思路,每个文件对应一个 blob,名字是内容+固定结构的摘要值。多个 blob 名字构成 tree,tree 也可以包含其他 tree。tree 名字也是内容+固结构的摘要值。最外层的 tree 对象再附上作者等提交信息以及上一次提交形成一次新提交,还是计算摘要值。整个 git 提交历史以及追踪的内容构成一棵 Merkle Tree。
感谢云风大佬分享。我感觉这种实现跟git差不多,那么为什么不用git这种来实现。是基于技术的实现还是商用的原因呢。

Post a comment

非这个主题相关的留言请到:留言本