一般游戏会把所需要资源数据打包成一个大文件,游戏运行时可以像访问普通文件一样,访问包内文件。如何打包如何更新资源包,是设计重点。
现在有很多资源包直接使用通用打包(压缩)格式,比如 zip 。也有自行设计的,多半是为了一些特殊需求。比如资源间有引用关系等。如果资源数量过多,通常还会对原始资源文件名做一次 hash 索引,加快包内文件检索效率。像暴血的 mpq 格式,还有 unity3d 的 asset bundle 格式都是这样的。
一旦资源打包,原始文件名信息就不再需要了。应用程序可以在运行时通过文件名的 hash 值索引到包内文件。(所以第三方的 mpq 解包工具需要提供一份额外的文件名列表)
既然是 hash ,那么就应该在包格式设计中考虑 hash 冲突的解决方案。asset bundle 的设计人这方面经验欠缺,他们在生成 Deterministic 包的时候, 遇到 hash 冲突就直接放弃打包 。这显然是不合理的。
对于一般几千个文件的包, 32bit hash 值肯定是够用的,但 hash 冲突绝对不能忽略。
一个简单的方法是加盐做二次 hash 。比如你在打包时发现有两个不同的文件名 A 和 B ,他们的 hash 值相同,比如 hash(A) 和 hash(B) 都是 H 。那么,在文件索引表中,就不应该在 H 名下记录数据,而是记录一个冲突标记,并引用一个 salt 。
然后,无论在打包阶段还是运行阶段,碰到 hash(A) 或 hash(B) 时,先查询到 H ,再重新计算一次 hash(A, salt) 或 hash(B,salt) 。这样就可以得到两个不一样的 hash 值了。
保证 hash(A, salt) 不等于 hash(B, salt) 应该在打包阶段完成,为了得到一致的数据包,salt 不用随机产生,而可以从一个固定串开始递增。反复尝试 salt 直到找到一个合适的串,可以区分 hash 冲突的串。
注意:通常你不应该采取直接把文件名和 salt 串拼接的方式来加盐。因为大部分 hash 算法是基于数据流 hash 的。如果两个串的 hash 值相同,那么在前面或后面连接一个相同的串,同样会冲突。
正确的方法是选择一个直接 seed 的 hash 算法,把 salt 当作 seed ; 或者把 salt 循环 xor 到文件名上也不错。
包间引用不可以光靠 hash 值,因为 hash 值不是 guid ,很难保证不同的包内文件 hash 严格唯一。一个简单的方法是把对外引用的资源(文件)名单独保存在资源里,并对文件名做一个 hash 索引。这样资源包内向资源包做引用时,先用一个 hash 值应用到文件名,再间接引用到包的外部。
大部分情况下,我们并不需要把 patch 真的合并到原始包中去。只需要把修改过的文件打一个新包即可。我个人建议 patch 包应该保留完整包的所有文件索引。但如果这个 patch 中并不对老文件做修改,就标记一下该文件不在包内即可。
这样,游戏运行时,可以只加载 patch 包,如果 patch 包指明数据不在包内,再打开前一个版本的资源包,它可能依旧是一个 patch 包,也可以是一个完整的资源包。
如果你的游戏需要频繁更新,那么可以定期生成一个完整资源包,并生成从这个完整版本后每个版本到最新版的 patch 包(这个过程可以通过完善你的工具链在自动进行,只要实现正确,生成 patch 的效率应该是很高的)。用户本地版本如果比这个完整资源包还老,就全量下载;如果版本较新,则只需要下载他的版本到最新版的 patch 包即可。
如果你把打包看成是数据文件的版本维护就清楚了。你需要的是一个类似 git 的工具。
将一个本地目录初始化成资源仓库;
向仓库中添加或删除一个本地文件;
查看当前仓库跟踪了哪些数据文件;
将当前版本打包;
为两个已存在的版本求 diff ,生成 patch ;
利用若干 patch 合成一个完整数据包。