« 三人合租的房租公平分配方案 | 返回首页 | Lua 虚拟机的封装 »

游戏资源仓库及升级发布

去年底,我为我们的 3d engine 设计了资源仓库的结构

随后交给开发组的一个同学实现,这半年来,一直在使用。最近做了引擎一个小版本的内部验收,我感觉这块东西还有比较大的改进余地。因为资源文件系统目前和开发期资源在线更新部分现在掺杂在一起,而网络更新部分似乎还有些 bug ,偶尔会卡住。我觉得定位 bug 成本较高,不如把这块重新实现一遍,顺便把新的改进想法加进去。

这段时间,我重新思考了资源仓库应该怎样设计更合理。越细想越觉得和 git 要解决的问题基本一致。我们的引擎的一个重要特性就是,在 PC 上开发,在移动设备上运行调试。我们需要频繁的将资源同步到设备上,这其实和 git 的运作方式是类似的。我们重新实现的该模块在本地文件系统上的数据组织结构最终也和 git 仓库差不太多了。

这次实现,我们去掉了目录和文件处理方式上的差异,一律都变成了以其内容的 hash 值为文件名(key)的数据块。仓库仅仅是一个 key-value 的数据仓库,以内容 hash (选用了 sha1 算法)为文件名放在仓库目录下。为了避免单个目录文件太多(对大多数文件系统不友好),把内容散列到最多 256 个子目录下。

普通文件就是完全没有修改过的文件本身;而目录是自己建立的索引信息。我采用的是文本格式,一行一个文件描述,记录有文件类型(文件 f 或目录 t )hash 值,文件名。最终一个目录索引文件看起来是这样的:

d 10a34637ad661d98ba3344717656fcc76209c2f8 f0
f da39a3ee5e6b4b0d3255bfef95601890afd80709 f0_1.txt

这表示该目录下有一个子目录 f0 和一个文件 f0_1.txt 。

这样做会使得子目录下任何一个文件的改变,都会改变目录索引本身的 hash 值,进而会影响父目录的 hash 值。也就是说,整个仓库中的任何一点修改,都最终会影响到根目录的索引。

在接口上,我仅提供了从 hash 值 query 数据;修改根目录指针(一个 hash 值);从文本路径查找最终对象的 hash 值等这么几个。

通过不同的根目录指针,我们可以让不同的历史版本存在于同一个仓库中,并且最大限度地重用版本间相同的数据。对于服务器/ PC 编辑器端,我们仅需要提供一个重建索引的操作方法。我们从根索引递归遍历下去,为每个目录创建出索引文件。

和 git 不同的是,我们可以不强制把本地文件系统( git 的工作区)的文件复制到仓库中,而是在仓库中建立一个引用文件。引用文件的文件名使用 hash.ref 的命名方式,表示这个 hash 值对应的实际数据并不在仓库中,而是引用的外部文件。其内容就是外部文件的路径、hash 值和时间戳。由于不同的文件可能内容完全相同,因为一个引用文件可以指向不同的路径,我们会在引用信息中用多行指名;这样在一个文件失效后,还可以依次索引到没有变更的文件。

在快速重建仓库索引时,发现有引用文件,就只比较时间戳。时间戳相同就不必重算 hash 值。


程序以 c/s 结构运行时,在移动设备上先建立一个空的镜像仓库,同步 PC 端的资源仓库。运行流程是这样的:

首先在客户端启动的时候,向服务器索取一个根索引的 hash ,在本地镜像上设定根。

客户端请求一个文件路径时,从根开始寻找对应的目录索引文件,逐级查找。如果本地有所需的 hash 对象,就直接使用;否则向服务器请求,直到最后获得目标文件。api 的设计上,open 一个资源路径,要么返回最终的文件,要么返回一个 hash ,表示当前还缺少这个 hash 对象;这样,可以通过网络模块请求这个对象;获得该对象后,无须理会这个对象是什么,简单写入镜像仓库,然后重新前面的过程,再次请求未完成的路径,最终就能打开所需的资源文件。


这个方式的巧妙之处在于,不需要传统的版本号管理的方式,就可以完美处理多版本问题。客户端和服务器同步过程,只有第一步的根 hash 同步是强制的,且仅同步一个 hash 值。如果移动设备上有多个不同的版本(比如在不同的开发人员的机器上切换),在一开始同步完根 hash 后,完全不用再有任何网络传输。这个过程比 git 切换版本还要快,因为我们在移动设备上没有工作区的概念,不需要在切换版本时拷贝文件到工作区。

在最终版本发布时,也可以做简单的打包,避免小文件过多。简单的把仓库文件打包即可。仓库中就是简单的 key-value 结构,处理包文件比本地文件目录更简单。

同时,发布版本的更新要比传统版本号方式要简单的多。

在发布产品中,我们多半不提供开发期这种按需索要缺少的资源的工作方式,而是将当前版本的所有数据都提前下载后。在这种预下载模式下客户端在启动时从服务器索取一个根 hash ,如果和本地的根相同,则说明版本没有更新;否则,将自己的根 hash (代表了一个版本)发送给服务器。

服务器理论上保留有所有历史发布过的版本,所以会找到这个历史版本。这时服务器可以自己对比这两个版本,计算出客户端缺失的对象,然后打包发送给客户端。

当然,服务器可以缓存一些常见的版本(因为大部分玩家都会停留在上个版本),省略这步比较操作,直接让玩家从 CDN 下载打包好的更新包;对于不常见版本,可以计算出一个最接近的更新包(在 CDN 上),然后把更新包中没有的文件单独打包出来。

这个流程其实和 git 的 fetch 操作非常类似,只不过我们可以针对网游的特质,做一点优化罢了。


因为我们的引擎几乎全部是用 lua 实现,包括这里提到的资源仓库的相关模块,所以理论上这块代码本身也可以被这个仓库管理。这样,还涉及一些代码自举和自更新的流程,这一块的细节也有点意思,我打算过两天另外写一篇 blog 介绍。

Comments

所以为啥不直接用 git 呢…… 啥都不用考虑了呢
非常精妙的想法!
哈希冲突怎么办
像 IPFS
merkle tree 了解一下
很像没有工作区的git版本库, 只是数据结构不是链式,而是树形.
前排

Post a comment

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