游戏资源打包

点击打开链接

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

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

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

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

请问你们的数据压缩是用什么方法?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

学习了,谢谢博主分享

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



点击打开链接

高级游戏资源打包技术详解

   一个大型的商业游戏包含很多资源,如图像、声音、文本、脚本和其他各种类型的数据,为游戏提供一个完整和高效的资源管理系统(包括SDK与编辑器)是游戏引擎开发商必须完成的工作。我们在Numen Game Engine 2.0中实现了一个底层资源打包接口,在接口的基础上实现了纹理编辑器、模型编辑器、单位编辑器和建筑物编辑器等工具,整个系统采用32位的ID来标识资源,具有一级分类的能力,支持资源包的各种常用操作。
     但是在Numen Game Engine 3.0中,我们为了实现一个完美的资源打包系统,完全放弃了以前的所有接口,采用更多新的技术,最终构建了一个功能强大,性能高效的资源管理接口,这篇文章中,我们将从资源编辑器的特性简介、关键技术和资源编辑器功能三个方面,介绍构建一个商业级资源打包系统需要掌握的关键技术和实现细节。

一、特性简介
     Numen Pack Editor 2.0支持以下主要功能特性:
1、创建、打开和关闭资源包。
2、压缩资源包。消除数据添加、修改和删除带来的数据碎片,扩展索引表容量,重设资源包密钥。
3、合并资源包。将外部的资源包整合进当前打开的资源包。
4、释放资源文件。将资源包内的任意文件释放到磁盘。
5、创建文件夹。
6、(批量)添加文件。可以指定文件的独立密钥与压缩选项。
7、从文件夹添加数据(包括文件夹与文件)。可以指定文件的独立密钥与压缩选项。
8、(批量)更新文件。可以选择按列表顺序与按文件名两种方式进行数据更新。
9、(批量)复制、剪切、粘贴、删除和重命名单元。
10、查看和修改资源属性。
     资源编辑器的功能实现主要依托于底层SDK提供的接口,同时,细致周到的用户界面设计和大量方便的组合功能,是编辑器使用层次提高的关键。 Numen Pack Editor 2.0在设计中,仅界面的设计就花费了大量时间,期间主要是对各种细节进行反复测试和修改。

二、关键技术
     总的来说,为了确保资源管理系统的高效,需要实现散列表、目录树、存储空间管理、加密/解密和数据压缩五个主要的接口,但是要实现完整的资源系统,还需要其他大量的基础函数,Numen Game Engine 2.0升级到3.0后,只需要实现前三项技术即可完成整个系统的构建。
     (一)散列表
     众所周知,散列表的出现就是为了解决字符串或非简单数据类型(如32位整数)的索引。一般来说,首先将需要索引的数据进行计算,产生一个32位的索引值,好的算法可以使这个索引值分布比较平均,减少产生冲突的可能性;插入操作时,用这个索引值与索引表大小进行模运算,获得它在索引表的位置,如果计算出的位置为空,则直接插入元素,否则将进行冲突处理;查询操作时,先在计算出的第一个索引表位置进行查找,发现元素则返回,否则继续进行查找;删除操作同查询操作,只是找到元素后会给元素打一个删除标记。
     我们有很多的散列算法可用,这里推荐暴雪在MPQ中开发的索引算法,其实该算法最大的好处在于不在散列表中存储要索引的数据,这样在比较操作时就不需要进行长数据的比较,也可以节约存储空间(后面会说到这个只对索引表有直接益处),举例来说:我们要插入一个字符串”e\world\texture\char\tank.png”(长度29字符),如果我们在索引表中存储完整的字符串,最理想的情况下我们也需要比较29个字符长度的数据,如果发生冲突,还将进行更多的比较。
     在MPQ中引入了两个额外的32位索引值来提供比较,这样在任何情况下都不会进行长数据的比较,如上面那个字符串,我们得到索引值dwHash,计算两个参考值dwCheck1和dwCheck2,这样每次元素比较最多只需进行3次32位整数值的数据比较。值得一提的是,这个算法也并不是万能的,在实际应用中,如果我们不需要大量长索引,那么普通算法的劣势将不明显,同时还将节约dwCheck1和dwCheck2的计算时间。
     另一个关键就是如何在索引表中放置冲突元素,一种方法是用链,另一种方法使用顺序,在MPQ中使用的是顺序存储,这样做的好处是实现简单,但是在最坏情况下,查找一个元素可能将遍历一次索引表,但是在正常游戏中,一般不会查询错误的单元,所以这样的情况很少出现。顺便提一下,在将冲突数据进行链的处理时,由于索引表需要存储在文件中,所以这里链需要改造成索引链(如下一个位置是15),而不是采用内存链表。
     (二)目录树
     在资源包中我们对每个单元的全路径进行索引,如e\texture \tank.png,那么在资源管理器中我们不可能通过遍历索引表来构建整个目录树(在1万个以上项目时速度将会明显变慢),而且我们的索引表也不存储任何名称信息。这就需要我们实现一个类似文件树的接口,它将保存所有的目录信息。
     在Numen Game Engine 3.0中,我们用链表实现了目录树,目录树的单元数据结构如下:
