« 继续完善 protobuf 库 | 返回首页 | 记一个 Bug »

游戏资源的压缩、打包与补丁更新

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 时间。打补丁的速度会慢一些。不过我觉得影响不会太大。因为经常更新的文件会趋向于放在资源包的末尾(每次更新都抽取出来,并连接到末尾),所以压缩空洞时需要搬移的数据有很大机会并不多。

这样设计补丁包的格式,也会更干净一点。

Comments

学习了,谢谢博主分享
针对每个小文件的diff方式,这个版本管理起来挺复杂的,跨了几个版本的话就需要很多patch了。同时性能也比较低下。文件一般都会分块压缩,bsdiff后的patch会比较大,还不如就保存新文件,游戏一般来说都是小文件存储为主。

@xxx

如果资源系统和包系统的设计不在一个层次上,那么建议用完整的文件名做引用。

问个低级问题,以cocos2d-x为例,*.plist作为界面ui描述文件,按云风大哥说的,将所有文件名用Hash算法得到HashCode,那*.plist等界面ui描述文件里面用到的图片资源文件名称,填的是原图片资源文件名称,还是填的HashCode呢?

问个低级问题,以cocos2d-x为例,*.plist作为界面ui描述文件,按云风大哥说的,将所有文件名用Hash算法得到HashCode,那*.plist等界面ui描述文件里面用到的图片资源文件名称,填的是原图片资源文件名称,还是填的HashCode呢?

其实目前来说硬盘空间已经不是问题了,所以保留旧文件也未尝不可,采用版本机制,高版本的文件可以代替低版本文件,这样不需要对原有的压缩包进行变化。只是游戏资源并不是一个压缩包,而是一堆压缩包。

以前网易的游戏就是这样一个特点,解包的时间比下载时间要长,不知道从什么时候开始,解包的时间几乎不用考虑了。

请问你们的数据压缩是用什么方法?zip吗?我不知道zip速度怎么样。我们的游戏数据太大了,普通的zip压缩能压倒20%大小,如果解压算法速度快的话,使用压缩数据肯定能提高一些装载速度

因为以前正好做过类似的东西,我顺面说一下:
1.MPQ其实也可以支持将文件名嵌入包中,(仅仅使用一个特殊的文件名标志,以普通数据文件的方式保存)而在发布时将文件名从包中删除。
2.只要有合理的文件系统,按照独立打包发布的更新文件,叫什么名字其实无所谓,只要在检索的时候将更新的包放在前面就行。大的更新包通过这种方式更新,最后仅仅时读取文件时多浪费一次hash查找的时间(在mpq中就是3次hash一次查找)
3.内置的压缩在MPQ中早就有了,而且不是针对每个文件的,而是针对2/4K的一块数据,所以即使时要求随机读取数据(如文中说的地图),也是可以满足要求,只需要寻址到那个位置,同时解压当前块的数据即可。。。。。需要的话,继续解压下一块。。。
4.我做文件更新格式的时候就是按照文中说的抽小文件的方式,不过后来感觉效率的确比较低。。。。。。针对每个小文件的diff方式实在是好。。。呵呵

95年OLE2的结构化存储就是用此类机制,内部实现了一个mini FAT

http://en.wikipedia.org/wiki/Compound_File_Binary_Format

学习了,谢谢博主分享

太好了,前几天正在做这个呢,很好的参考

Post a comment

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