一、为什么资产引用会决定游戏启动速度
在编辑器里拖一张纹理、一个角色蓝图到另一个资产上很简单。但这次拖拽实际上建立了一条依赖关系:
MainMenu → CharacterBlueprint → SkeletalMesh → Material → Texture如果这条链最终被某个启动时加载的对象持有,整个依赖树可能都会在游戏启动时被加载。于是一个“菜单只需要一个图标”的引用,可能间接把角色、动画和一整套材质都带进内存。
资产管理要解决的不是“文件在哪”,而是:
- 什么时候加载?
- 谁拥有它的生命周期?
- 什么时候可以释放?
- 打包时应该放进哪个 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 RegistryPrimary 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 → 世界模型、音效、特效玩家只浏览列表时,不需要加载完整世界模型。打开详情页时加载 UI 和 Preview;真正生成物品时再加载 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 结果。
排查时应该看三件事:
- Asset Registry 是否发现了 Primary Asset?
- 软引用的目标是否被加入对应的规则/Bundle?
- 目标是否被 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——这些决定了游戏能不能从“编辑器里能跑”成长为“正式版本可持续扩展”。