1778 字
9 分钟
UE 资产管理——从 Soft Reference 到 Primary Asset

一、为什么资产引用会决定游戏启动速度#

在编辑器里拖一张纹理、一个角色蓝图到另一个资产上很简单。但这次拖拽实际上建立了一条依赖关系:

MainMenu
→ CharacterBlueprint
→ SkeletalMesh
→ Material
→ Texture

如果这条链最终被某个启动时加载的对象持有,整个依赖树可能都会在游戏启动时被加载。于是一个“菜单只需要一个图标”的引用,可能间接把角色、动画和一整套材质都带进内存。

资产管理要解决的不是“文件在哪”,而是:

  1. 什么时候加载?
  2. 谁拥有它的生命周期?
  3. 什么时候可以释放?
  4. 打包时应该放进哪个 Chunk?

二、硬引用和软引用#

1 硬引用:使用前必须加载#

UPROPERTY(EditDefaultsOnly)
TSubclassOf<ACharacter> CharacterClass;
UPROPERTY(EditDefaultsOnly)
UTexture2D* ItemIcon;

硬引用的语义是:这个对象要被加载,当前对象才能完整工作。 它适合真正的必需依赖,例如角色运行时一定要有的碰撞配置。

但如果把所有可选内容都做成硬引用:

GameInstance
→ 全部角色
→ 全部武器
→ 全部地图
→ 全部 UI 皮肤

启动时间和常驻内存都会失控。

2 软引用:保存路径,需要时再加载#

UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UTexture2D> ItemIcon;
UPROPERTY(EditDefaultsOnly)
TSoftClassPtr<AActor> RewardActorClass;

软引用只保存一个资产路径,不会因为声明这个变量就把目标资产加载进来:

if (!ItemIcon.IsValid()) {
ItemIcon.LoadSynchronous(); // 会阻塞当前线程,谨慎使用
}
UTexture2D* Icon = ItemIcon.Get();

LoadSynchronous() 很方便,但在游戏线程上同步加载大资产会产生卡顿。真正的运行时流程应该尽量使用异步加载:

Streamable.RequestAsyncLoad(
ItemIcon.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(
this,
&UItemDetailsWidget::OnIconLoaded));

软引用并不代表“永远不会加载”,它代表加载时机由业务决定


三、Asset Manager 在管理什么#

UE 的 Asset Manager 可以看作一层全局资产注册和加载服务:

资产文件
↓ Primary Asset Label / Asset Registry
Primary AssetId
↓ Asset Manager
异步加载、Bundle、规则、Chunk

它和普通 TSoftObjectPtr 的区别是:软引用只描述“我指向谁”,Asset Manager 还知道“这类资产属于哪个系统、如何发现、如何分组和加载”。

1 Primary Asset#

适合成为 Primary Asset 的通常是玩法数据的入口:

  • 角色定义
  • 武器定义
  • 任务定义
  • 地图/关卡定义
  • DLC 或赛季内容
UCLASS(BlueprintType)
class UItemDefinition : public UPrimaryDataAsset {
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly)
FName ItemId;
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UTexture2D> Icon;
UPROPERTY(EditDefaultsOnly)
TSoftClassPtr<AActor> PreviewActor;
virtual FPrimaryAssetId GetPrimaryAssetId() const override {
return FPrimaryAssetId(TEXT("Item"), ItemId);
}
};

Primary Asset 的 ID 不应该依赖对象当前在内存中的地址。一个稳定的 Item:HealingPotion ID,才适合存档、网络消息和内容版本之间互相引用。


四、Asset Bundle:按使用场景分组加载#

同一个物品定义可能包含不同用途的依赖:

Item:HealingPotion
├── Default → 基础数据、名称
├── UI → 图标、展示缩略图
├── Preview → 展示 Actor、动画
└── World → 世界模型、音效、特效

玩家只浏览列表时,不需要加载完整世界模型。打开详情页时加载 UIPreview;真正生成物品时再加载 World

FPrimaryAssetId ItemId(
TEXT("Item"),
FName(TEXT("HealingPotion")));
TArray<FName> Bundles;
Bundles.Add(TEXT("UI"));
UAssetManager::Get().LoadPrimaryAsset(
ItemId,
Bundles,
FStreamableDelegate::CreateLambda([] {
// 详情页依赖已经准备好
ShowItemDetails();
}));

Bundle 是一种加载意图。它不会自动替你设计所有依赖,团队仍然需要统一约定哪些软引用属于哪个 Bundle。


