资产管理与异步加载
Asset Management & Async Loading
UE 资产默认按引用自动加载:硬引用 (Hard Reference) 会让被引用资产随引用者一起加载,不加控制就可能在启动时加载全部内容。软引用 (Soft Reference,TSoftObjectPtr/FSoftObjectPath) 只存路径,配合流式管理器 (Streamable Manager) 异步按需加载;资产管理器 (Asset Manager) 进一步把内容划分为主资产 (Primary Asset) 和次资产 (Secondary Asset),用资产包 (Asset Bundle) 管理加载、卸载、烘焙分块与内存审计。
硬引用与软引用
| 方式 | 写法 | 加载时机 | 风险 |
|---|---|---|---|
| 直接属性引用(硬) | UPROPERTY(EditDefaultsOnly) USoundCue* Sound; | 引用者加载时自动加载 | 内存占用膨胀 |
| 构造期引用(硬) | ConstructorHelpers::FObjectFinder | 构造时查找并加载 | 失败则为 nullptr,后续访问崩溃,应断言 |
| 间接引用(软) | TSoftObjectPtr<UStaticMesh> / TSoftClassPtr | 需要时手动加载 | 忘记加载即为空 |
| 运行时字符串 | FindObject<>(只找已加载)/ LoadObject<>(找不到则加载)/ LoadClass<>(顺带校验类型) | 调用时 | 同步加载可能卡帧 |
官方原话的警示是:硬指针属性引用的资产会在包含它的对象加载时被加载,如果不小心,可能在游戏启动时加载 100% 的资产。FSoftObjectPath 是一个存完整资产路径的结构体,在编辑器中显示得像普通对象指针,并正确处理烘焙与重定向;TSoftObjectPtr 本质上是包着 FSoftObjectPath 的 TWeakObjectPtr,模板参数限制编辑器里可选的类型。资产已在内存时 Get() 返回它,否则用 ToSoftObjectPath() 拿到路径去加载。(Asynchronous Asset Loading)
软引用与 GC 的弱引用 (TWeakObjectPtr) 是同一族概念:都不阻止对象被回收、都需要判空,见 垃圾回收与对象生命周期。
异步加载:FStreamableManager
下图展示一次异步加载的生命周期:请求发出后主线程继续运行,完成时回调委托;在回调之前管理器持有硬引用保证资产不被 GC,回调之后释放。
sequenceDiagram participant G as 游戏代码 participant SM as FStreamableManager participant GC as 垃圾回收 G->>SM: RequestAsyncLoad(路径数组, 委托) Note over G: 主线程继续,不阻塞 SM->>SM: 后台加载资产 SM-->>G: 全部加载(或缺失)后调用委托 Note over SM,GC: 回调前持有硬引用,资产不会被回收 G->>G: 在委托中 Get() 并自己保存硬引用 SM-->>GC: 回调后释放引用
要点(Asynchronous Asset Loading):
SynchronousLoad是简单的阻塞加载,对小对象可以,但可能让主线程卡顿过久;RequestAsyncLoad异步加载一组资产,完成后调用委托(委托机制见 委托与 Tick 机制)。- 官方建议把
FStreamableManager放在全局单例里(如GameSingletonClassName指定的对象)。 - 坑:管理器只在回调前持有引用,回调后释放——想让资产常驻,必须在别处保存硬引用。
- 同一套方法在开发版和烘焙后的设备上表现一致,不需维护两条代码路径。
资产注册表与对象库
想“找出满足条件的资产”而不加载它们时,用资产注册表 (Asset Registry):它存储资产元数据,给属性加 AssetRegistrySearchable 即可被查询,结果是 FAssetData。对象库 (ObjectLibrary) 按路径和基类收集一组已加载对象或未加载的 FAssetData,美术/策划往目录里加资产就会被发现,无需维护手工总表;查询后把 FAssetData 转成软路径再异步加载。(Asynchronous Asset Loading)
资产管理器 (Asset Manager)
Asset Manager 是编辑器和打包游戏中都存在的唯一全局对象,可被项目重写;它在保留 UE 松散包架构优点的同时,把内容划分为有意义的分块 (Chunk),并提供审计磁盘与内存占用的工具,服务于烘焙 (Cooking) 与分块 (Chunking)。(Asset Management)
| 概念 | 说明 |
|---|---|
| 主资产 (Primary Asset) | 可由 Asset Manager 通过 FPrimaryAssetId(类型 + 名称,如 MyGameZoneTheme:Forest)直接操作;类需重写 GetPrimaryAssetId。默认只有关卡 (UWorld) 是主资产 |
| 次资产 (Secondary Asset) | 不直接管理,被主资产引用时由引擎自动加载 |
| 蓝图类主资产 | 用 GetPrimaryAssetObjectClass 等带 “Class” 的函数取得类,可继承、可生成实例 |
| 非蓝图主资产(Data Asset) | 是类的实例而非类,访问更简单、更省内存,但不能被继承 |
| 资产包 (Asset Bundle) | 与主资产关联的命名资产列表,如在 TSoftObjectPtr 属性上写 meta=(AssetBundles="TestBundle");也可运行时用 FAssetBundleData 注册 |
| Streamable Manager | Asset Manager 内部真正执行异步加载、用 Streamable Handle 保持对象在内存中;引擎中有多个 |
使用流程(Asset Management):
- 如需特殊功能,在
DefaultEngine.ini的[/Script/Engine.Engine]下设AssetManagerClassName指向自定义类。 - 注册主资产:项目设置 Asset Manager 页的 Primary Asset Types to Scan、Directories to Exclude(排除测试资产)、Primary Asset Rules、Only Cook Production Assets(发售包里出现 DevelopmentCook 资产会报错);或在
StartInitialLoading中调用ScanPathsForPrimaryAssets,官方建议同类主资产放同一子目录以加快发现。 - 用
LoadPrimaryAsset(s)/LoadPrimaryAssetsWithType加载并指定资产包,用Unload*卸载。 - 运行时动态数据可用
ExtractSoftObjectPaths从结构体提取软引用,RecursivelyExpandBundleData递归展开依赖。
继承 UPrimaryDataAsset 是最省事的起点,它已实现 GetPrimaryAssetId 与资产包数据的保存。(Asset Management)
实际项目中的用法与取舍
| 场景 | 推荐 |
|---|---|
| 总会用到的小资产(UI 图标、默认材质) | 硬引用即可,简单可靠 |
| 只在特定情况出现的大资产(某把武器的高模、某关的 Boss) | 软引用 + 异步加载 |
| 道具、角色、地图等“目录型”数据 | 主资产 + Data Asset + 资产包,按 UI/游戏中等状态分包加载 |
| 需要按条件查找 | 资产注册表 / 对象库,先查元数据再加载 |
| 开放世界的关卡内容 | 交给 世界分区 按距离流送 |
官方承认把直接访问内存的玩法代码改成异步加载需要时间,但回报是“卡顿少得多、内存低得多”。(Asynchronous Asset Loading)常见坑:在构造函数里大量 FObjectFinder 造成连锁加载;对软引用 Get() 前不检查是否已加载;在热路径上用 LoadSynchronous(增强输入 的官方示例即用同步加载映射上下文,对小资产可接受)。资产所在模块/插件的打包与依赖见 模块、插件与构建系统;对象路径与 UClass 的关系见 UObject 与反射系统。按需加载与渲染侧 细节层次 一样,都是用“只为当前需要的内容付费”换取内存与时间。
PBRT 的视角:解析期间异步创建场景对象(PBRT 4e C.2、C.3)
- 并行解析:场景格式的
Import指令表示被导入的文件可以与当前文件并行解析——导入文件结束时丢弃它对图形状态的修改,所以当前文件不必等待。每个导入文件有自己的BasicSceneBuilder(复制当时的图形状态),共享同一个BasicScene,因此BasicScene的方法都要加互斥锁。 - 边解析边创建:
WorldBegin之后相机、胶片、采样器等不再改变,SetOptions()用RunAsync()启动多个任务创建它们并立即返回,解析继续。理由:RealisticCamera计算出瞳边界约需 1 秒,HaltonSampler初始化随机排列约 0.1 秒。介质为每个介质维护一个任务表;取结果用TryGetResult(),结果未就绪时会解锁、去做别的并行工作再加锁,从而避免持锁等待造成的死锁。 - 图像纹理:读盘常是瓶颈,所以尽早异步加载;同一图像被多个纹理使用时只由一个任务读取,其余等待后从缓存获取。
- 效果:作者称复杂场景的渲染开始时间常常比上一版快 4 倍。调用方应尽量推迟取结果,先做独立工作,减少
GetResult()阻塞。
出处
- Asset Management UE 5.8 文档
- Referencing Assets UE 5.8 文档
- Asynchronous Asset Loading UE 5.8 文档
- PBRT 4e source excerpt: chapter C (Processing the Scene Description)
- Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)附录 C C.1–C.4 节与延伸阅读,pbr-book.org/4ed/Processing_the_Scene_Description