AssetManager 与 PrimaryAsset

自动关联目录:AssetManager 与 PrimaryAsset

AssetManager 解决的是"怎么把成百上千个资产组织成可管理、可分块、可按需加载的集合"。它引入的核心概念是 PrimaryAsset:一个你可以直接用「类型 + 名字」指认的资产。

一句话定位:PrimaryAsset 是资产的"主键",AssetManager 是主键到路径的索引服务。有了主键,你就不必再在代码里硬写 /Game/xxx/yyy 这样的路径。

两类资产

Primary AssetSecondary Asset
标识FPrimaryAssetId(Type:Name)只有路径
能否被直接指认能不能,只能通过引用链被带进来
典型角色、关卡、道具、技能贴图、网格、材质、音效
管理方AssetManager 扫描并索引由 PrimaryAsset 引用
分块可作为 Chunk 的锚点跟随引用它的 PrimaryAsset

划分标准:这个资产会不会被"点名要"(玩家选角色、进某个关卡、用某个道具)?会 → Primary;不会 → Secondary。

FPrimaryAssetId

FPrimaryAssetId = Type + ":" + Name
例如:Character:Hero01、Level:Forest 、Item:Sword
FPrimaryAssetId Id(TEXT("Item"), TEXT("Sword"));
FPrimaryAssetId Id2 = FPrimaryAssetId::FromString(TEXT("Item:Sword"));

// 取路径
FSoftObjectPath Path = UAssetManager::Get().GetPrimaryAssetPath(Id);
API作用
GetPrimaryAssetPath(Id)主键 → 路径
GetPrimaryAssetIdForPath(Path)路径 → 主键
LoadPrimaryAsset(Id, Bundles, Callback)按主键加载
GetPrimaryAssetObject<T>(Id)取已加载的对象
ScanPathsForPrimaryAssets按路径扫描注册

用主键而不是路径,好处是资产移动位置后代码不用改——路径变了,注册表里的映射自动更新。

让一个类成为 PrimaryAsset

UCLASS()
class UMyItemData : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    FText DisplayName;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TSoftObjectPtr<UTexture2D> Icon;

    // 主键的类型部分
    virtual FPrimaryAssetId GetPrimaryAssetId() const override
    {
        return FPrimaryAssetId(TEXT("Item"), GetFName());
    }
};

UPrimaryDataAsset 相比 UDataAsset 多的是参与 AssetManager 的扫描与分块。

配置扫描路径

在 DefaultGame.ini:

[/Script/Engine.AssetManager]
+PrimaryAssetTypesToScan=(PrimaryAssetType="Item",AssetBaseClass=/Script/MyGame.MyItemData,bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=((Path="/Game/Items")),Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True))
+PrimaryAssetTypesToScan=(PrimaryAssetType="Character",AssetBaseClass=/Script/MyGame.MyCharacterData,bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=((Path="/Game/Characters")),Rules=(ChunkId=0))
字段含义
PrimaryAssetType主键的类型部分
AssetBaseClass基类,用于筛选
Directories扫描目录
Rules.ChunkId默认分块——不打 Chunk 就全进主包
Rules.Priority优先级
bIsEditorOnly仅编辑器资产,不打包

忘了配 PrimaryAssetTypesToScan 的表现:资产明明在目录里,运行时 GetPrimaryAssetPath 返回空。这是本分支最高频的"低级错误"。

Asset Registry

Asset Registry 是一份"不用加载资产就能查询其元数据"的索引。编辑器启动时扫描所有资产并建索引,运行时从打包好的 AssetRegistry.bin 读。

FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
IAssetRegistry& AR = ARM.Get();

TArray<FAssetData> Assets;
AR.GetAssetsByClass(UTexture2D::StaticClass()->GetFName(), Assets);
AR.GetAssetsByPath(FName("/Game/Items"), Assets, /*bRecursive=*/true);

for (const FAssetData& Data : Assets)
{
    // 不加载就能读 tag
    FString Result;
    Data.GetTagValue(TEXT("MyTag"), Result);
}
用途说明
编辑器工具批量扫描不加载即可读元数据,速度快几个数量级
运行时列举资产依赖打包的 AssetRegistry.bin
自定义 tag通过 AssetRegistrySearchable 的 UPROPERTY 暴露

运行时 Registry 只包含在包里的资产——编辑器里能查到、打包后查不到,多半是资产没被打进去(没有引用链也没有 PrimaryAsset 规则)。

Bundle:按需加载的分组

Bundle 是"一个 PrimaryAsset 里再切一刀"的机制:把它的依赖按用途分组,用到哪组才加载哪组。

UPROPERTY(EditAnywhere, meta = (AssetBundles = "UI"))
TSoftObjectPtr<UTexture2D> Icon;      // 归到 UI bundle

UPROPERTY(EditAnywhere, meta = (AssetBundles = "Game"))
TSoftObjectPtr<UStaticMesh> Mesh;     // 归到 Game bundle
TArray<FName> Bundles{ TEXT("UI") };
UAssetManager::Get().LoadPrimaryAsset(Id, Bundles,
    [](FPrimaryAssetId LoadedId){ /* 就绪 */ });
内置 Bundle含义
(空)只加载 PrimaryAsset 自身
Preload紧跟 PrimaryAsset 一起加载
Client / Server按端区分

典型收益:道具列表页只加载几百个 Icon(UI bundle),不加载几 GB 的网格和动画(Game bundle)。

Chunk 划分

ChunkId 决定资产被打进哪个 .pak,是热更新与按需下载的基础。

方式说明
PrimaryAssetTypesToScan 的 Rules.ChunkId按类型统一指定
PrimaryAssetLabel(资产标签)编辑器里给目录/资产打标签,覆盖默认规则
ChunkId = -1不指定,跟引用链走
ChunkId = 0主包(第一个 pak)

标签的优先级高于扫描规则:同一资产被多条规则命中时,取 ChunkId 最大的那条。

常见的划分:

Chunk内容
0启动必需(Loading 界面、基础 UI、引擎资源)
1..N按关卡、按角色、按语言、按活动

最容易踩的坑:以为某个资源在 Chunk 5,但它同时被 Chunk 0 里的资产引用,于是被重复打进两个包——包体变大但不会报错。用 SizeMap 和 UnrealPak.exe -List 核对。

常见坑

坑说明
没配 PrimaryAssetTypesToScanGetPrimaryAssetPath 返回空
硬编码资产路径资产移动后全部失效
把贴图/材质做成 PrimaryAsset主键爆炸,索引无意义
ChunkId 全默认全进主包,热更新失去意义
运行时查不到资产没被打进包,Registry 里没有
依赖 AssetBundles 但没在加载时传 bundle 名依赖没被加载,取到空
资产被多个 Chunk 引用重复打包,包体膨胀
编辑器能跑、打包后不行多半是 Cook 阶段引用链不同(见 Cook 与打包流程)