资源及文件系统程序员
载入及管理多种媒体,是游戏引擎必须具有的能力。多数引擎会采用某种类型的资源(或资产)管理器,载入并管理游戏所需的资源,并确保在同一时间每一个媒体文件只可载入一份。每一个资源管理器都会大量使用文件系统。本文将介绍现代三维游戏引擎中的各类文件系统API,再分析典型资源管理器的运做方式。数据库
文件系统网络
文件名和路径数据结构
关于文件和文件夹路径的概念,绝对路径和相对路径的概念,它们在各类操做系统之间的区别,属于常识范畴,此处不赘述。异步
关于搜寻路径,是指含若干个路径(以特殊字符分隔)的字符串,寻找文件时会从这些路径逐个寻找,PATH环境变量就是一种搜寻路径。在运行期搜寻资产是费时的作法,而一般资产路径会在运行期以前就得知,因此应该彻底避免搜寻资产。数据库设计
关于路径API,通常用于对路径进行多种操做,如分离“目录/文件名/扩展名”、使路径规范化、绝对和相对路径互转等等。游戏引擎一般会实现或封装轻量化的路径处理API,以便实现跨平台,从各类特殊的储存媒体(如记忆棒、DVD盘、网络文件系统等等)中存取数据,以及提供操做系统API未能提供的功能,如串流(即在游戏运行中同时载入数据)。ide
许多游戏引擎都会把文件I/O API封装成自定义的API,这样至少有三个好处:函数
保证I/O API在全部目标平台上均有相同行为;工具
API能够简化到只剩下实际须要的函数,使维护开支维持最小限度;布局
可提供延伸功能,如处理各类特殊的储存媒体(同自定义路径处理API)。
每次调用输入/输出,都须要称为缓冲区的数据区块,以供程序和磁盘之间传送字节。当API负责管理数据缓冲,就称之为有缓冲功能的API,不然为无缓冲。C标准程序库中,以f开头的文件API是带缓冲的,如fread()
,没有f开头是无缓冲的,如read()
。有时自行管理缓冲区是有必要的。例如往日志写数据可能会显著下降性能,能够先把数据累积在内存缓冲,满溢后才写进盘内,甚至把缓冲输出函数置于另外一线程里,以免令主游戏循环发生流水线停顿。
C标准库的两种文件I/O库都是同步的,即程序发出I/O请求之后,必须等待读/写数据完毕,程序才能继续运行。
串流是指在背景载入数据,而主程序同时继续运行。为了支持串流,必须使用异步文件I/O库。多数异步I/O库允许主程序在请求发出后一段时间,等待I/O操做完成才继续运行。有些异步I/O库允许程序员取得某异步操做所需时间的估算,一些API也能够为请求设置时限,并设置请求超时的安排(例如取消请求、通知程序、继续尝试等)。
异步I/O操做常有不一样的优先权,例如从硬盘中串流音频,而且在串流其余资源时播放音频,显然前者优先权高于后者。异步I/O系统必须能暂停较低优先权的请求,才可让较高优先权的I/O请求有机会在时限前完成。
异步文件I/O的实现原理,通常是利用另外一线程进行同步操做来实现。主线程调用异步函数时,会把请求放入一个队列,并当即传回。同时,I/O线程从队列中取出请求,并以阻塞I/O函数处理这些请求。请求的工做完成后,就会调用主线程以前提供的回调函数告之该操做己完成。若主线程选择等待完成I/O请求,就会使用信号量处理(每一个请求对应一个信号量,主线程把自身处于休眠状态,等待I/O线程在完成请求工做后通知信号量)。
资源管理器
资源管理器由两部分组成:一部分负责管理离线工具链,用来建立资产并把它们转换成引擎可用的形式;另外一部分在执行期管理资源,确保资源在使用前已载入内存,不须要时从内存卸下。
离线资源管理与工具链
资产的版本控制
小型的游戏项目中,游戏资产的管理方式能够是把组织不严谨的文件以项目特设的目录结构置于公用网盘中;有些游戏团队使用源码版本控制工具来管理资源。
可是,艺术资产一般有极大的数据量,直接从中央版本库复制到本地每每是低效的。如下是一些参考解决方案:
游戏引擎不会使用多数资产本来的格式,而是须要经过一些资产调节管道(ACP)将资产转换为引擎所需的格式,其中每一个资源须要有元数据描述如何对资源进行处理。例如描述压缩纹理时,使用哪一种压缩方法;描述导出动画片断时,导出哪一个范围的帧。
为了管理这类元数据便须要某种数据库。不一样的引擎差异巨大,有的是嵌入到资产源文件自己,有的是每一个资产源文件伴随一个小文本文件,有的将元数据写进XML文件中,有的使用真正的关系数据库。它通常提供如下功能:
用Perforce以提供版本控制,元数据改成XML。Builder管理演员(包含行为的动态对象)和关卡(含静态背景网格和关卡信息等)两种类型的资源,动画能够组成名为动画包(buddle)的伪文件夹;引擎含一组基于命令行的工具,用于查询数据库,处理资源原生DCC文件,生成某演员或关卡。
资产调节管道用于将DCC原生格式文件转换成引擎可用的形式,通常通过3个处理阶段:
如同程序的源文件,各资产之间也有依赖关系。这些依赖关系一般会影响资产在管道内的处理次序,也可告诉咱们,当某个源资产作出改动后,要从新生成哪些资产。生成依赖不单围绕资产自己的改动,也关系到数据格式的改动。每一个资产调节管道都须要一组规则来描述资产间的依赖关系,并本身搭建系统或使用像make这样的工具来以正确顺序生成资产。必定要管理好资产间的依赖。
资源通常储存为磁盘上的文件,并位于使创做者方便而组织的树状目录中。但引擎一般不会理会资源被放置于资源树中的哪一个位置,引擎会把多个资源包裹为单一文件。文件载入时间和寻道时间、开启每一个文件的时间、从文件读至内存的时间相关。这种方法能减小文件载入时间。
OGRE使用ZIP存档资源,ZIP格式的好处:
虚幻3采起相似的手法,可是其全部资源都必须置于大型的pak自定义格式文件中,并不允许资源以盘上独立文件出现。
每类资源均可能有不一样的文件格式。单一文件格式也可储存多种不一样类型的资产。许多引擎会自定义文件格式,由于引擎所需部分信息可能没有标准格式能够支持,以及对资源脱机处理,以让其听从某种内存布局加速运行时载入。
资源全局统一标识符
全部资源都须要资源全局统一标识符(GUID)来识别,最多见就是使用资源的文件系统路径。也有使用128位散列码。虚幻3的GUID格式是包名和包内资源路径串接而成,如《战争机器》的一个资源GUID为Locust_Boomer.PhysicalMaterials.LocustBommerLeather
。
资源管理器都含某种形式的资源注册表,以保证在任什么时候间,载入内存的每一个资源只会有一份副本。最简单的实现方法是使用字典,键为资源的GUID,而值是指向内存中资源的指针。资源载入内存时,加进资源注册表字典。卸下资源时,就删除其注册表记录。
若不能从表中找到请求的资源,最直觉的处理手法就是自动载入该资源。但这样作可能会由于临时从硬盘或光驱等缓慢设备读取数据而严重拖慢游戏帧率。
所以引擎可采起这两种替代手法:
资源管理器的职责之一是自动管理资源生命期,或对游戏提供所需API供手动管理。每一个资源对生命期有不一样需求:
某资源的载入时期一般在玩家第一次看见该资源便能决定,但什么时候卸下资源归还内存,就难以回答,由于可能存在多个关卡共享的资源。解决方案之一就是对资源引用计数,即载入新关卡时,遍历所需资源并引用加1,再遍历即将结束的关卡的资源,全部引用减1。当有引用计数减为0是卸载,当有新的资源的引用计数由0变为1时载入。
资源加载的内存位置可能不一样,像纹理、顶点缓冲、着色器驻留在显存,大部分资源驻留在主内存,但不一样的资源可能须置于不一样的地址范围。设计游戏引擎时,内存分配器和资源系统要相互配合。有时用已有的内存分配器来设计资源系统,有时则要让内存分配器配合资源管理所需。
每一个资源文件可包含一个或多个数据对象,这些对象可能以不一样的方式引用或依赖其余对象,资源数据库能够表达为相互依赖的数据对象所组成的有向图。交叉引用能够分为内部(单个文件里对象间的引用)和外部(引用另外一个文件的对象)。
处理资源内部引用
在C++中, 因为指针的内存地址总会变,并且离开运行中的程序就失去意义,因此不能用指针来表示对象间的依赖。
将资源引用存为包含全局惟一标识符(GUID)的字符串或散列码,资源管理器要维护一个全局资源查找表,其中键为GUID,值为资源在内存中的地址。这样每次经过全剧资源查找表就能够将资源对象的GUID转换为指针。
储存对象到二进制文件的另外一经常使用方法是,把指针转换为文件偏移值,并创建指针修正表。
下图给出了储存二进制文件以及将文件载入内存的指针修正示意图,具体过程为:
从文件载入C++对象,建立对象时必须调用构造函数。这个问题有两个常看法决方案:
void* pObject = ConvertOffsetToPointer(objectOffset);
::new(pObject) ClassName; // placement new语法,ClassName为对象所属的类名
处理资源外部引用
要正确表示外部引用,除了指明偏移值或GUID,还要加上资源对象所属文件的路径。通常作法是:载入每一个资源文件时,扫描文件中的交叉引用表,并载入全部被外部引用但未载入的资源文件,当载入全部互相依赖的资源时,就用主查找表把全部指针转换成真实的内存地址(经过GUID或文件偏移值)。
有一些资源载入后须要一些处理才能供引擎使用,这种载入后的全部处理被称为载入后初始化。
资源的载入后初始化和拆除,都有独特的需求。在C中,可使用查找表,把每一个资源类型映射到一对函数指针,一个负责载入后初始化,一个负责拆除。在C++中,可使用构造函数和析构函数来处理载入后初始化和拆除。可是为了方便多态,通常为每一个类设置如Init()
和Destroy()
的虚函数用于独立初始化和销毁工做。
载入后初始化和资源内存分配策略息息相关,有时初始化会在文件的数据上新增数据(如额外计算类中的成员数据),有时初始化的数据用来取代己载入的数据(如引擎载入过期格式的网格数据,自动转换为最新格式,以保证向后兼容)。能够采用先载入到临时内存区域,初始化完成后再把相关数据复制到内存最终位置(例如《迅雷赛艇》的引擎)。