游戏人生

不积跬步,无以至千里;不积小流,无以成江海。

My Links

Blog Stats

引擎设计跟踪(九.9) 文件包系统(Game Package System)

很早之前,闪现过写文件包系统的想法, 但是觉得还没有到时候. 由于目前工作上在做android ndk开发, 所以业余时间趁热做了android的移植, 因为android ndk提供的mountable obb调试时不太好用,或许因为有坑还没有发现. 所以把Ogre的zip文件系统拿了过来. 因为引擎里已经有了类似Ogre的IStream抽象, 所以做起来比较简单. 把zip文件改成obb后缀上传到手机,就可以测试了. 目前除了GLES没实现,全部代码已经移植完了.移植笔记在这里:

http://hi.baidu.com/crazii_chn/item/62705798f8a76bd91b49dfd8

突然想写包系统, 是因为简单看了zziplib的内容, 感觉在文件打开/查找上效率稍微有点低, 不太适合文件多的情况. zziplib是把包内的*所有*文件(包括子文件夹内的文件), 组成线性文件表(用链表链接起来), 查找时逐个遍历. 简单想了一下, 如果用目录树的话, 效率会更高, 因为按节点匹配,可以很快定位到最终文件. 同时, 目录树内某一个节点的所有同级子节点还可以排序(如果用纯C的话,可能也用树比较方便,或者使用有序数组进行binary_seach),我直接用的map/set, 省了很多事. 总之用树的话比线性表效率要高.

同时想了下游戏中的诸多需求, 如可能频繁更新(OnlineGame), 添加/删除文件等等, 觉得基本IO功能虽然很好写,但是复杂的需求,写起来要考虑的还是蛮多蛮复杂的. 

1.考虑到频繁添加文件的需求, 个人觉得包内的文件表应该放在尾部.因为文件表相对真正的数据来说很小, 放在尾部的好处就是可以很快追加文件, 追加完以后把新的表写入.

package layout:

+--------------------------------------+----------+
| Data                                               | File Table |

+--------------------------------------+----------+

after adding a new file:

+----------------------------------+-------------+----------+
| Data                                         |new file data  | File Table |

+----------------------------------+-------------+----------+

                                                   |  newly written content  |

这样频繁添加的需求大致可以解决了.

2.频繁的删除: 记得某些数据库(初中时玩过FoxBASE+编程)在删除条目的时候只是打上标记而不真正删除, 我觉得这个策略也适合游戏包的删除. 因为删除文件后包内有碎片, 要想没有碎片, 每次删除一个文件都要重写数据, 重写包数据时间过长难以接受. 所以现在所做的就是把文件打上删除标记, 而不真正删除, 而且被标记的数据不会在被利用.


那么这些文件什么时候真正删除呢? 不删除的话, 积累越来越多, 利用率变得越来越低,无法接受.


当删除的数据(文件内容)总和达到一个阈值以后, 比如256M等等(用户根据情况来指定), 把包内的被删除的文件真正去掉. 这个时候可能已经有很多被删除的文件了, 相当做一次碎片整理. 当然这个碎片整理比OS的磁盘整理要简单多了.
数据包整理的方式很简单: 从第一个被删除的文件开始, 把后面的数据(至第二个被删除文件的数据开始处)往前挪动, 填补被删除的空缺, 这个时候第一个空缺被往后移, 跟第二个被删文件的空缺连在一起了, 使用同样的方法处理所有被删除的文件. 这种方法可以使数据移动最小化.
删除的时候额外要做的工作就是, 根据文件偏移来定位文件表项, 然后更新其偏移量信息.

 

使用如上实现方式, 对于上层用户(game/app developer)来说, 策略最好如下: 

a.每次单版本更新, 先删除文件, 然后检查是否需要整理, 如果需要则整理, 整理后添加新文件. 因为先添加文件的话如果需要整理, 写入之后还要再挪动, 不管怎么说都不是太好的方法.

b.如果客户端当前版本比最新版本差的版本太多, 那么先下载所有更新包(patch), 同样先删除每个更新包内需要删除的文件, 所有的删除完以后再检查是否需要整理, 或者强制整理, 最后再逐个添加每个包内新加的内容. 原因同上.

 

