【游戏引擎架构】6.2 资源管理器

资源管理器可以分为离线部分系统和运行时系统

离线资源管理

数据库

UE的数据库可以直接浏览、编辑资产,看到运行时的状态;但也存在两个较大的缺点:

  1. 版本管理不友好。资产以二进制的大文件存储,无法直接使用版本管理工具比对差异。虽然引擎本身对P4做了diff蓝图的支持,但蓝图以外的内容或者不使用p4进行版本管理的团队还是无法对资产作差
  2. 移动文件有残留。移动文件时会在原来的位置留下一个重定向文件。虽然这种设计可以使得引用链上游的资产不需要更改指向,但会导致冗余数据的留存。对此UE也设计了删除这个中间文件的按钮(位于右键菜单中),不过目前UE4应该还需要手动清理

资产管道

用于将第三方工具创建出来的资产导入引擎使用
资产管道的处理通常分为导出、编译和链接:

  1. 导出
    从第三方工具导出成磁盘上存储的内容
  2. 编译
    经过编译的步骤转化为目标引擎可以识别的格式
  3. 链接
    把资产之间的引用关系链接起来

运行时资源管理

文件结构

可以已有的或自定义文件结构存储。但在进行文件结构设计时,进行打包和压缩有助于加速资源 的加载

  1. 打包
    建议打包是因为这种存储方式可以加快文件的读取
    文件读取的耗时大头可分为三个部分:
    1. 寻道时间
    2. 开启文件的时间
    3. 读取文件的时间
      将n个资源打包成1个 可以减少上述步骤2:开启文件的耗时
      可以参考UE将多个资源文件打成一个pak的方式
  2. 压缩
    压缩资源文件可以加快资源的传输,由于蓝光光盘/DVD等存储媒介数据传输极慢,压缩的加载提升尤为明显

内存管理

需要考虑如何进行内存管理可以最大化内存利用率、减少内存碎片
内存的分配策略可分为以下几种:

  1. 堆分配:最简单粗暴的方式,引擎层完全不考虑内存碎片的存在。依赖操作系统的虚拟分页对内存碎片的映射来解决。但这种方式不适用于虚拟分页做得不太好的主机平台
  2. 堆栈分配:不会产生内存碎片的分配方式。使用单个或者双栈来进行内存的分配
    单栈:内存单向增长,以关卡为单位分配、入栈和出栈。适合以关卡为单位进行的游戏
    双栈:分为上下两个栈,上面的栈内存自上而下增长,下面的栈内存自下而上增长。双栈的设计可用于不同的分工,如一个栈存放常驻内存的资源,另一个存放临时资源。或者一个用于当前正在展示的内容,另一个用于即将使用的内容,实现一个缓冲策略等等
  3. 池分配:设定一个固定的块大小,作为内存分配的基本单位。使用链表串联一个资源的各个块。优点是灵活性和适用度较高;缺点是为了填充成完整块,容易造成资源浪费
  4. 资源组分配:基于池分配器做了一点改进,解决池分配的缺点。
    使用一个链表将所有填充的部分串联起来,需要使用内存的时候从这里查找。但资源组分配的缺点是需要复杂度较高,而且需要考虑生命周期的问题:原资源释放的时候会造成使用其填充内存的资源被部分释放。书中提到了一个解决方案是保证原资源的生命周期>=填充资源的生命周期。比如,一个关卡内部的资源就可以使用这个关卡的填充内存。
  5. 资源文件分段分配:将资源文件根据生命周期或者使用场景划分为不同的段,分给不同的时机加载。
    比如UE4在游戏启动的时候只加载pak文件的头部路径信息,而不真正将所有资源加载到内存。(UE5拆得更彻底,直接将描述信息拆成一个单独的文件.utoc和真正存储资产内容的.uasset)使用到的时候才读取资源部分

文件间引用

存取得时候需要处理引用关系尤其是指针的存取。因为指针实际上只是一个内存地址,程序终止以后这个地址就没有意义了。

但我们需要把内容存储到磁盘上,因此不能直接存此时指针的内容而是需要做一个转换。

转换的方式可以是转为存储GUID或者偏移量

  1. 存储GUID
    Global Unique Identifier 全局唯一标识符,每个资源管理系统都应该设计的资源唯一标识
  2. 存储偏移量
    如果存储的是偏移量,需要另外存储一个指针转换表来进行映射

此外还需要考虑C++的对象引用。对于此书中提出两个解决方案:1. 二进制文件不使用C++对象 2. 使用一个表来映射C++对象的偏移量和其所属的类

最后由于存在文件之间的交叉引用,读取的时候,先将所有的引用内容全部反序列化回内存,再将所有的指针转回内存地址