中EN
概念页

资源管理器

Resource Manager

游戏由网格、材质、纹理、着色器、动画、音频、关卡布局、碰撞与物理参数等大量资源(也叫资产、媒体)构成。内存有限,所以引擎要保证每个资源在内存中只有一份(五个网格共用一张纹理时只载入一份)。每个引擎都有某种资源管理器:有的是统一集中的子系统,有的散布在多年间由不同人写的各个子系统中,但职责和要解决的问题是一样的。本页讲运行时部分:职责、文件组织与归档、文件格式、资源 GUID、资源注册表、生命周期(引用计数)和资源的内存管理;交叉引用与加载后初始化见 资源交叉引用与加载后初始化,离线工具链见 资产管线与工具链。

运行时资源管理器的职责(卷 I 7.2.2.1)

  • 保证每个唯一资源在任意时刻只有一份在内存中;
  • 管理每个资源的生命周期,加载需要的、卸载不再需要的;
  • 加载复合资源(由其他资源构成,如 3D 模型 = 网格 + 材质 + 纹理 + 可选骨骼与动画);
  • 维护引用完整性:内部(单个资源内的交叉引用)与外部(资源之间),加载复合资源时确保子资源都载入并正确修补交叉引用;
  • 管理已加载资源的内存,放到合适的位置;
  • 允许按资源类型做加载后的自定义处理(logging in / 加载初始化);
  • 通常(但不总是)提供统一接口管理各种资源,并易于扩展新类型;
  • 引擎支持时,处理流式(异步)加载(见 引擎文件系统与异步文件读写)。

文件与目录组织(卷 I 7.2.2.2)

  • 有的引擎(通常是 PC 引擎)每个资源一个「散」文件,目录树主要为创作者方便而设计,引擎不在乎文件放在哪。书中假想游戏《Space Evaders》的树:Resources/NPC/{Pirate,Marine}、Player、Weapons/{Pistol,Rifle,BFG}、Levels/{Level1,Level2}、Objects/{Crate,Barrel}(「无处不在的可破坏箱子和爆炸桶」)。
  • 有的引擎把多个资源打包进一个文件:OGRE 允许散文件或大 ZIP 包中的虚拟文件(ZIP 是开放格式,zlib 完全免费,zziplib 为 LGPL);PlayStation 用 psarc 归档;Unreal 要求所有资源都在专有格式的包 (package / pak) 中,不允许散文件,由 UnrealEd 创建和管理。
  • 归档格式的四个好处:
    1. 虚拟文件「记得」相对路径,挂载后的归档看起来就像文件系统;OGRE 和 PlayStation 的 FIOS 都用形如路径的字符串标识资源,程序员多数时候无需关心是散文件还是归档中的虚拟文件。
    2. 可压缩:省磁盘,更重要的是加载更快——尤其从比硬盘和 SSD 慢得多的蓝光盘读取时,解压开销常被少读数据省下的时间抵消。PS5 有专用硬件以极高速度解压 Oodle 格式(RAD Game Tools 开发的高效压缩算法)的 psarc,不占 CPU。
    3. I/O 更高效:机械硬盘上三大开销是寻道、打开每个文件、读数据,前两者不可忽视,单个大文件能把它们降到最低。SSD 没有寻道问题;PS5 和 Xbox Series X 都以高速 M.2 SSD 为主存储,所以寻道只在读蓝光盘时才是问题。
    4. 促进模块化:Naughty Dog 支持 N 种语言时,每个文本和音频资产有 N 个版本,同名但放在 N 个不同的 psarc 中;运行时只挂载玩家所选语言的归档,切换语言时卸载旧的、挂载新的,引擎其他部分无需知道当前语言(本地化见 游戏本地化与 Unicode)。

资源文件格式(卷 I 7.2.2.3)

  • 有的用开放标准格式:纹理用 TGA、PNG、TIFF、JPEG、BMP,或 DirectX 的 S3TC(DXTn/DXTC)压缩格式;网格从 Maya、Lightwave 导出为 OBJ 或 COLLADA。
  • 一种格式也可装多种资产,如 RAD Game Tools 的 Granny SDK 格式可存网格、骨骼层级和骨骼动画(几乎可改作存任何数据)。
  • 许多引擎自定义格式:标准格式缺少所需信息;或为了尽量把处理放到离线,以缩短运行时加载处理时间——例如数据需要特定内存布局时,用原始二进制格式,由离线工具排好布局。

