9 年前,我设计了网易游戏的资源包以及补丁包的数据格式。
当初的设计目的是:方便解析,快速定位资源包内的文件,方便更新、每次更新尽可能的节约带宽。这些年来,虽然各个项目修修补补的改进了资源包的格式,但本质上并没有特别大的修改。
一开始我们直接把需要打包的文件连接起来,在文件末尾附上文件索引表。当初为了快速定位文件名,文件名做了 hash 处理,可以用 hash 值直接定位文件。而资源包里并没有储存文件名信息,而是保存在一个额外的 index 文件中。这个 index 文件并不对外发布。所以直接对资源包解包是无法准确还原文件名的。
btw, 暴雪的 mpq 文件也是作类似处理的。除非你猜测出文件名,否则也很难对文件名还原。网上许多 mpq 解包工具都针对特定游戏附了一个额外的文件名列表。
和许多其它游戏 Client (比如暴雪的 MPQ 文件)不同。我们的包格式里文件与文件之间是允许有空洞的。这是考虑到资源包文件都比较大。如果用传统的打包软件运作的方式:从包内删除一个文件,就重新打包或移动内部数据。在玩家更新资源的时候,就会有大量的文件 IO 操作。比如 WOW 或 SC2 在更新的时候,下载更新包的时间往往只占整个更新时间的一小部分,大部分时间花在把补丁打在已有的资源包上。
如果频繁更新客户端,对于用户,这会有很讨厌的等待。
所以当初考虑到这个因素,我们在删除包内文件时,并不移动资源包内的数据,而是把空间留下来。如果新增加的文件较之小,就重复利用这个空间。如果利用不上,就浪费在那里。这有点像内存管理算法,时间久了,资源包内会有一些空洞,但也是可以接受的。
同时,还有另一个方式更新新的资源。那就是将需要更新的文件单独打包,以相同文件名(后缀不同)保存在用户硬盘上。游戏引擎在读取资源的时候,优先在更新的资源包内检索。这个方式在 Id soft 的 Quake/Doom 系列中也有采用。
为了保证用户补丁更新速度。我们的补丁中并不是保存的资源包内的小文件。而是在开发机上以增量方式重新打包。补丁文件其实是整个资源包的 diff 文件。由于前面所述的打包方案,这个 2 进制 diff 文件其实可以做到很小。尤其对某些文件的局部修改,对整个资源包的影响很小。
在公司,有后来的同事质疑过这种方式,觉得其对减少补丁体积的作用不大。反而增量打包增加了许多制作补丁包的时间。主张直接在补丁中放入更新的小文件,然后让最最终用户机上以小文件为单位做 patching 。
的确,2 进制 diff 的作用有限,现在很多项目改用文本数据格式,很小的修改就会影响整个文件的 diff 结果。不过原始的设计也有其历史原因。因为 10 年前硬盘 I/O 速度很慢,而大话西游在设计时又需要实现无缝加载的大地图。所以地图文件的格式是经过特别设计的。这种方式很适合地图文件的修改和更新。另外,对于未压缩的图片文件的更新也有其意义。
总结完旧有设计后。我希望在新引擎中对资源包格式做一定的改进。
首先是加入内置的压缩。原有格式是只负责打包而不管压缩的。若数据需要压缩,由上层模块去负责。这么做跟大话西游中的地图文件需要随机访问有关。另外和增量打包也有点关系。如果对每个小文件统一做压缩,2 进制 diff 就几乎无效了。
这次希望以文件系统的方式来管理资源包,而不是简单的将文件连接起来。把大文件分块,以类似 FAT 表的形式来管理大文件。每个块则可以单独选择压缩或不压缩。这样即能压缩数据,又可以提供文件随机访问的能力。(能够方便的实现数据包的嵌套)
在资源包内支持链接功能。
开发期,我们可以让所有资源都不用理会复杂的引用关系,而是各自有独立的一份。比如模型文件引用的贴图都可以依附在模型文件的目录下。这样在开发期很方便管理这些数据。而打包时,可以比较所有需打包文件,把内容相同的文件剔除,只是做一个链接。同时引擎也能识别出引用关系,同样的资源只加载一次。
关于增量打包的问题,开发人员发布补丁的效率的确需要考虑。其实在开发期,有个简便快速的制作流程,也方便搭建每日构建。虽然按原有方式也有许多方法加快制作补丁的速度(比如预先计算所有文件的 md5 值保存起来,加块增量打包软件的分析速度)。我希望可以制作时直接生成补丁文件。这个补丁则可以是每个小文件的 diff 信息。另外再制作一个 patch 工具,将补丁打上去。
这个 patch 工具的工作流程可以是从旧的包中抽取出需要 patch 的小文件,和补丁中的 diff 信息合并,得到一个需要更新的包。然后将旧包中那些小文件删除,并向前压缩掉空洞。最后将新旧两个包连接起来。
压缩空洞的这个过程会占用用户机的一些文件 IO 时间。打补丁的速度会慢一些。不过我觉得影响不会太大。因为经常更新的文件会趋向于放在资源包的末尾(每次更新都抽取出来,并连接到末尾),所以压缩空洞时需要搬移的数据有很大机会并不多。
这样设计补丁包的格式,也会更干净一点。