struct FILETREEITEM
{
FILETREEITEM* pPrevItem; // 前一个单元
FILETREEITEM* pNextItem; // 后一个单元
FILETREEITEM* pChildItem; // 子单元
FILETREEITEM* pParentItem; // 父单元
DWORD dwHashIndex; // 索引序号
int nNameLength; // 名称长度
WCHAR strItemName[1]; // 单元名称,不包含路径.(可变长度)
};
     采用“上下左右”四个单元指针的目的是为了提高插入的效率和使单元访问更容易,Numen引擎中用了一个小技巧,就是每个单元的第一个子单元(包括根单元),它的pPrevItem指向的是本级最后一个单元,这样做是为了使插入操作能进行后序插入,普通情况下,要实现这个目的,需要提供一个额外的 pLastItem指针。
     设计目录树时的难点在于指针的正确处理,如果说单向链表很简单,双向链表并不难,那第一次设计这种属性链表时一定不会一帆风顺的。另外还需要注意一些操作影响的不仅仅是一个单元,还将影响所有的子单元。
     使用可变长度的单元名称将节约大量内存空间。
     为了使各种操作有更多的可定制性,我们还需要在对目录树的各种操作中加入回调函数,如下所示:
// 文件操作参数
#define FILETREEOP_RENAMEITEM 0x00000001 // 重命名单元
#define FILETREEOP_COPYITEM 0x00000002 // 复制单元
#define FILETREEOP_MOVEITEM 0x00000004 // 移动单元
#define FILETREEOP_REMOVEITEM 0x00000008 // 删除单元
#define FILETREEOP_OVERWRITE 0x00000010 // 覆盖操作
#define FILETREEOP_INTERNALCREATE 0x00000020 // 内部创建

// 文件树操作回调函数
// 参数: -strSrcName 源单元名称
//        -strDstName 目标单元名称
//        -dwFlags 文件操作参数,参考FILETREEOP宏定义.
//        -lpUserData 用户自定义数据
// 返回: 0: 执行单元操作
//     其他: 放弃单元操作
typedef LRESULT (CALLBACK *LPCBFILETREEOP)( LPCWSTR strSrcName, LPCWSTR strDstName, DWORD dwFlags, LPVOID lpUserData );
     有了这些回调函数,这样我们就可以非常灵活的实现我们的资源管理器了。
     最后值得一提的是,目录树也是实现采用检验值方式的索引表进行重构的基础,因为没有存储索引信息的索引表,是不可能改变自身大小的,必须通过外部记录的索引信息,将索引表扩容后,进行索引重建。
     (三)存储空间管理
     对操作系统比较熟悉的人都知道,文件系统在长时间使用后会产生大量数据碎片,资源管理器也将面临这个问题。在Numen Game Engine 2.0中,我们只是简单的把任何新增的数据加入到资源包的结尾处,可以定期通过压缩资源包来消除空间浪费。
     在 Numen Game Engine 3.0中,我们实现了一个空间管理类,这样任何空间的申请和释放都通过这个类来管理,这个类建立了32个单向链表,每个链表记录一定容量的空闲空间信息,如我们先后释放了4096,89,12589大小的存储空间,然后我们添加大小为70,4000,10000大小的数据,我们将可以申请到重复利用的空间,而不需要增加资源包的大小,同时未用完的空间将被加入到链表中等待分配。
     正是由于存储空间管理器的引入,大大降低了数据的删除和修改操作对资源包大小产生的影响,在配合资源压缩功能,完全不必担心数据碎片带来的空间浪费。
     (四)数据加密/解密
     这个方面的内容可参考的教程和代码较多,算法的强度和效率也各有不同,所以开发商可以根据引擎定位和项目需求进行具体定制。
     资源包中的加密分为文件头加密、索引表加密、目录树加密、数据块信息加密和数据加密,这些加密方式为资源包提供了完整和灵活的保护方案,用户可以根据数据的重要程度和读取效率需求自由定制加密方案。
     (五)数据压缩
     通过提供各种压缩算法,如Huffman,RLE,ZIP,LZW等,为引擎数据提供压缩服务,目前Numen Game Engine 3.0的资源包支持Huffman,RLE和LZW三种压缩算法选择,用户可以自己选择使用他们中的一种或多种对数据进行压缩。