五、资产生命周期:加载完成不等于永远保留#

异步加载完成后,目标对象需要由某个系统持有引用,否则后续 GC 可能会回收它:

UPROPERTY()
TObjectPtr<UItemDefinition> CurrentDefinition;
TSharedPtr<FStreamableHandle> CurrentLoadHandle;

常见的生命周期关系是:

菜单打开
→ 请求 UI Bundle
→ Widget / ViewModel 持有引用
→ 菜单关闭
→ 释放 Widget 和 Handle
→ 没有其他引用时允许 GC 回收

如果一个全局缓存把所有加载过的资产都放进 TMap<FName, UObject*>,它们就会变成常驻对象。缓存应当有明确策略:

  • 缓存上限
  • 最近使用时间
  • 关卡切换时清理
  • 低内存平台的降级方式

六、同步加载为什么会制造 Hitch#

同步加载通常包含几类工作:

查找路径
→ 读取 Pak / 文件
→ 解压
→ 反序列化 UObject
→ 创建渲染资源
→ 上传 GPU

这些工作中任意一项在 Game Thread 上耗时过长,都会让一帧突然变成 100ms、300ms 甚至更久。画面卡顿不一定来自 Tick,也可能是某个点击回调里偷偷调用了 LoadSynchronous()

可交互场景的加载流程应该显式化:

void UItemPreviewSubsystem::OpenPreview(FPrimaryAssetId ItemId) {
SetLoading(true);
CurrentLoadHandle = AssetManager.LoadPrimaryAsset(
ItemId,
{ TEXT("Preview") },
FStreamableDelegate::CreateUObject(
this,
&UItemPreviewSubsystem::OnPreviewReady));
}

加载开始、加载完成、加载失败、取消,都应该是可以观察的状态,而不是让 UI 在同步函数上“卡住以后突然出现”。


七、Cook、Pak 和资产管理的关系#

Editor 能找到资产,不等于打包后一定会带上资产。Cook 只会处理被规则认为需要的内容:

硬引用
→ 通常能自动追踪
软引用路径
→ 需要 Asset Manager 规则或显式引用扫描
Primary Asset / Chunk 规则
→ 决定如何被发现、Cook、打包和下载

最典型的 bug 是:编辑器里图标正常,打包后打开列表图标为空。原因不是 Widget,而是软引用资产没有进入 Cook 结果。

排查时应该看三件事:

  1. Asset Registry 是否发现了 Primary Asset?
  2. 软引用的目标是否被加入对应的规则/Bundle?
  3. 目标是否被 Cook 到当前平台和 Chunk?

八、RPG 物品系统的资产结构#

ItemDefinition(Primary Asset)
├── 基础数据:ItemId、名称、稀有度、属性
├── UI Bundle:图标、详情缩略图
├── Preview Bundle:展示模型、旋转动画
└── World Bundle:生成拾取物、音效、特效
USTRUCT()
struct FItemSaveData {
GENERATED_BODY()
UPROPERTY()
FPrimaryAssetId DefinitionId;
UPROPERTY()
int32 Count = 0;
};

存档里保存 PrimaryAssetId,而不是保存一个 UObject 指针。读档时:

SaveGame 读到 Item:HealingPotion
→ Asset Manager 异步加载 Definition
→ 根据当前平台选择 UI / World Bundle
→ 重新构建运行时对象

这和 8 月 16 日存档文章里的版本迁移是连起来的:存档保存稳定的 ID 和数据,不保存不可持久化的内存地址。


九、总结#

概念一句话
硬引用当前对象加载时连带加载目标,适合必需依赖
软引用只保存路径,加载时机由业务控制
Primary Asset有稳定 ID、可被 Asset Manager 发现和分组的资产入口
Asset Bundle按 UI、Preview、World 等使用场景拆分依赖
Streamable Handle管理异步加载请求和生命周期
Cook / Pak决定资产是否真正进入可发布的内容包

资产管理是运行时架构的一部分,不是打包前才处理的美术问题。 什么时候加载、谁持有、如何释放、存档保存什么 ID——这些决定了游戏能不能从“编辑器里能跑”成长为“正式版本可持续扩展”。

UE 资产管理——从 Soft Reference 到 Primary Asset
https://www.m4doka.xyz/posts/ue/ue-11-asset-manager/
作者
m4doka
发布于
2026-08-22
许可协议
CC BY-NC-SA 4.0