引擎设计跟踪(九.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的特性放这里比较好.
由于游戏的包系统,个人只知道大致原理, 也没有看过相关代码实现, 所以可能考虑的不够周全. 有时间了看看开源的代码学习一下. 如果有新的想法的话,就及时更新以备忘.
引擎设计跟踪(九.9) 文件包系统(Game Package System)的更多相关文章
- 引擎设计跟踪(九.14.2a) 导出插件问题修复和 Tangent Space 裂缝修复
由于工作很忙, 近半年的业余时间没空搞了, 不过工作马上忙完了, 趁十一有时间修了一些小问题. 这次更新跟骨骼动画无关, 修复了一个之前的, 关于tangent space裂缝的问题: 引擎设计跟踪( ...
- 引擎设计跟踪(九.14.2j) TableView工具填坑以及多国语言
Blade的UI都是预定义的接口, 然后由插件来负责实现, 目前只有MFC的插件. 最近加上了TableView的视图, 用于一些文件的查看和编辑, 比如前面在文件包的笔记中提到需写一个package ...
- 引擎设计跟踪(九.14.2f) 最近更新: OpenGL ES & tools
之前骨骼动画的IK暂时放一放, 最近在搞GLES的实现. 之前除了GLES没有实现, Android的代码移植已经完毕: [原]跨平台编程注意事项(三): window 到 android 的 移植 ...
- 引擎设计跟踪(九.14.2i) Android GLES 3.0 完善
最近把渲染设备对应的GLES的API填上了. 主要有IRenderDevice/IShader/ITexture/IGraphicsResourceManager/IIndexBuffer/IVert ...
- 引擎设计跟踪(九.14.2g) 将GNUMake集成到Visual Studio
最近在做纹理压缩工具, 以及数据包的生成. shader编译已经在vs工程里面了, 使用custom build tool, build命令是调用BladeShaderComplier, 并且每个文件 ...
- 引擎设计跟踪(九.8) Gizmo helper实现与多国语言
最近把gizmo helper的绘制做好了. 1.为了复用代码,写了utility来创建sphere, cube, cylinder, plane, ring(line), circle(solid) ...
- 引擎设计跟踪(九.14.3.4) mile stone 2 - model和fbx导入的补漏
之前milestone2已经做完的工作, 现在趁有时间记下笔记. 1.设计 这里是指兼容3ds max导出/fbx格式转换等等一系列工作的设计. 最开始, Blade的3dsmax导出插件, 全部代码 ...
- 引擎设计跟踪(九.14.2 final) Inverse Kinematics: CCD 在Blade中的实现
因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://gr ...
- 引擎设计跟踪(九.14.2e) DelayLoaded DLLs (/DELAYLOAD)
关于DLL的delay load: http://msdn.microsoft.com/en-us/library/151kt790.aspx 最近在做GLES的shader compiler, 把现 ...
随机推荐
- leetcode刷题笔记
(1)Best Time to Buy and Sell Stock Total Accepted: 10430 Total Submissions: 33800My Submissions Say ...
- IIS上部署MVC网站,打开后ExtensionlessUrlHandler-Integrated-4.0解决办法
IIS上部署MVC网站,打开后ExtensionlessUrlHandler-Integrated-4.0解决方法 IIS上部署MVC网站,打开后500错误:处理程序“ExtensionlessUrl ...
- java作用域public ,private ,protected 及不写时的区别(转)
在说明这四个关键字之前,我想就class之间的关系做一个简单的定 义,对于继承自己的class,base class可以认为他们都是自己的子 女,而对于和自己一个目录下的classes,认为都是自己的 ...
- Java 第二天
1.不带访问修饰符号的类成员变量默认是friendly(可以同一个包中访问) 2.protected的类成员可以在同一个包及其子类中访问 3.方法中可以定义内部类(如下面的代码),该类只能访问方法中的 ...
- python & pandas链接mysql数据库
Python&pandas与mysql连接 1.python 与mysql 连接及操作,直接上代码,简单直接高效: import MySQLdb try: conn = MySQLdb.con ...
- 1.python的第一步
学习python也有一段时间了,自认为基本算是入门了,想要写一些博客进行知识的汇总的时候.却发现不知道该从何说起了,因为python这门语言在语法上其实并不难,关键在于如何建立程序员的思维方式,而对于 ...
- perl thread
#!/usr/local/bin/perl use threads; @domain = ("tom.com", "chinadns.com", "1 ...
- Python学习教程(learning Python)--1.2.2 Python格式化输出基础
本节讨论为何要格式化输出数据? 先看一段代码吧,本程序的功能是计算月支付金额. amount_due = 5000.0 #年支付金额 monthly_payment = amount_due / 12 ...
- Moses 里的参数(未完成)
老师要求看看Moses里都有什么参数,调整了参数又会对翻译结果有什么影响,先将找到的参数列出来 首先是权重: [weight] WordPenalty0= LM= Distortion0= Phras ...
- iphone/ipad关于size, frame and bounds总结和UIScroll view学习笔记
1. iphone/ipad大小 Device Screen dimensions(in points) iphone and ipod 320 X 480 ipad 768 X 1024 2. UI ...