【游戏引擎架构】6.2 资源管理器
资源管理器可以分为离线部分系统和运行时系统
离线资源管理
数据库
UE的数据库可以直接浏览、编辑资产,看到运行时的状态;但也存在两个较大的缺点:
- 版本管理不友好。资产以二进制的大文件存储,无法直接使用版本管理工具比对差异。虽然引擎本身对P4做了diff蓝图的支持,但蓝图以外的内容或者不使用p4进行版本管理的团队还是无法对资产作差
- 移动文件有残留。移动文件时会在原来的位置留下一个重定向文件。虽然这种设计可以使得引用链上游的资产不需要更改指向,但会导致冗余数据的留存。对此UE也设计了删除这个中间文件的按钮(位于右键菜单中),不过目前UE4应该还需要手动清理
资产管道
用于将第三方工具创建出来的资产导入引擎使用
资产管道的处理通常分为导出、编译和链接:
- 导出
从第三方工具导出成磁盘上存储的内容 - 编译
经过编译的步骤转化为目标引擎可以识别的格式 - 链接
把资产之间的引用关系链接起来
运行时资源管理
文件结构
可以已有的或自定义文件结构存储。但在进行文件结构设计时,进行打包和压缩有助于加速资源 的加载
- 打包:
建议打包是因为这种存储方式可以加快文件的读取
文件读取的耗时大头可分为三个部分:- 寻道时间
- 开启文件的时间
- 读取文件的时间
将n个资源打包成1个 可以减少上述步骤2:开启文件的耗时
可以参考UE将多个资源文件打成一个pak的方式
- 压缩:
压缩资源文件可以加快资源的传输,由于蓝光光盘/DVD等存储媒介数据传输极慢,压缩的加载提升尤为明显
内存管理
需要考虑如何进行内存管理可以最大化内存利用率、减少内存碎片
内存的分配策略可分为以下几种:
- 堆分配:最简单粗暴的方式,引擎层完全不考虑内存碎片的存在。依赖操作系统的虚拟分页对内存碎片的映射来解决。但这种方式不适用于虚拟分页做得不太好的主机平台
- 堆栈分配:不会产生内存碎片的分配方式。使用单个或者双栈来进行内存的分配
单栈:内存单向增长,以关卡为单位分配、入栈和出栈。适合以关卡为单位进行的游戏
双栈:分为上下两个栈,上面的栈内存自上而下增长,下面的栈内存自下而上增长。双栈的设计可用于不同的分工,如一个栈存放常驻内存的资源,另一个存放临时资源。或者一个用于当前正在展示的内容,另一个用于即将使用的内容,实现一个缓冲策略等等 - 池分配:设定一个固定的块大小,作为内存分配的基本单位。使用链表串联一个资源的各个块。优点是灵活性和适用度较高;缺点是为了填充成完整块,容易造成资源浪费
- 资源组分配:基于池分配器做了一点改进,解决池分配的缺点。
使用一个链表将所有填充的部分串联起来,需要使用内存的时候从这里查找。但资源组分配的缺点是需要复杂度较高,而且需要考虑生命周期的问题:原资源释放的时候会造成使用其填充内存的资源被部分释放。书中提到了一个解决方案是保证原资源的生命周期>=填充资源的生命周期。比如,一个关卡内部的资源就可以使用这个关卡的填充内存。 - 资源文件分段分配:将资源文件根据生命周期或者使用场景划分为不同的段,分给不同的时机加载。
比如UE4在游戏启动的时候只加载pak文件的头部路径信息,而不真正将所有资源加载到内存。(UE5拆得更彻底,直接将描述信息拆成一个单独的文件.utoc和真正存储资产内容的.uasset)使用到的时候才读取资源部分
文件间引用
存取得时候需要处理引用关系尤其是指针的存取。因为指针实际上只是一个内存地址,程序终止以后这个地址就没有意义了。
但我们需要把内容存储到磁盘上,因此不能直接存此时指针的内容而是需要做一个转换。
转换的方式可以是转为存储GUID或者偏移量
- 存储GUID
Global Unique Identifier 全局唯一标识符,每个资源管理系统都应该设计的资源唯一标识 - 存储偏移量
如果存储的是偏移量,需要另外存储一个指针转换表来进行映射
此外还需要考虑C++的对象引用。对于此书中提出两个解决方案:1. 二进制文件不使用C++对象 2. 使用一个表来映射C++对象的偏移量和其所属的类
最后由于存在文件之间的交叉引用,读取的时候,先将所有的引用内容全部反序列化回内存,再将所有的指针转回内存地址