资源 GUID(卷 I 7.2.2.4)

  • 每个资源都要有全局唯一标识符 (GUID)。最常见的是资源的文件系统路径(存为字符串或 32 位哈希):直观、映射到磁盘文件,且操作系统保证路径唯一。
  • 也有引擎用不直观的 GUID,如由工具分配并保证唯一的 128 位哈希。
  • Unreal 的资源在大包文件里,包路径不能唯一标识单个资源(GEA 这里的描述对应 UE3 时代;现行 UE4/UE5 编辑器中通常一个资产一个 .uasset 包文件,.pak/IoStore 容器只是烘焙打包后的发行形式——见 烘焙、打包与 Zen 加载器 及 Epic 文档「Cooking Content and Creating Chunks」。书中「Unreal 不允许散文件、所有资源必须在大包中」的说法因此只适用于旧版),所以包内组织成文件夹层级,GUID = 包名 + 包内路径。例:《战争机器》中 Locust_Boomer.PhysicalMaterials.LocustBoomerLeather 指 Locust_Boomer 包里 PhysicalMaterials 文件夹下名为 LocustBoomerLeather 的材质。(字符串 ID 见 字符串与唯一标识符。)

资源注册表(卷 I 7.2.2.5)

  • 为保证唯一副本,多数资源管理器维护已加载资源的注册表,最简单就是字典:键为 GUID,值为指向内存中资源的指针(字典实现见 容器与哈希表)。加载时加入、卸载时移除;请求时按 GUID 查找,找到返回指针,找不到则自动加载或返回失败码。
  • 找不到就自动加载看似直观,有些引擎也这么做,但问题严重:加载很慢(定位打开文件、从可能很慢的蓝光盘读大量数据、可能还要加载后初始化),游戏中途请求会造成明显卡顿甚至数秒冻结。所以引擎通常二选一:
    1. 游戏进行中完全禁止加载:关卡全部资源在开始前集中加载,玩家看读取画面或进度条;
    2. 异步加载(流式):玩家在关卡 X 时后台加载关卡 Y。体验无读取画面,更好,但实现难得多。

资源生命周期(卷 I 7.2.2.6)

生命周期是从首次载入到内存被回收挪作他用的时间段。资源管理器可以自动管理,或提供 API 让游戏手动管理。

类型生命周期例子
全局资源(全局资产)游戏启动时加载,常驻整个游戏玩家角色的网格、材质、纹理和核心动画;HUD 纹理和字体;全程使用的标准武器。凡全程可见/可听且不能临时加载的都应作全局资源
关卡资源与关卡相同玩家首次看到关卡前载入,永久离开后丢弃
短于关卡更短过场动画的动画与音频片段,播放前载入,播完丢弃
流式资源难以定义背景音乐、环境音效、全屏影片边播边读;按硬件要求的块大小读取,如音乐按 4 KiB 块(可能是底层音频系统的缓冲大小),内存中只有两块:正在播放的和正在载入的下一块
  • 何时加载容易回答(玩家首次见到之前),何时卸载难:很多资源跨关卡共享,不能在关卡 X 结束时卸载、又因关卡 Y 立刻重新加载。
  • 引用计数方案:加载新关卡时遍历其资源表,每个资源计数 +1(先不加载);再遍历不再需要的关卡的资源,计数 −1,降到 0 的卸载;最后加载计数刚从 0 变 1 的资源。
  • 书中表 7.2 的例子:关卡 X 用 A、B、C,关卡 Y 用 B、C、D、E(B、C 共享)。
事件ABCDE
初始00000
X 计数 +111100
X 加载(1)(1)(1)00
X 进行中11100
Y 计数 +112211
X 计数 −101111
X 卸载、Y 加载(0)11(1)(1)
Y 进行中01111

括号表示正在加载或卸载(原书用粗体表示在内存中、灰底表示不在内存中)。B、C 始终留在内存中,没有被卸载后再加载。

资源的内存管理(卷 I 7.2.2.7)