三、资源编辑器功能
     这里将对Numen Pack Editor 2.0中的部分功能进行图文说明:
     (一)压缩数据包
     这里为用户提供了改变索引表大小和资源包加密的机会,资源包索引将会对这个资源包进行重构,花费的时间根据资源包的数据大小,加密强度和压缩等级而定。
     (二)合并资源包
     合并资源包是为了团队配合制作资源而产生,比如美工进行分工后,可以自行制作自己的图形资源,并保存在自己的文件夹中,当资源需要进行整合时,可以统一汇总,而且每个资源包可以保持自己独立的密钥,使团队合作的层次得以体现。
     (三)添加文件
     如果不能批量添加文件到指定文件夹,那添加文件将会变成一件浪费人力的工作,同样我们也需要为一批添加的文件同时指定加密和压缩方案。
     (四)添加文件夹的文件
     这是一个十分重要的功能,当用户有一大批已经制作好的资源需要进行添加时,我们不需要手工建立所有的文件夹,只需要点击几次鼠标,便可将所有资源一次性加入到资源包中。
     (五)更新文件
     如果一个资源编辑器不具备强大和灵活的更新能力,那就不叫资源编辑器了。用户首先选择需要更新的文件,然后选择按序号或者命名更新,就可以一次性完成一批数据的更新操作,而且编辑器提供了更新确认对话框,所以用户不必担心误操作覆盖掉重要的数据。
     (六)文件属性
     如果文件加入资源包后,不能随时查看和编辑它的属性,这将是不可想象的灾难,因为我们根本无法预知未来的需求变化,比如添加时我们希望数据能快速的读取,所以没有给它加入加密和压缩,但是游戏发布后,我们希望保护我们的资源,这是利用文件属性对话框就可以轻易的实现这个目的。
     四、结束语
     Numen Pack Editor 2.0 已经比较完善的支持了资源包管理系统应具备的各种功能,而且在用户接口设计上非常注重细节,为用户进行资源处理提供了高效的手段。但是一切技术都是不断发展和完善的,现在的编辑器也需要在更多的项目实践中不断完善和发展,关于Numen Game Engine和Numen Software的更多信息请随时访问www.numenstudio.com进行查询。



点击打开链接

zpack:一个简单的文件打包格式

January 18th, 2011 zero Leave a comment Go to comments

最近实现了一个简单的文件打包格式

一向很喜欢简单高效的东西,所以非必要的加密,压缩功能一概欠奉。反正也开源了,用户很容易自行添加。

文件以文件名的hash作为检索,为了防止hash冲突的情况,除了用来建立hash table的主hash以外,索引表里还另外保存了两个用不同算法算出来的hash值。这样一来,读取包内文件时并不需要原始的文件名信息,只要根据输入的文件名算出来的3个hash值都和hash table里保存的相应值一致,就基本可以认定是用户所需要的。当然,对于32bit的hash来说冲突的情况实际上很少,而计算hash也有一定开销(虽然和磁盘io相比基本可以忽略),如果包内文件又很少,可以只比较两个,甚至一个hash值。

包内文件采用简单的首尾相连的形式连续存放,索引表放在文件最后,这样在包内文件变化时能最小化写操作。删除文件时,并不把后面的数据前移,而是简单的留下空洞;添加文件时会优先利用之前留下的空洞。这样在多次添加/删除操作后可能会在包内留下一些无法再利用的小碎片,当用户觉得有必要进行整理时(例如碎片空间达到总大小的一定比例),可以调用zpak提供的defrag函数来整理。

虽然包文件看似具有目录结构(见上图),但实际上包内文件并没有以树状形式存储,目录信息只存在于文件名中。为方便起见,在zpack核心代码之外,提供了ZpExplorer类让用户可以以目录形式进行操作,比如删除目录,添加目录,解压目录等。

最终基本上达到了设计目的,无论从功能,接口,代码量上看都不大容易再简化了。原来计划只提供一个命令行工具的,后来也不能免俗,做了一个图中所示类似Windows Explorer的东西。

后记:

