很早之前,闪现过写文件包系统的想法, 但是觉得还没有到时候. 由于目前工作上在做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的特性放这里比较好.

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

引擎设计跟踪(九.9) 文件包系统(Game Package System)的更多相关文章

  1. 引擎设计跟踪(九.14.2a) 导出插件问题修复和 Tangent Space 裂缝修复

    由于工作很忙, 近半年的业余时间没空搞了, 不过工作马上忙完了, 趁十一有时间修了一些小问题. 这次更新跟骨骼动画无关, 修复了一个之前的, 关于tangent space裂缝的问题: 引擎设计跟踪( ...

  2. 引擎设计跟踪(九.14.2j) TableView工具填坑以及多国语言

    Blade的UI都是预定义的接口, 然后由插件来负责实现, 目前只有MFC的插件. 最近加上了TableView的视图, 用于一些文件的查看和编辑, 比如前面在文件包的笔记中提到需写一个package ...

  3. 引擎设计跟踪(九.14.2f) 最近更新: OpenGL ES & tools

    之前骨骼动画的IK暂时放一放, 最近在搞GLES的实现. 之前除了GLES没有实现, Android的代码移植已经完毕: [原]跨平台编程注意事项(三): window 到 android 的 移植 ...

  4. 引擎设计跟踪(九.14.2i) Android GLES 3.0 完善

    最近把渲染设备对应的GLES的API填上了. 主要有IRenderDevice/IShader/ITexture/IGraphicsResourceManager/IIndexBuffer/IVert ...

  5. 引擎设计跟踪(九.14.2g) 将GNUMake集成到Visual Studio

    最近在做纹理压缩工具, 以及数据包的生成. shader编译已经在vs工程里面了, 使用custom build tool, build命令是调用BladeShaderComplier, 并且每个文件 ...

  6. 引擎设计跟踪(九.8) Gizmo helper实现与多国语言

    最近把gizmo helper的绘制做好了. 1.为了复用代码,写了utility来创建sphere, cube, cylinder, plane, ring(line), circle(solid) ...

  7. 引擎设计跟踪(九.14.3.4) mile stone 2 - model和fbx导入的补漏

    之前milestone2已经做完的工作, 现在趁有时间记下笔记. 1.设计 这里是指兼容3ds max导出/fbx格式转换等等一系列工作的设计. 最开始, Blade的3dsmax导出插件, 全部代码 ...

  8. 引擎设计跟踪(九.14.2 final) Inverse Kinematics: CCD 在Blade中的实现

    因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://gr ...

  9. 引擎设计跟踪(九.14.2e) DelayLoaded DLLs (/DELAYLOAD)

    关于DLL的delay load: http://msdn.microsoft.com/en-us/library/151kt790.aspx 最近在做GLES的shader compiler, 把现 ...

随机推荐

  1. apache和IIS共享80端口问题

    使用apache代理功能和IIS共享80端口的解决办法. 第一步:把iis所发布的网站默认端口由80改为8080: 第二步:修改apache的httpd.conf配置文件.  首先,要让apache支 ...

  2. 2)Java中的==和equals

    Java中的==和equals   1.如果比较对象是值变量:只用==   2.如果比较对象是引用型变量:      ==:比较两个引用是不是指向同一个对象实例.      equals:       ...

  3. [转]给C++初学者的50个忠告

    1.把C++当成一门新的语言学习(和C没啥关系!真的.):   2.看<Thinking In C++>,不要看<C++变成死相>:   3.看<The C++ Prog ...

  4. Delphi XE5 for android 调用Java类库必看的文件

    C:\Program Files\Embarcadero\RAD Studio\12.0\source\rtl\android 的目录 Androidapi.AppGlue.pasAndroidapi ...

  5. linux下的mount命令的用法详解

    挂接命令(mount) 首先,介绍一下挂接(mount)命令的使用方法,mount命令参数非常多,这里主要讲一下今天我们要用到的. 命令格式:mount [-t vfstype] [-o option ...

  6. PIL不能关闭文件的解决方案

    今天写了一个能指定图片尺寸,以及比例 来搜索分类图片的Python脚本.为了读取多个格式的文件的头,采用了Python PIL库. im = PIL.Image.open(imPath) if im的 ...

  7. Bash美化

    首先声明下,这些美化方式都不是我自己想的,而是多个牛人的方法. 第一:简单点 这个方法来自于:http://www.vimer.cn/?p=1554 没有美化前是这样,鼠标光标在很右边: 在.bash ...

  8. [SRH.Docker] HBase Java 第一天学习记录

    主要对HBase Table 的 简单操作, 直接上代码吧!!! http://pan.baidu.com/s/1hqzTTze       ui92

  9. 如何使用 Microsoft Azure Media Services 现场直播,(Live Streaming) 直播流媒体系统

    不久之前,微软公司宣布了 Microsoft Azure Media Services 实时直播服务 ( Live ) 开始进入技术预览阶段,公开接受用户测试. 而这些实时直播服务其实早已被 NBC ...

  10. Go append方法

    append用来将元素添加到切片末尾并返回结果.看代码: package main import "fmt" func main() { x := [],,} y := [],,} ...