【UE4】加載資源的方式(七)使用AssetManager進行加載
參考資料&原文鏈接
Unreal Engine 4 —— Asset Manager介紹
Unreal Engine 4 —— Fortnite中的Asset Manager與資源控制
有關概念介紹
以下內容來自Unreal Engine 4 —— Asset Manager介紹:
涉及到的類
Assest
Asset指的是在Content Browser中看到的那些物件。貼圖,BP,音頻和地圖等都屬於Asset.
Asset Registry
Asset Registry是資源注冊表,其中存儲了每個特定的asset的有用的信息。這些信息會在asset被儲存的時候進行更新。
Streamable Managers
Streamable Managers負責進行讀取物件並將其放在內存中。
Primary Assets
Primary Assets指的是在游戲中可以進行手動載入/釋放的東西。包括地圖文件以及一些游戲相關的物件,例如character classs或者inventory items.
Secondary Assets
Secondary Assets指的是其他的那些Assets了,例如貼圖和聲音等。這一類型的assets是根據Primary Assets來自動進行載入的。
Asset Bundle
Asset Bundle是一個Asset的列表,用於將一堆Asset在runtime的時候載入。
Asset Manager
Asset Manager是一個可選的全局單例類,用於管理primary assets和asset bundles,這些東西在runtime的時候很有用。
Primary Assets
Primary Asset指的是可以針對於UObject::GetPrimaryAssetId()返回一個有效的值的UObject。
對於默認的工程,所有在/Game/Maps路徑下的Maps會被設為Primary Assets,而所有其他的assets如果需要設定為Primary Assets,都需要手動指定。
所有的Primary Assets是通過FPrimaryAssetId來進行引用的,FPrimaryAssetId擁有如下的屬性:
PrimaryAssetType
:這指的是一個用於描述物件的邏輯類型的名字,通常是基類的名字。例如,我們有兩個繼承自同一個本地類AWeapon的BPAWeaponRangedGrenade_C和AWeaponMeleeHammer_C,那么它們會有同樣的PrimaryAssetType——"Weapon"
PrimaryAssetName:這指的是用於描述這個asset的名字。通常來說,這就是這個object的名字(short name),但是對於例如說maps來說,這個值就是對應的asset path。
PrimaryAssetType:PrimaryAssetName可以組成整個游戲實例中的asset的唯一的描述。當客戶端在和服務端通信的時候,就可以通過這個字符串來確認某個物件。例如,"Weapon:BattleAxe_Tier2"本質上和"/Game/Items/Weapons/Axes/BattleAxe_Tier2.BattleAxe_Tier2_C"是一樣的。
在FPrimaryAssetId中分別有兩個FName的Tag,分別是PrimaryAssetTypeTag和PrimaryAssetNameTag。因此,當一個Primary Asset被保存了之后,就可以直接在Asset Registry中通過這兩個Tag來找到這個Asset。
主資源、次資源和主資源標簽
以下內容來自官方文檔 - AssetManager:
從概念上來說,虛幻引擎 4 的資源管理系統將所有資源分為兩類:主資源 和 次資源。資源管理器通過主資源的 主資源 ID 即可直接對其進行操作,調用 GetPrimaryAssetId
即可獲得此 ID。為將特定 UObject
類構成的資源指定為主資源,覆蓋 GetPrimaryAssetId
即可返回一個有效的 FPrimaryAssetId
結構。次資源不由資源管理器直接處理,但其被主資源引用或使用后引擎便會自動進行加載。默認只有 UWorld
資源(關卡)為主資源;所有其他資源均為次資源。為將次資源設為主資源,必須覆蓋其類的 GetPrimaryAssetId
函數,返回一個有效的 FPrimaryAssetId
結構。
將次資源設置為主資源
下面部分內容來自【UE4】資源管理之UAssetManager用法
示例:
//.h
//定義資源類型
static const FPrimaryAssetType CharacterType = TEXT("Character");
//重寫方法以獲取主資產ID,GetPrimaryAssetId是在UObject就聲明的抽象函數,所以不用擔心父類沒有這個抽象方法。
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
//.cpp
FPrimaryAssetId AMyCharacter::GetPrimaryAssetId() const
{
//AssetType是FPrimaryAssetType類型的變量
return FPrimaryAssetId(CharacterType,GetFName());
}
如果要將資源提升為PrimaryAsset,需要實現GetPrimaryAssetId方法。方法的返回值就是AssetManager對這個資源進行管理的標識符了。 返回FPrimaryAssetId對象時,需要傳入AssetType和GetFName兩個參數。 其中AssetType是FPrimaryAssetType的對象,而GetFName()方法返回的是資源對象名。這兩個參數一起確定了這個資源的Id。 也就是說,我們的資源對象可以屬於不同或相同的資源類型,按照資源類型對可以對PriamryAsset進行分類。
建議將CharacterType的定義放在一個全局的靜態文件中,方便統一管理。
例如:
#pragma once
#include "CoreMinimal.h"
namespace ResString
{
//定義資源類型
static const FPrimaryAssetType MyCharacterType = TEXT("Character");
//資源名
static const FName MyCharacterName = TEXT("MyCharacter");
}
注冊資源
注冊資源有兩種方式,一種是在引擎里面注冊,一種是在代碼里面注冊。
引擎里面注冊
我們通過在C++中通過實現類的GetPrimaryAssetId方法對類進行了Id和Type的設置。如果我們想要在藍圖中對資源進行引用的話,還需要在項目設置里面對AssetType進行注冊。換句話說,我們在C++中只是對AssetType進行聲明和定義(因為這個AssetType的定義位置比較隨意,雖然我們推薦定義在AssetManager,但實際任何位置,甚至直接返回TEXT都可以。),如果要讓UE4去識別我們的AssetType,就需要對其進行注冊。 首先打開ProjectSettings,然后找到AssetManager,在設置面板里面可以看到PrimaryAssetTypestoScan,這個變量就是對AssetType進行注冊的地方了。參數說明請參考官方文檔。
果然是Map才是主資源- -。
接下來添加我們自己的資源類型:
可選的配置類型如下:
這里需要注意的是數組元素里有一項是PrimaryAssetType,這項的值必須與注冊的資源返回的Id參數AssetType的TEXT值一致。可能有點繞口,就是說PrimaryAssetType的值與此項注冊的資源類在C++中賦值的AssetType一致。
代碼里面注冊
還是先看函數:
/**
* Scans a list of paths and reads asset data for all primary assets of a specific type.
* If done in the editor it will load the data off disk, in cooked games it will load out of the asset registry cache
*
* @param PrimaryAssetType Type of asset to look for. If the scanned asset matches GetPrimaryAssetType with this it will be added to directory
* @param Paths List of file system paths to scan
* @param BaseClass Base class of all loaded assets, if the scanned asset isn't a child of this class it will be skipped
* @param bHasBlueprintClasses If true, the assets are blueprints that subclass BaseClass. If false they are base UObject assets
* @param bIsEditorOnly If true, assets will only be scanned in editor builds, and will not be stored into the asset registry
* @param bForceSynchronousScan If true will scan the disk synchronously, otherwise will wait for asset registry scan to complete
* @return Number of primary assets found
*/
virtual int32 ScanPathsForPrimaryAssets(FPrimaryAssetType PrimaryAssetType, const TArray<FString>& Paths, UClass* BaseClass, bool bHasBlueprintClasses, bool bIsEditorOnly = false, bool bForceSynchronousScan = true);
/** Single path wrapper */
virtual int32 ScanPathForPrimaryAssets(FPrimaryAssetType PrimaryAssetType, const FString& Path, UClass* BaseClass, bool bHasBlueprintClasses, bool bIsEditorOnly = false, bool bForceSynchronousScan = true);
參數:
掃描路徑列表,讀取指定類型的所有主資產的資產數據。
如果在編輯器中完成,它將從磁盤上加載數據,在打包的游戲中,它將從資產注冊表緩存中加載數據。如果掃描資產匹配GetPrimaryAssetType這將被添加到目錄。
PrimaryAssetType
要查找的資產類型。如果掃描的資產與這個GetPrimaryAssetType匹配,它將被添加到目錄。
Paths
要掃描的文件系統路徑列表。
BaseClass
所有加載的資源的基類,如果掃描的資源不是這個類的子類,它將被跳過。
bHasBlueprintClasses
如果為真,則資產是子類BaseClass的藍圖。如果為false,它們是基本UObject資產。
bIsEditorOnly
如果為true,資產將只在編輯器構建中掃描,而不會存儲到資產注冊表中。
bForceSynchronousScan
如果為true將同步掃描磁盤,否則將等待資產注冊表掃描完成。
return
發現的主要資產數量。
如希望直接在代碼中注冊主資源,則覆蓋資源管理器類中的 StartInitialLoading 函數並從該處調用 ScanPathsForPrimaryAssets。因此,推薦您將所有同類型的主資源放入相同的子文件夾中。這將使資源查找和注冊更為迅速。
觀察代碼可以發現是和手動在引擎里面注冊時一樣的。那么代碼就好寫了:
//掃描
UAssetManager::Get().ScanPathsForPrimaryAssets(ResString::MyCharacterType,TArray<FString>{"/Game/Core"},AMyCharacter::StaticClass(),true);
類似於UObjectLibrary:
UObjectLibrary* TextureOL;
//如果UObjectLibrary未被加載
if (!TextureOL)
{
//加載一手,類型寫自己想要的
TextureOL = UObjectLibrary::CreateLibrary(UTexture2D::StaticClass(), false, false);
//手動指定不要被GC回收
TextureOL->AddToRoot();
}
//掃描路徑
TextureOL->LoadAssetDataFromPath("/Game/UI/Images");
加載資源
資源管理器函數 LoadPrimaryAssets
、LoadPrimaryAsset
和 LoadPrimaryAssetsWithType
可用於在適當的時間開始加載主資源。之后資源可通過 UnloadPrimaryAssets
、UnloadPrimaryAsset
和 UnloadPrimaryAssetsWithType
進行卸載。使用這些加載函數時,可指定一個資源束列表。以此法進行加載將使資源管理器按以上描述的方式加載這些資源束應用的次資源。
/**
* Loads a list of Primary Assets. This will start an async load of those assets, calling callback on completion.
* These assets will stay in memory until explicitly unloaded.
* You can wait on the returned streamable request or poll as needed.
* If there is no work to do, returned handle will be null and delegate will get called before function returns.
*
* @param AssetsToLoad List of primary assets to load
* @param LoadBundles List of bundles to load for those assets
* @param DelegateToCall Delegate that will be called on completion, may be called before function returns if assets are already loaded
* @param Priority Async loading priority for this request
* @return Streamable Handle that can be used to poll or wait. You do not need to keep this handle to stop the assets from being unloaded
*/
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToLoad, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
/** Single asset wrapper */
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAsset(const FPrimaryAssetId& AssetToLoad, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
/** Loads all assets of a given type, useful for cooking */
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAssetsWithType(FPrimaryAssetType PrimaryAssetType, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
示例代碼:
void UWC_TestUI::OnBtnClickCommonBtn_AssetManager()
{
UAssetManager::Get().LoadPrimaryAsset(FPrimaryAssetId(ResString::MyCharacterType,ResString::MyCharacterName), TArray<FName>{ResString::MyCharacterName},FStreamableDelegate::CreateUObject(this,&UWC_TestUI::OnAssetManagerAsyncLoadCompleted));
}
同樣的,資源加載完成了回有一個回調函數,寫法和SteamableManager是一樣的。
void UWC_TestUI::OnAssetManagerAsyncLoadCompleted()
{
FPrimaryAssetId AssetId = FPrimaryAssetId(ResString::MyCharacterType, ResString::MyCharacterName);
UObject * MyCharacterObj = UAssetManager::Get().GetPrimaryAssetObject(AssetId);
}
可以看到我們的資源是被成功加載了的:
注意我這里加載的是角色藍圖,如果要生成Actor的話還是得拿到它的UClass再生成:
void UWC_TestUI::OnAssetManagerAsyncLoadCompleted()
{
FPrimaryAssetId AssetId = FPrimaryAssetId(ResString::MyCharacterType, ResString::MyCharacterName);
UObject * MyCharacterObj = UAssetManager::Get().GetPrimaryAssetObject(AssetId);
UClass * MyCharacterClass = UAssetManager::Get().GetPrimaryAssetObjectClass<AMyCharacter>(AssetId);
FVector Location(230.0,0,120);
FRotator Rotation(0.0,0,0);
GetWorld()->SpawnActor(MyCharacterClass,&Location,&Rotation);
}
卸載資源
一樣的傳入FPrimaryAssetId即可,這里就不作演示了。
/**
* Unloads a list of Primary Assets that were previously Loaded.
* If the only thing keeping these assets in memory was a prior Load call, they will be freed.
*
* @param AssetsToUnload List of primary assets to load
* @return Number of assets unloaded
*/
virtual int32 UnloadPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToUnload);
/** Single asset wrapper */
virtual int32 UnloadPrimaryAsset(const FPrimaryAssetId& AssetToUnload);
/** Loads all assets of a given type, useful for cooking */
virtual int32 UnloadPrimaryAssetsWithType(FPrimaryAssetType PrimaryAssetType);
AssetsRegistry
編輯器如何發現資產以及如何在加載資產之前使其了解更多有關資產類型的信息。
該資產的注冊表是異步收集有關卸載的資產信息為編輯負荷編輯子系統。此信息存儲在內存中,因此編輯器可以創建資產列表而無需加載它們。該信息具有權威性,並且會隨着內存中的資產更改或磁盤上的文件更改而自動保持最新。在內容瀏覽器是該系統的主要消費者,但它可能在任何地方編輯代碼中使用。
獲取資產列表
要按類形成資產列表,只需加載資產注冊表模塊,然后調用 Module.Get().GetAssetsByClass()
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetData;
const UClass* Class = UStaticMesh::StaticClass();
AssetRegistryModule.Get().GetAssetsByClass(Class->GetFName(), AssetData);
創建過濾器
FARFilter
可以在調用時提供AGetAssets()
以創建按多個條件過濾的資產列表。過濾器由多個組件組成:
- 包裹名字
- 包路徑
- 收藏
- 班級
- 標簽/值對
一個組件可能有多個元素。如果資產滿足所有組件,則資產通過過濾器。為了滿足一個組件,資產必須匹配其中的任何元素。
例如,如果存在路徑為 /Game/Meshes/BeachBall 的 StaticMesh 資源:
- 如果過濾器僅包含 PackagePath ,則資產將通過
/Game/Meshes
。只有一個組件具有一個元素。 - 如果過濾器包含 PackagePath
/Game/Meshes
和 ClassesUParticleSystem
AND , 則資產將通過UStaticMesh
。有兩個組件,第一個有一個元素,第二個有兩個。 - 如果過濾器包含 PackagePath
/Game/Meshes
且僅包含 Class ,則資產將失敗UParticleSystem
。有兩個組件,每個組件都有一個元素。 - 如果過濾器包含 PackagePath
/Game/NotMeshes
和 Class ,則資產將失敗UStaticMesh
。此過濾器還使用兩個組件,每個組件都有一個元素。
使用具有兩個組件 Class 和 PackagePath 的過濾器的示例:
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetData;
FARFilter Filter;
Filter.Classes.Add(UStaticMesh::StaticClass());
Filter.PackagePaths.Add("/Game/Meshes");
AssetRegistryModule.Get().GetAssets(Filter, AssetData);
異步數據收集
資產注冊表UAsset
異步讀取文件,並且在您請求它時可能沒有所有資產的完整列表。如果您的編輯器代碼需要完整列表,則資產注冊表會在發現/創建、重命名或刪除資產時提供委托回調。當資產注冊表完成其初始搜索時,還有一個委托,這對許多系統都很有用。
您可以通過加載資產注冊表模塊來注冊這些委托,然后使用中提供的功能IAssetRegistry
:
/** Register/Unregister a callback for when assets are added to the registry */
virtual FAssetAddedEvent& OnAssetAdded() = 0;
/** Register/Unregister a callback for when assets are removed from the registry */
virtual FAssetRemovedEvent& OnAssetRemoved() = 0;
/** Register/Unregister a callback for when assets are renamed in the registry */
virtual FAssetRenamedEvent& OnAssetRenamed() = 0;
/** Register/Unregister a callback for when the asset registry is done loading files */
virtual FFilesLoadedEvent& OnFilesLoaded() = 0;
/** Register/Unregister a callback to update the progress of the background file load */
virtual FFileLoadProgressUpdatedEvent& OnFileLoadProgressUpdated() = 0;
/** Returns true if the asset registry is currently loading files and does not yet know about all assets */
virtual bool IsLoadingAssets() = 0;
特點
這種加載方式的特點是:
- 被認為是UObjectLibrary的替代品,從上面的實踐來說確實很不錯。
- 確實可以很精確的控制資源的餓加載和釋放的時機。
- 這是被視為UObjectLibrary的替代品,從編碼表現來說確實很不錯。
- 配置非常給力精確。
- AssetsRegistry的存在也使得登記、獲取、過濾、監聽資產更為便捷。
本文標簽
游戲開發
、游戲開發基礎
、Unreal Engine
、UE資源加載
。