很多人向我提出以下几个问题(建议),这里统一解答一下

  • 希望提供一个patch更新机制,方便用在网络游戏中

毫无疑问,网络游戏更新补丁时经常会需要添加或修改资源包里的文件,但这并不意味着patch机制应该成为打包格式的一部分。所谓更新,无非就是添加文件(目录),删除文件(目录),用新的文件代替旧的文件,这些zpack都已经提供了接口,还需要更多的功能吗?

另外有用户提出希望添加包文件合并的功能,也是为了补丁方便。我提出的替代方案是把一个包(比较小的那个)先释放到硬盘上,然后把所有文件/目录添加到另一个包里。虽然直接将包合并或许能快一些(因为可以节省一步中转),但我认为节省的这点时间和下载补丁的时间相比简直是九牛一毛。况且zpack还没有提供压缩功能,补丁包压成zip下载似乎更合算

  • 是否线程安全

不是。一个IPackage(以及打开的IFile)不能在多个线程同时使用,即使是只读的也不行。因为STL的fstream不是线程安全的。

如果需要多线程同时读取包内文件,可以每个线程分别调用zp::open,打开一份独占的IPackage实例,这样就不会互相影响了

  • 用开源的打包格式,岂不是资源包里的东西都随便让人看了

没错。暴雪的mpq你都随便看了,你的游戏有什么需要藏着掖着的?

退一步说,如果希望不要太容易被用户看到包里的东西,可以自己改一下包格式。开源嘛,代码全都在你手里了。

我比较偏向的方案是,如果有需要保密的文件,比如比较敏感的脚本,数据表什么的,可以在打到包里之前就做好加密。



### 回答1: 打包HTML小游戏代码是为了方便在其他设备或环境中运行游戏。以下是打包HTML小游戏代码的步骤: 1. 打开你的HTML小游戏代码文件夹,并确保所有的游戏文件都在同一个文件夹内,包括HTML文件、CSS文件、JavaScript文件和所有游戏资源文件。 2. 使用压缩软件(如WinRAR或7-Zip)将该文件夹压缩成一个ZIP文件。 3. 将ZIP文件上传到一个文件分享网站(如Google Drive、Dropbox或OneDrive),并生成一个下载链接。 4. 发送下载链接给其他人,或者将该链接嵌入到你的网站或社交媒体平台上,以供其他人下载和玩游戏。 如此,其他人就可以从网上下载并玩你的HTML小游戏了。同时,你也可以将该ZIP文件通过USB设备或其他可移动储存设备分享给其他设备运行。 ### 回答2: 首先,需要将所有的 HTML、CSS 和 JavaScript 文件整合到一个文件夹中,以便于进行后续的打包操作。 然后,可以使用打包工具,如 webpack、gulp 等,对这些文件进行打包打包的过程中,可以使用一些插件来压缩代码、混淆代码、去除注释等操作,以提高代码的性能和安全性。 在打包的过程中,可以对不同的浏览器进行兼容性处理,以确保游戏能够在各种浏览器中正常运行。 最后,需要将打包后的代码上传到服务器或者存储在云端,以供用户进行访问和游戏。在上传的过程中,可以对文件进行压缩、加密等操作,以提高文件的安全性和下载速度。 总的来说,HTML 小游戏代码打包是一个比较复杂的过程,需要掌握一定的前端开发知识和打包工具的使用。但是,通过打包可以大大提高代码的性能和安全性,为用户带来更好的游戏体验。 ### 回答3: HTML小游戏的代码打包可以使用多种工具完成。一种常用的工具是webpack,它可以将多个文件打包成一个文件,减少加载时间和带宽使用。打包前,需要创建一个webpack的配置文件,指定需要打包的入口文件和输出文件的路径。在配置文件中指定需要使用的loader和plugin,例如babel-loader将ES6的语法转换为ES5的语法,uglifyjs-webpack-plugin可以对打包后的文件进行压缩和混淆。 使用webpack打包时,可以将所有的HTML、CSS和JavaScript文件都打包在一起,也可将它们分开打包。一般而言,将CSS和JavaScript文件分开打包可以提高加载速度。另外,为了提高用户的体验,可以使用Webpack Dev Server工具,在开发阶段中实时更新页面。 除了webpack,还有其他的工具可以实现HTML游戏的代码打包,例如gulp和grunt。这些工具使用起来有些差异,但是都提供了很好的自动化构建和打包功能,进而让开发者专注于游戏本身而不是一些琐碎的任务。为了保证游戏质量和用户体验,在代码打包之前,应该要进行充分的测试和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值