【UE4】加載資源的方式(一)使用引用進行簡單加載
參考資料&原文鏈接
UE4資源簡介
以下內容來自於UE4的資源管理:
UE4的資源,就是在工程文件夾下的那些非代碼文件,比如Content下面的網格,材質,藍圖等這些文件,大部分資源是以uasset作為后綴的,也有其他后綴如地圖關卡的umap等。在打包時,這些文件可能會根據平台需要,被cook成更小的平台專用文件,然后被放在后綴是pak的壓縮包里。游戲運行時,程序就會掛載解壓這些pak包,然后加載包中的資源文件來使用。打個不是很合適的比方,Pak包就類似於Unity的AssetBundle包,uasset文件就類似於unity的.meta管理的那些資源文件。
程序在用資源的時候,並不是直接在用這些文件。而是要把這些文件轉化為UObject或其他程序可以用的內存對象。比如網格資源文件,程序用的實際是UStaticMesh對象。而把資源文件,轉變為內存里的UObject對象,就是資源管理做的事情。
UE4資源索引簡介
UE4是通過路徑來關聯索引資源的,就跟操作系統下面的文件一樣,每個資源都有他的唯一路徑。但是這個路徑又和文件路徑稍微有點區別。
比如像上圖這樣的一個靜態網格資源,點擊Copy Reference,再粘貼到文本,可以看到他們路徑是這樣的:StaticMesh'/Game/Geometry/Meshes/1M_Cube.1M_Cube'
我把路徑用顏色標了一下方便描述:
- 紅色部分:(StaticMesh)
是資源的類型,平時加載資源的時候也可以不要,加載函數內部會截掉這部分只留單引號里面的路徑,所以其實前面這個StaticMesh或Blueprint可以不寫,在編輯器Content Browser里右鍵點資源拷路徑是有這部分的,我猜引擎留着這部分可能是為了用戶清楚他的類型是什么,更方便識別一些,這個只是給人看的,程序不看這個。
- 綠色部分:(/Game)
是資源的分區。大部分資源的路徑都是以/Game開頭,這個其實表示這個資源是在游戲的Content目錄下面,也可以是/Engine就表示引擎下面的,比如下圖,或者對應插件目錄
- 藍色部分:(/Geometry/Meshes)
就跟操作系統文件管理器中的一樣,表示資源在哪個文件夾下面
- 黃色部分:(1M_Cube)
資源的包(Package)名,也就是這個資源所在的真實物理資源文件(uasset/umap)的名字,包其實就是UE4將對象按照自己的規則序列化到磁盤上的文件,在Content Browser里看到的每一個文件都是一個包
- 紫色部分:(1M_Cube)
資源的對象名,因為物理的資源文件里面可能有多個對象,這個名字可以唯一標識包的內部每個對象的唯一名字。比如藍圖資源里有多個UObject,一個關卡文件里有多個Actor,一個UI藍圖里有多個控件。如果不寫,UE4的某些接口會默認以包名補充到后面,也就是說默認使用和包名相同的對象名,但有的接口又可能不做處理,所以還是建議寫。如果用的不是默認對象,而是資源對象的類,就要在后面加一個_C,如果是CDO對象,就要在前面加Default__
- 冒號后面的部分
有些資源路徑后面會帶冒號:接一個文件名,這種其實是對象的子對象名,有的資源對象內部有子對象,比如C++類里的子類,這個不常用,知道即可。
資源加載的大致步驟
程序在用資源的時候,並不是直接在用這些文件。而是要把這些文件轉化為UObject或其他程序可以用的內存對象。比如網格資源文件,程序用的實際是UStaticMesh對象。而把資源文件,轉變為內存里的UObject對象,就是資源管理做的事情。
對於UE4來說,這個過程大概有這幾個步驟:
- 讀取資源文件的數據到內存。
- 根據內存的二進制數據,把空殼對象反序列化成實際的對象。
- 如果這個對象有依賴其他對象,就遞歸的去做1和2的操作,直到這個對象完整可用。
- 調用對象的初始化函數,並將對象加入到引擎的對象管理中。
資源的卸載
默認情況下,加載中的資源由引擎持有引用,不會被卸載,加載完成后的資源會依賴引擎的gc卸載。如果沒有被使用到,會在下次gc的時候釋放掉。如果需要立即釋放可以手動強制引擎gc。
但是可能有時候由於內存或其他各種原因,只想立即釋放掉指定資源的內存,不想調用全局gc導致游戲產生卡頓,這時候要怎么辦呢?我查了引擎官方文檔以及網上各種文章,基本沒有明確給出答案,下面我會根據以往我的經驗介紹一些做法(野路子),不代表正確,但實測確實能立即釋放掉內存,只是要自己額外花一些功夫保證安全。
- 大部分UObject,可以手動調用ConditionalBeginDestory,這里會主動先把對象的資源清理掉,留下一個空殼UObject。可以看引擎源碼,這個函數相當於主動調用BeginDestroy,在對象gc時候也會調用,但是因為是Conditional的,如果之前已經調用過,在gc的時候就不會再次調用BeginDestroy從而避免了邏輯錯誤。通過這種方式,業務需要自行保證對象不再被使用,否則會出BUG。
- 如果是貼圖,材質或Mesh等資源,可以直接調用ReleaseResource,這里內部會先把貼圖或網格內存先釋放,執行結束之后留下一個空殼UObject,對於空殼UObject其實就不怎么占用內存了,等待gc的時候銷毀即可。另外因為有的ReleaseResource會Flush渲染命令隊列可能產生卡頓,所以可以盡量把ReleaseResource這個函數提交到渲染命令隊列,雖然內存釋放不那么及時,但也比gc快很多,大部分情況一幀內就會釋放掉。通過這種方式,業務也需要自行保證對象不再被使用,否則會出BUG。
- 如果是RenderTarget,引擎有提供專門的池,可以回收復用。
- 如果是動態材質,也有專門的函數可以刪除。
- 如果是Actor或ActorComponent等,有專門的刪除函數,可以調用DestroyActor或DestroyComponent,基本上能釋放掉大部分資源。對於SceneComponent可以主動調用DestroyPhysicsState和DestroyRenderState釋放掉物理或場景中的對象,也能立即釋放一些內存。
硬性引用、軟性引用、構造時引用
ue4提供了許多種機制來控制引用資產的方式並通過擴展將其裝入內存。
這些引用分為兩種方式:
硬性引用:即對象 A 引用對象 B,並導致對象 B 在對象 A 加載時加載。例如:被“硬”指針指向的資源都會被UE4在啟動的時候進行載入,要注意性能問題。有關硬性引用的一個注意事項是,當對象加載並實例化時,以硬性方式引用的資產會自動加載,可能導致內存使用量迅速增加。
軟性引用:即對象 A 通過間接機制(例如字符串形式的對象路徑)來引用對象 B,軟引用的好處是可以按照需要加載。
構造時引用:在構造時加載資產並賦給變量。
直接屬性引用
這是最常見的資產引用情況,並通過 UPROPERTY 宏公開,面板會公開一個UPROPERTY。
EditDefaultsOnly
說明此屬性可通過屬性窗口進行編輯,但只能在原型上進行。此說明符與所有"可見"說明符均不兼容。
UPROPERTY(EditDefaultsOnly, Category = "Test Texture")
TSoftObjectPtr<UTexture2D> TestImg;
間接屬性引用
間接引用有兩種方式:FStringAssetReferences
和TAssetPtr
。
美術師或設計師引用資源的最簡單的方法是,創建一個UProperty 強指針,並賦予其一個類目。在虛幻引擎4中,如果您使用強指針UObject 屬性引用一個資源,那么當加載包含該屬性的對象時將會加載那個資源(通過把對象放置在地圖中,或者通過從類似於游戲信息這樣的東西中進行引用該對象)。
FStringAssetReferences
FStringAssetReferences
是一個簡單的結構體,包含了一個具有資源完整名稱的字符串。如果在類中創建一個那種類型的屬性,那么它將會顯示在編輯器中。
在Editor中,他的表現就是一個允許異步加載的UObject*
。它還可以正確的處理烘焙和重定向。
示例:
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")
FStringAssetReference SAR;
可以看出這個的確是支持UObject
的,什么都能選。
或者你也可以在代碼里面這樣寫,動態加載。
void UWC_TestUI::Test()
{
FStringAssetReference MyCharacterBP = "Blueprint'/Game/Core/BP_MyCharacter.BP_MyCharacter'";
//嘗試從路徑中查找資源
UObject* MyCharacterObj = MyCharacterBP.ResolveObject();
//轉成藍圖類型
UBlueprint* MyCharacterPtr = Cast<UBlueprint>(MyCharacterObj);
if (MyCharacterPtr != nullptr)
{
GetWorld()->SpawnActor<AMyCharacter>(MyCharacterPtr->GeneratedClass);
}
}
FStringAssetReference類的作用主要是通過一個字符串,找到該字符串所對應的資源。或者通過給定的資源,找到該資源所對應的在項目中的路徑,也就是前面所說的字符串。
其中,asset.ResolveObject就是查找字符串對應的資源,返回一個UObejct,我們通過將其轉化成UBlueprint類型然后再去的他的GenerateClass即可。
UObject常用的函數有:
/**
* Attempts to find a currently loaded object that matches this path
*
* @return Found UObject, or nullptr if not currently in memory
*/
UObject* ResolveObject() const;
嘗試查找當前加載的與此路徑匹配的對象。
返回找到的UObject,如果當前不在內存中則返回nullptr。
/**
* Attempts to load the asset, this will call LoadObject which can be very slow
* @param InLoadContext Optional load context when called from nested load callstack
* @return Loaded UObject, or nullptr if the reference is null or the asset fails to load
*/
UObject* TryLoad(FUObjectSerializeContext* InLoadContext = nullptr) const;
嘗試加載資產,這將調用LoadObject,這可能是非常慢的(LoadObject是同步加載)。
InLoadContext
:可選加載上下文時,從嵌套的加載callstack調用。
返回一個已經加載的UObject的引用,或者是空或資產加載失敗。
/** Returns string representation of reference, in form /package/path.assetname[:subpath] */
FString ToString() const;
返回引用的字符串表示形式,格式為/package/path.assetname[:subpath]
。
TAssetPtr
TAssetPtr
基本上就是一個封裝了FStringAssetReferences
的TWeakObjectPtr
,它使用一個特定的類作為模板,以便限制編輯器用戶界面,使策划僅能選擇特定的類。
使用IsPendding()
方法可檢查資產是否已准備好可供訪問。
如果所引用資源存在於內存中,那么TAssetPtr.Get()
將返回該資源。如果該資源不在內存中,那么可以調用ToStringReference()
來查找它引用的資源,並可使用模板化LoadObject<>()
方法、StaticLoadObject()
或FStreamingManager
來加載對象。
前兩個方法以同步方式加載資產,這可能會導致幀速率突降。再次調用TAssetPtr.Get()
可以解除該資源的引用。
示例:
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")
TAssetPtr<UTexture2D> AP;
如果你非要寫代碼以實現按需加載的話可以繼續往下看。
FStringAssetReference和TAssetPtr的按需加載
前面已經用過它們來作為一個簡單引用以加載資源,現在再來看一下它的定義:
可以看到FStringAssetReference
的本質是FSoftObjectPath
。
// Not deprecating these yet as it will lead to too many warnings in games
//UE_DEPRECATED(4.18, "FStringAssetReference was renamed to FSoftObjectPath as it is now not always a string and can also refer to a subobject")
typedef FSoftObjectPath FStringAssetReference;
//UE_DEPRECATED(4.18, "FStringClassReference was renamed to FSoftClassPath")
typedef FSoftClassPath FStringClassReference;
TAssetPtr
的本質是TSoftObjectPtr
。
// Not deprecating these yet as it will lead to too many warnings in games
//UE_DEPRECATED(4.18, "TAssetPtr was renamed to TSoftObjectPtr as it is not necessarily an asset")
template<class T=UObject>
using TAssetPtr = TSoftObjectPtr<T>;
//UE_DEPRECATED(4.18, "TAssetSubclassOf was renamed to TSoftClassPtr")
template<class TClass = UObject>
using TAssetSubclassOf = TSoftClassPtr<TClass>;
以下內容來自官方文檔 - FSoftObjectPath:
FSoftObjectPath
A struct that contains a string reference to an object, either a top level asset or a subobject.
包含對對象(頂級資產或子對象)的字符串引用的結構。
Remarks
A struct that contains a string reference to an object, either a top level asset or a subobject. This can be used to make soft references to assets that are loaded on demand. This is stored internally as an FName pointing to the top level asset (/package/path.assetname) and an option a string subobject path. If the MetaClass metadata is applied to a FProperty with this the UI will restrict to that type of asset.
包含對對象(頂級資產或子對象)的字符串引用的結構。這可以用於對按需加載的資產進行軟引用。它在內部存儲為FName,指向頂級資產(/package/path.assetname)和一個選項,一個字符串子對象路徑。如果MetaClass元數據被應用到FProperty, UI將限制在該類型的資產。
以下內容來自官方文檔 - TSoftObjectPtr:
TSoftObjectPtr
TSoftObjectPtr is templatized wrapper of the generic FSoftObjectPtr, it can be used in UProperties
TSoftObjectPtr是通用的模板化包裝器,它可以用於UProperties。
以下內容來自官方文檔 - 異步資源加載
FSoftObjectPaths和TSoftObjectPtr
讓美術或設計師引用資源的最簡單方法是創建硬指針的UProperty並為它指定一個類別。在UE4中,如果有一個硬UObject指針屬性引用了一個資源,則加載包含這個屬性的對象(放在貼圖中,或者從gameinfo等引用)時,就會加載這個資源。如果處理不當,就會在游戲開始時加載全部資源。如果您希望美術/設計師能夠使用同一個UI作為硬指針來引用特定資源,而不必總是加載被引用資源,可以使用
FSoftObjectPath
或TSoftObjectPtr
。
FSoftObjectPath
是一個簡單的結構體,其中有一個字符串包含資源的完整名稱。如果您在類中添加這個類型的屬性,它就會像UObject *
屬性一樣顯示在編輯器中。它還會正確處理烘焙和重定向,因此如果您使用SoftObjectPath,就一定能在設備上正確工作。
TSoftObjectPtr
基本上是包含了FSoftObjectPath
的TWeakObjectPtr
,將用於設置特定類的模板,這樣就可以限制編輯器UI僅允許選擇特定類。如果被引用資源存在於內存中,則TSoftObjectPtr.Get()
將返回這個資源。如果不存在,可以調用ToSoftObjectPath()
來找出它引用的資源,使用下述方法加載這個資源,然后再次調用TSoftObjectPtr.Get()
來取消引用。如果美術或設計師要手動設置引用,則
TSoftObjectPtrs
和Soft Object Paths十分有用,但如果想要通過查詢等功能來查找符合特定要求的資源,而不加載所有資源,則可以使用資源注冊表和對象庫。
總結:
FSoftObjectPath(FStringAssetReference ):包含Asset位置字符串的結構體,可以指向UObject*
的資源類型。
TSoftObjectPtr(TAssetPtr):使用FSoftObjectPath構造出來的結構體,用於監視資源狀態,指向尚未加載但可以根據請求加載的資產的指針。
TAssetSubclassOf :指向已定義的基類的子類的指針,該子類尚未加載,但可以根據請求加載。用於指向藍圖而不是基本component(大概是因為藍圖是Asset)。
在C++里面使用:
//.h
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")
FStringAssetReference SAR;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")
TAssetPtr<UTexture2D> AP;
//.cpp
void UWC_TestUI::OnBtnClickCommonBtn_SARLoad()
{
FStringAssetReference SAR_SoUnrealPath;
//檢查是否指向一個有效的UObject,返回false則不是一個有效的UObject或者被初始化為null
if (SAR.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("SAR.AssetName:%s"), *SAR.GetAssetName());
UE_LOG(LogTemp, Warning, TEXT("SAR.AssetPathName:%s"), *(SAR.GetAssetPathName().ToString()));
UE_LOG(LogTemp, Warning, TEXT("SAR.AssetPathString:%s"), *SAR.GetAssetPathString());
SAR_SoUnrealPath = SAR.GetAssetPathString();
}else
{
//指定路徑
SAR_SoUnrealPath = TEXT("Texture2D'/Game/UI/Images/SoUnreal.SoUnreal'");
}
//資源同步加載
UObject * UE4PathObj = UAssetManager::GetStreamableManager().LoadSynchronous(SAR_SoUnrealPath);
//強轉一下
UTexture2D* UE4PathPtr = Cast<UTexture2D>(UE4PathObj);
if (UE4PathPtr != nullptr)
{
if (Img_SAR != nullptr)
{
Img_SAR->SetBrushFromTexture(UE4PathPtr);
}
}
}
void UWC_TestUI::OnBtnClickCommonBtn_APLoad()
{
FStringAssetReference AP_UE4Path;
//檢查是否指向一個有效的UObject,返回false則不是一個有效的UObject或者被初始化為null
if (AP.IsValid())
{
AP_UE4Path = AP.ToSoftObjectPath();
}else
{
//指定路徑
AP_UE4Path = TEXT("Texture2D'/Game/UI/Images/GetUnreal.GetUnreal'");
}
//注意這里可以傳模板進去,下面就不用再強轉了
UTexture2D * UE4PathPtr = UAssetManager::GetStreamableManager().LoadSynchronous<UTexture2D>(AP_UE4Path);
//強轉一下
//UTexture2D* UE4PathPtr = Cast<UTexture2D>(UE4PathObj);
if (UE4PathPtr != nullptr)
{
if (Img_AP != nullptr)
{
Img_AP->SetBrushFromTexture(UE4PathPtr);
}
}
}
注意AP是一個TAssetPtr,是一個用UTexture2D包起來的模板,所以在加載的時候已經有了它的類型,就不用強轉了。
特點
此方法的特點是:
- 簡單、對美術或者UI設計師十分友好,可以不動代碼而預覽效果。
- 在使用之前需要先進行手動指定引用,並且若是沒有手動指定引用則可以用代碼來指定。代碼指定的時候注意資源名或者路徑變更的時候會發生錯誤,因為是寫死在代碼里面的。這個時候維護就要花額外的時間。
本文標簽
游戲開發
、游戲開發基礎
、Unreal Engine
、UE資源加載
。