先删除文件再添加文件, 更新包之间的依赖是个问题. 比如update1添加了一个文件, update2把它删了, 如果先删除所有文件的话, 原始包里面还没有这个文件, 怎么处理?
*按版本顺序*遍历当前版本之前的所有patch包, 直到找到一个存在该文件的patch包, 比如update1, 然后在本地把该文件做删除标记. 按版本顺序的原因是中间可能有反复的添加和删除.

这种做法最大的缺点在于不同客户端的数据, 由于更新的方式不一样(逐版本更新/一次性更新), package layout可能会不一样(感觉应该不会, 但没仔细想), 最终导致没办法做crc等校验.
当然也可以写patch合并工具, 生成任意两个版本之间的一次性更新包.

另外, 为了简单起见, 每个patch包的格式, 可以与游戏主数据包相同(使用同一种包格式), 包里面额外存放一些patch信息, 比如需要删除的文件列表的txt...

 

除了以上以外, diff工具也要有, 用于生成patch, 还要有package expolorer, 不过这两个还没有时间写, 目前只实现了打包工具(命令行), 和简单的IO操作, 可以完成最基本的运行需求, 数据整理功能也没有实现. 计划diff工具写成命令行, package expolorer兼有有打包和生成patch功能, 作为编辑器的插件来写, 复用基本菜单,工具和视图(虽然这种视图还没有写).

 


 

3/3/2014更新

3.包的嵌套
理论上, 有一种特殊的情况也需要支持: 一个包内放了另一个包. 比如Ogre里面也有zip格式嵌套.
首先, 个人觉得这种情况确实存在, 但是为某一种具体的包格式写具体的嵌套实现是一种不良的设计. 其次, 引擎已经有了IStream抽象, 一个包系统完全可以只依赖这个接口来做依赖的IO, 到了这个时候, 嵌套已经完全解决了. 因为读取包文件依赖的是一个抽象的接口, 这个接口的具体实现可以是native IO, 或者相同格式的包系统, 或者其他格式的包系统. 但在这个包系统内部, 不需要关系它的实现.

 

4.调试: Native IO fallback

我所见过的某些包系统会优先查找本地文件(比如MPQ,和WPF), 如果有本地文件的时候就加载本地文件, 否则再从包里面查找.这么做主要是方便调试, 对于调试时,频繁更新的文件可以放在在本地.
当然它有一个缺点就是很容易被玩家"篡改", 比如DiabloII中, 在游戏目录下放几个文件夹和文件, 那么游戏就会优先加载这些文件, 我记得DiabloII的简体中文字体patch就是这样. 这样究竟好不好, 允不允许,或许是另外一个话题.但至少,把这个feature做成是可配置的feature应该不难, 这样如果最终发布时想关闭也没有问题.

 

个人觉得这个功能, 不应该在包系统里实现.因为包系统作为一个完整的系统, 接口已经齐全. 这个功能可以放在IArchive(文件系统的抽象)里面做二次封装.目前这个功能还没有加上, 但最初设计资源管理系统的时候,考虑到文件包和本地数据路径的切换, 使用了类似URI的东西, 比如media:/test.dds,  已经可以将media映射到本地路径或者一个包文件, 但只能切换整个包, 不能单独加载某个磁盘文件.

说道二次封装,这里贴一个层次依赖关系:

IStream

   |           \

BPKFile             IArhive

  |              /

 BPKArchive (+Native IO fallback feature)

BPKFile依赖IStream是为了实现嵌套读取, 这是它的最小依赖. 如果不需要嵌套的话, 可以去掉这个依赖, 这样BPK的独立性更高. IStream是BPK系统的一部分, 只不过它正好和现有的接口重合.

BPKArchive是用于将包系统集成到引擎的类,是属于整个引擎框架的一部分, 理论上它可以依赖引擎中的其他模块, 可以放入引擎插件. 而Istream,IArhive和BPKFile属于基础类库,独立性比较高, 不依赖于引擎的架构.
所以说BPKArchive的独立性和抽象程度都是较低的, 具化程度更高, 所以Native IO fallback的特性放这里比较好.

 

由于游戏的包系统,个人只知道大致原理, 也没有看过相关代码实现, 所以可能考虑的不够周全. 有时间了看看开源的代码学习一下. 如果有新的想法的话,就及时更新以备忘.

posted on 2014-03-01 17:30 crazii 阅读(...) 评论(...) 编辑 收藏