我们的游戏引擎是基于虚拟文件系统,可以通过网络把开发机上的目录映射到手机上。这对开发非常方便,开发者只需要在自己的开发机上修改资源,立刻就能反应到手机上。
但当游戏发行(也就是我们正在准备的工作),我们还是需要把所有资源打包,并当版本更新时,一次性的下载更新补丁更好。
之前一直没时间做这方面的工作,直到最近才考虑这个问题。我们到底应该设计一个怎样的补丁更新系统。
我不是第一次设计这玩意,早在 20 多年前我就为大话西游设计过一个。但我这次想重新思考这个问题,用一些更标准的技术来做,比如,使用标准的 zip 包格式,而不是自己重新设计。
当然,怎么把文件打包是次要问题,主要问题是怎么解决版本间的差异更新。用户可能停留在不同的版本上,都应该可以正确更新到最新的版本。如有可能,还应该支持版本回滚。
传统的方法是用一个递增的版本号,打包时,仅打包版本间的差异。用户要更新版本时,下载从本地版本到最新版本间的所有 patch 文件,按严格的次序依次打包。我觉得这个方法固然没什么大问题,但不是特别好。因为它不够健壮,缺失一个 patch 就会让升级无法完成。而频繁的版本更迭会导致太多的 patch 。虽然可以定期打包一个全量的包来阻止太多的 patch 文件,但也只是个不太干净的补救手段。版本回滚和分支版本发布都会比较麻烦。
我们的 vfs 系统其实是一棵 Merkle tree 。每个文件的文件名就是它内容的 hash 值。而整棵树的根的 hash 值就是一个天然的版本号。(btw, 它天然是防篡改的。)所谓打包,就是把当前版本的整棵树的文件打包为一个包文件。这个文件的文件名可以就是它的根的 hash ,也就是版本号。
所以,版本号不需要是递增的数字,这样,从一个版本切到另一个版本,也不用区分是更新、还是回滚、亦或是分叉。git 就是这样管理版本的,我们的 vfs 也一样,只不过现在要处理如何打包补丁的问题。
所谓补丁,我们是为了减少更新的带宽,减少用户设备上的存储空间。因为 vfs 中文件的文件名就是内容的 hash 。所以找到补丁和上个版本的差异,只是找到那些新增的文件即可。假设在打包机器上已经有很多历史版本的包,那么,我们需要做的就是用当前版本的完整列表和历史版本包文件内列表相比较,找到新增文件数量最少的那个,并打包新增加的文件即可。
在包里面,可以在补上一点元信息:这个包是补丁包,它的完整版本还依赖另一个版本 hash 。
用户在更新时,一旦需要切到某个特定版本(更新服务器上有所有版本的列表以及建议的最新版本),就下载那个版本的 hash 名的文件即可。下载后,检查元信息,看看所依赖的版本 hash 本地是否存在,如果不存在,再重复前面的过程。
这样更新的好处是,完全兼容平时开发中的 vfs 同步。如果我们用开发版本同步过某些历史版本(这些版本未必发布过更新补丁),再下载更新补丁的话,也能顺利的找到需要的补丁文件,把本地资源补全到完整版本。
这个方案中,不再区分完整版本包和补丁包。它们都代表了某个特定版本,只不过包内数据全或不全。我们在包的元信息中记录三样信息:
这个版本的根 hash 是哪个文件。一般同时是包自己的文件名,但这个信息不应该依赖包的文件名,所以也记录在包内的元信息里。这样,包文件名就可以任意发挥。
这个包的数据不完整的话,数据还依赖哪(几)个 hash 版本。
这个包依赖哪个版本的二进制执行文件。这个通常是源代码的 git hash 版本号。因为执行文件是不打包在资源包里的,所以需要单独注明,已便运行时校验。