资源加载后放哪里与内存管理密切相关:纹理、顶点缓冲、索引缓冲、着色器代码等必须在显存(PS4 上是映射为经高速「garlic」总线访问的内存块);其余多数可在主内存,但不同资源可能放在不同地址范围(常驻的全局资源一处,频繁加载卸载的另一处)。内存分配子系统的设计常与资源管理器紧密相关——可能为现有分配器设计资源管理器,也可能反过来。核心问题是加载卸载时避免碎片(见 自定义内存分配器)。

方案做法适用 / 取舍
基于堆忽略碎片,用 malloc / 全局 new最适合支持虚拟内存的 PC:物理内存会碎片化,但映射到连续虚拟空间能缓解。物理内存有限、虚拟内存管理简陋或没有的主机上碎片会成问题,可定期碎片整理
基于栈启动时先分配全局资源(load-and-stay-resident,LSR)并取标记;加载关卡时在栈顶分配;关卡结束回滚到标记,一次释放全部关卡资源且不碰全局资源条件:游戏线性、以关卡为中心(读取画面—关卡—读取画面)且每个关卡能整个放进内存。永不碎片化
双端栈两个栈在同一块内存中相向增长,可互相调剂《Hydro Thunder》:下栈放持久数据,上栈放每帧释放的临时分配。Bionic Games 某项目用它乒乓加载关卡:当前关卡 A 以解压形式在下栈,关卡 B 的压缩版载入上栈;切换时清空下栈,把 B 从上栈解压到下栈。解压远快于读盘,基本消除关卡间读取时间
基于池(分块)资源数据按等大块加载,用池分配器分配,卸载不产生碎片。每块关联某个关卡(如每关一个块链表),多个关卡同时在内存时也能各自管理生命周期支持流式加载的引擎常用。代价一:资源数据必须按「分块」设计,避免大于一块的连续结构(数组、大结构体),改用块内放得下或不需连续内存的结构(如链表)。代价二:文件大小不是块的整数倍时最后一块浪费;块越小浪费越少但布局限制越苛刻(极端情况块为 1 字节时任何结构都不能超过 1 字节)。块大小「通常为若干 KiB 量级」,但紧接着举的例子是 Naughty Dog 的流送系统在 PS3 上用 512 KiB 块、PS4 上用 1 MiB 块(原书问题:两句同处卷 I 7.2.2.7,一说「几 KiB」一说「512 KiB–1 MiB」,量级相差两到三个数量级,属书内自相矛盾;以书中给出的 ND 实际数值为准更可信);块大小最好是操作系统 I/O 缓冲大小的整数倍
  • 资源块分配器(GEA 自拟的名字):维护所有含空闲空间的块及其空闲区域的链表,从中按任意策略分配(如用通用堆管理空闲链表,或在每块空闲区映射一个小栈分配器)。问题是块只能整块释放,块里分配的内存会随资源卸载「凭空消失」。解决:只用某关卡的块满足生命周期与该关卡一致的请求,分配器按关卡分别管理,调用者需指明为哪个关卡分配。由于加载资源时除文件本身外通常还要动态分配内存,这能有效回收原本浪费的块空间。
  • 分段资源文件:一个资源文件含 1–4 个段 (section),每段再分块:主内存数据段、显存数据段、加载期间临时使用后丢弃的段、调试信息段(调试模式加载,最终发行版根本不加载)。
  • PS5 的 AMPR:AMM 库控制物理到虚拟页映射,APR 库向 M.2 SSD 和硬件解压器发送命令列表。Naughty Dog 最近把大部分资产加载改为基于 AMPR:预分配一大段连续虚拟地址,每个资产映射到其中一个子范围;流入时让 AMM 为该范围映射物理页,再让 APR 把数据加载并解压进去;流出时只需让 AMM 取消映射,物理页回到空闲池供其他资产复用。(AMPR 介绍见 引擎文件系统与异步文件读写。)

与 UE5 的对照

UE5 的运行时资产引用与异步加载见 资产管理与异步加载。GEA 对 Unreal 的描述(包文件、包名 + 包内路径作 GUID、UnrealEd 管理)是概念层面的,具体机制以该页的 UE 官方来源为准。

  1. Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 7 章 7.2、7.2.2.1–7.2.2.7 节

相关页面

