标签:没有 包括 暂停 内存地址 blog 请求超时 音频 自己 职责
资源及文件系统
载入及管理多种媒体,是游戏引擎必须具备的能力。多数引擎会采用某种类型的资源(或资产)管理器,载入并管理游戏所需的资源,并确保在同一时间每个媒体文件只可载入一份。每个资源管理器都会大量使用文件系统。本文将介绍现代三维游戏引擎中的各种文件系统API,再分析典型资源管理器的运作方式。
文件系统
文件名和路径
关于文件和文件夹路径的概念,绝对路径和相对路径的概念,它们在各种操作系统之间的区别,属于常识范畴,此处不赘述。
关于搜寻路径,是指含若干个路径(以特殊字符分隔)的字符串,寻找文件时会从这些路径逐个寻找,PATH环境变量就是一种搜寻路径。在运行期搜寻资产是费时的做法,而通常资产路径会在运行期之前就得知,所以应该完全避免搜寻资产。
关于路径API,一般用于对路径进行多种操作,如分离“目录/文件名/扩展名”、使路径规范化、绝对和相对路径互转等等。游戏引擎通常会实现或封装轻量化的路径处理API,以便实现跨平台,从各种特殊的储存媒体(如记忆棒、DVD盘、网络文件系统等等)中存取数据,以及提供操作系统API未能提供的功能,如串流(即在游戏运行中同时载入数据)。
许多游戏引擎都会把文件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()
的虚函数用于独立初始化和销毁工作。
载入后初始化和资源内存分配策略息息相关,有时初始化会在文件的数据上新增数据(如额外计算类中的成员数据),有时初始化的数据用来取代己载入的数据(如引擎载入过时格式的网格数据,自动转换为最新格式,以保证向后兼容)。可以采用先载入到临时内存区域,初始化完成后再把相关数据复制到内存最终位置(例如《迅雷赛艇》的引擎)。
标签:没有 包括 暂停 内存地址 blog 请求超时 音频 自己 职责
原文地址:http://www.cnblogs.com/yeqluofwupheng/p/7700559.html