游戏开发资产管线与工具链Asset Conditioning Pipeline & Tools游戏引擎的输入数据来自美术使用的数字内容创作 (DCC) 工具,但 DCC 格式几乎从不直接用于游戏,必须经过资产调理管线 (Asset Conditioning Pipeline, ACP) 导出、处理并按平台打包。围绕它还有世界编辑器、记录资产元数据的资源数据库,以及几种工具架构(独立工具、与引擎共享底层、编辑器内嵌于引擎、Web 界面),各有取舍。游戏开发引擎文件系统与异步文件读写Engine File System & Asynchronous File IO资源管理器大量依赖文件系统。引擎通常把操作系统的文件 API 包装成自己的 API:一是跨平台、屏蔽硬件差异;二是操作系统缺少引擎需要的功能(如流式加载),主机还要统一访问记忆棒、硬盘、蓝光盘、网络文件系统等介质。引擎文件系统 API 通常涵盖:文件名与路径操作、打开/关闭/读写文件、扫描目录、处理异步 I/O 请求(流式加载)。游戏开发资源交叉引用与加载后初始化Resource Cross-References & Post-Load Initialization资源之间以任意方式相互引用(网格引用材质,材质引用纹理),整个资源数据库是一张有向依赖图。内存中的指针离开运行中的程序就失去意义,所以存盘时要把引用改写为 GUID 或文件内偏移,加载后再修正 (fix up) 回指针;含非 POD 的 C++ 对象还要用 placement new 调用构造函数;跨文件的外部引用要先加载全部相关文件。加载后通常还需要按资源类型做加载后初始化(以及释放前的拆除)。游戏开发自定义内存分配器Custom Memory Allocators内存从两方面影响性能:动态分配(malloc / 全局 new)很慢,以及访问模式主导现代 CPU 的性能。GEA 的经验法则是「尽量少做堆分配,绝不在紧密循环里做堆分配」,并介绍游戏引擎常用的自定义分配器:栈分配器(含双端栈)、池分配器、对齐分配、单帧与双缓冲分配器;以及如何用栈/池避免碎片,或用碎片整理加指针重定位(智能指针、句柄)解决碎片。游戏开发字符串与唯一标识符Strings & Unique Identifiers字符串在 C/C++ 中只是字符数组,变长意味着要么硬编码长度上限、要么动态分配;比较和复制都是 O(n)。引擎内部大量用字符串做资源名和对象 ID,处理不当会严重拖慢游戏。GEA 的建议:运行时代码尽量避免字符串类,若用则了解其成本并按引用传递;路径类是值得专门做的例外;对象与资产的唯一标识用哈希字符串 ID(string id),兼得字符串的可读性与整数的比较速度。游戏开发容器与哈希表Containers & Hash Tables容器的职责都是容纳和管理零个或多个元素,差别在于实现方式与取舍。GEA 概览了常见容器与操作、迭代器、大 O 复杂度与内存布局的综合考量,讨论引擎为何常自研容器、C++ 标准库 / Boost / Folly / Loki 的利弊,并重点讲了动态数组的增长策略与哈希表(开放哈希与封闭哈希、哈希函数、线性/二次探测、Robin Hood 哈希)。游戏开发 · UE5资产管理与异步加载Asset Management & Async LoadingUE 资产默认按引用自动加载:硬引用 (Hard Reference) 会让被引用资产随引用者一起加载,不加控制就可能在启动时加载全部内容。软引用 (Soft Reference,TSoftObjectPtr/FSoftObjectPath) 只存路径,配合流式管理器 (Streamable Manager) 异步按需加载;资产管理器 (Asset Manager) 进一步把内容划分为主资产 (Primary Asset) 和次资产 (Secondary Asset),用资产包 (Asset Bundle) 管理加载、卸载、烘焙分块与内存审计。游戏开发游戏本地化与 UnicodeGame Localization & Unicode本地化(国际化,I18N)是把软件改编为其他语言版本,最好从第一天起规划并贯穿每个开发步骤。GEA 介绍了 Unicode 与 UTF-32/UTF-8/UTF-16/UCS-2 编码、char 与 wchart、Windows 的字符串 API 约定、主机上的选择,字符串之外的本地化问题,以及核心基础设施——字符串数据库 + 按 ID 查找当前语言字符串的运行时函数,并以 Naughty Dog 的本地化工具为例。图形学细节层次Level of Detail细节层次 (LOD) 在物体对图像贡献变小时改用更简单的表示。LOD 系统由三部分组成:生成(简化或手工制作多个版本)、选择(按距离、投影面积等收益函数挑选)、切换(尽量减少跳变 popping)。LOD 主要减少顶点处理,也通过减少 quad 过度着色降低像素开销;进一步还可在预算约束下全局选择 LOD,实现时间关键(恒定帧率)渲染。游戏开发动画压缩Animation Compression未压缩的动画每关节每样本最多 10 个 32 位浮点通道,数据量巨大。GEA 列出的压缩手段按力度递增:省略无用通道、量化(如 16 位 RL 编码)、降低采样率或省略近似线性的关键帧、用 B 样条拟合曲线、小波压缩,以及最根本的:只把需要的片段放进内存(选择性加载与流式加载)(GEA4 卷 II 13.8)。游戏开发音频引擎架构Audio Engine Architecture音频渲染软件和多数系统一样是分层的"栈":硬件 → 驱动 → 底层 API → 3D 音频引擎,功能集通常模仿录音棚的多通道调音台。每个 2D/3D 声音是一条语音 (voice),沿语音总线经过编解码、增益、预发送滤波、辅助发送分出干湿两路、混响、后发送滤波和声像,然后由主混音器合成多声道信号,再经主输出总线(预放、LFE 生成、EQ、压缩、主音量)送到扬声器。资产上分片段、音效提示 (cue)、声音库和流式声音;混音靠分组、闪避、总线预设与混音快照、实例限制和语音抢占(GEA4 卷 II 15.5)。游戏开发游戏世界的加载与流送Loading & Streaming Game Worlds世界加载系统负责两件事:把世界块和所需资源从磁盘读进内存的文件 I/O,以及这些资源的内存分配与释放;另外还要管理游戏对象的生成与销毁。GEA 按演进顺序介绍:一次只载一个关卡的栈式加载(LSR 资源压栈底),用小块气闸 (air lock) 区域实现基本无缝的过渡,再到把所有资产切成等大块、用块池分配器按关卡加载区域流送的细粒度方案(Naughty Dog);对象生成的内存问题有"不许运行时分配"、按尺寸分池、小内存分配器和内存重定位四种对策;存档则分检查点和随处存档(GEA4 卷 II 17.4)。综合引擎运行时架构总览Engine Runtime Architecture Overview本页把 wiki 中分散在 GEA 第 1、6、8、17 章及 GPP、UE5 相关页面里的内容串成一条线,回答"一个游戏引擎在运行时是怎样组织起来的":静态结构上是自下而上的分层,上层依赖下层;启动关闭按依赖顺序显式进行;每一帧由主循环驱动各子系统批量更新,游戏对象的逻辑被安排在子系统阶段之间,并按依赖分桶或设先后关系;多核上一帧被拆成成百上千个作业;玩法基础系统(对象模型、事件、脚本、世界加载流送、高层流程)位于引擎与游戏的分界线附近。各结论的细节与出处见所链接的概念页。综合游戏中的内存管理Memory Management in Games本页把 wiki 中关于内存的内容串成一条线:内存从两方面影响游戏,一是访问模式(缓存未命中远比计算昂贵),二是分配本身(通用堆分配慢且产生碎片,而主机上没有自动换页的虚拟内存兜底)。对策按生命周期组织:常驻与关卡数据用栈、每帧临时数据用单帧/双缓冲分配器、同尺寸小对象用池或小内存分配器、大小不一的动态对象用堆加碎片整理与句柄;资源按引用计数和分块加载管理,流送系统把世界切成等大的块;UE 则在 UObject 层用垃圾回收管理对象生命周期。最后,没有准确的追踪工具,以上一切都无从验证。各结论的细节与出处见所链接的概念页。游戏开发 · UE5烘焙、打包与 Zen 加载器Cooking, Packaging & Zen Loader打包是一组构建操作:编译 (Build) → 烘焙 (Cook,把资产转成目标平台格式) → 暂存 (Stage) → 打包 (Package) → 可选的部署 (Deploy)。UE5 的运行时加载器 Zen Loader 会在暂存阶段离线算好包与对象的依赖图,并把数据装进 .utoc/.ucas 容器(IoStore),由 I/O 分发器以很低的 CPU 开销定位数据块;.pak 只用于不常访问的零散文件。

被这些页面引用2 篇

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原