在UE4中,項目中的所有資源都是存儲在硬盤中,當需要用到資源時,則需要將其加載進入內存中使用。為了更好的表示(引用)資源,UE4提供了兩種引用資源的方式——硬引用、軟引用。
資源的硬引用
硬性引用,即對象 A 引用對象 B,並導致對象 B 在對象 A 加載時加載。通俗點說,硬引用所表示的資源在引用初始化時就加載進內存,因此硬引用的資源幾乎不需要加載方法。
硬指針
引用資源的最簡單方法是創建指針UProperty並為它指定一個類別,這種稱為硬指針。
在UE4中,如果有一個硬UObject指針屬性引用了一個資源(往往在藍圖上設置引用),則加載包含這個屬性的對象(放在貼圖中,或者從gameinfo等引用)時,就會加載這個資源。
UPROPERTY(EditDefaultsOnly, Category=Building)
USoundCue* ConstructionStartStinger;
FObjectFinder<T> / FClassFinder<T>
若需要用C++代碼而非藍圖來設置引用,則往往需要FObjectFinder、FClassFinder。
#include "UObject/ConstructorHelpers.h" \\需要include的頭文件
在UE4源碼里面,FObjectFinder構造函數里通過調用LoadObject()來加載資源,而FClassFinder構造函數里調用的也是LoadObject()。
注意在使用它們的時候還得遵守如下規則:
- 只能在類的構造函數中使用,如果在普通的邏輯代碼中嵌套這份代碼,會引起整個編譯器的crash。(實際上里面代碼就有檢查是否在構造函數里,否則crash)
- 其次,FObjectFinder/FClassFinder變量必須是static的,從而保證只有一份資源實例。
- FObjectFinder<T>:一般用來加載非藍圖資源,比如StaticMesh、Material、SoundWave、ParticlesSystem、AnimSequence、SkeletalMesh等資源:
static ConstructorHelpers::FObjectFinder<UTexture2D> ObjectFinder(TEXT("Texture2D'/Game/Textures/tex1.tex1'"));
UTexture2D* Texture2D = ObjectFinder.Object;
- FClassFinder<T>:一般用來加載藍圖資源並獲取藍圖Class。這是因為如果C++要用藍圖創建對象,必須先獲取藍圖的Class,然后再通過Class生成藍圖對象:
static ConstructorHelpers::FClassFinder<AActor> BPClassFinder(TEXT("/Game/Blueprints/MyBP"));
TSubclassOf<AActor> BPClass = BPClassFinder.Class;
...//利用Class生成藍圖對象
- FClassFinder的模版名不能直接寫UBlueprint,例如:FClassFinder<UBlueprint>是錯誤的。創建藍圖時選擇的是什么父類,則寫對應的父類名,假如是Actor,那么要寫成:FClassFinder<AActor>,否則無法加載成功。
- FClassFinder的模版名必須和TSubclassOf變量的模版名一致,當然也可使用UClass*代替TSubclassOf<T>。實際上TSubclassOf<T>也是UClass*,只是更加強調這個Class是從T派生出來的。
- 在啟動游戲時若報錯提示找不到文件而崩潰(例如:Default property warnings and errors:Error: COD Constructor (MyGameMode): Failed to find /Game/MyProject/MyBP.MyBP)
這是因為UE4資源路徑的一個規范問題,解決辦法有兩種:- 在copy reference出來的文件路徑后面加_C,例如:"Blueprint'/Game/Blueprints/MyBP.MyBP_C'"(_C可以理解為獲取Class的意思)。
- 去掉路徑前綴,例如:"/Game/Blueprints/MyBP"
資源的軟引用
軟性引用,即對象 A 通過間接機制(例如字符串形式的對象路徑)來引用對象 B。
硬引用的問題是在容易一開始就加載全部硬引用表示的資源,這可能導致資源載入時間過長。而軟引用則是可隨時靈活加載資源的一種引用,而不用硬性地一開始就加載。
FSoftObjectPaths、FStringAssetReference
- FSoftObjectPath:是一個簡單的結構體,其中包含了資源的完整名稱(一個字符串)。它實質就是用一個字符串來表示對應的資源,從而可以隨時通過字符串找到硬盤上的目標資源,將其載入進內存。
FSoftObjectPath.SolveObject() 可以檢查其引用的資源是否已經載入在內存中,若載入則返還資源對象指針,否則返還空。
FSoftObjectPath.IsPending() 可檢查資源是否已准備好可供訪問。而如何利用FSoftObjectPath加載資源進內存,后面還會說到。
- FStringAssetReference:其實只是一個聽起來更容易理解的別名,它實際在UE4源碼里是這樣的:
typedef FSoftObjectPath FStringAssetReference;
TSoftObjectPtr<T>
TSoftObjectPtr是包含了FSoftObjectPath的TWeakObjectPtr,可通過模板參數來設置特定資源類型,這樣就可以限制編輯器UI僅允許選擇特定的資源種類。
TSoftObjectPtr.Get() 可以檢查其引用的資源是否已經載入在內存中,若已載入則返還資源對象指針,否則返還空。想要資源加載進內存,則可以調用ToSoftObjectPath()來得到FSoftObjectPaths用於加載。
同步加載資源
LoadObject/LoadClass
- LoadObject<T>():加載UObject,一般用來加載非藍圖資源。
UTexture2D* Texture2D = LoadObject<UTexture2D>(nullptr,TEXT("Texture2D'/Game/Textures/tex1.tex1'"));
- LoadClass<T>():加載UClass,一般用來加載藍圖資源並獲取藍圖Class。實際上源碼里LoadClass的實現是調用LoadObject並獲取類型。
- LoadClass的模版名稱,和上面FClassFinder一樣,不能直接寫UBlueprint。
- LoadClass路徑規范也和上面的FClassFinder一樣,帶_C后綴或去掉前綴。
另外有兩個函數叫:StaticLoadObject()和StaticLoadClass(),是LoadObject()和LoadClass()的早期版本,前兩者需要手動強轉和填寫冗雜參數,后兩者則是前兩者的封裝,使用更方便,推薦使用后者。
TSubclassOf<AActor> BPClass = LoadClass<AActor>(nullptr, TEXT("/Game/Blueprints/MyBP"));
此外一提,還有一個可能常用的全局函數FindObject(),用來查詢資源是否載入進內存,若存在則返還資源對象指針,否則返還空。但是我們不用先查詢再使用LoadXXX,因為LoadXXX里本身就有用到FindObject來檢查存在性。
TryLoad/LoadSynchronous
- TryLoad():FSoftObjectPaths的方法,直接根據路徑加載資源。
- LoadSynchronous():TSoftObjectPtr<T>的方法,也是直接根據路徑加載資源。
由於軟引用里包含資源完整路徑名,因此無需再寫一次路徑名,而是調用如上成員方法來加載資源進內存。而軟引用的作用不僅如此,它還可以用於下面要介紹的資源異步加載方式。
異步加載資源
即使可以控制加載資源的時機,但如果加載的資源對象很大(或者同一時刻加載多個資源),還是會造成卡頓,為了避免阻塞主線程,異步加載的方式必不可少。
FStreamableManager.RequestAsyncLoad()
首先,需要創建FStreamableManager,官方建議將它放在某類全局游戲單例對象中,例如使用GameSingletonClassName在DefaultEngine.ini中指定的對象。
- FStreamableManager.RequestAsyncLoad():將異步加載一組資源並在完成后調用委托。
void UGameCheatManager::GrantItems()
{
//獲取 FStreamableManager的單例對象引用
FStreamableManager& Streamable = ...;
//得到一組軟引用
TArray<FSoftObjectPath> ItemsToStream;
for(int32 i = 0; i < ItemList.Num(); ++i)
ItemsToStream.AddUnique(ItemList[i].ToStringReference());
//根據一組軟引用來異步加載一組資源,加載完后調用委托
Streamable.RequestAsyncLoad(ItemsToStream, FStreamableDelegate::CreateUObject(this, &UGameCheatManager::GrantItemsDeferred));
}
void UGameCheatManager::GrantItemsDeferred()
{
//do something....
}
FStreamableManager其實也有同步加載的方法:SynchronousLoad()方法將進行一次簡單的塊加載並返回對象。
卸載資源
如果資源永不再使用,想將資源對象從內存上卸載,代碼如下:
Texture2D* mytex; //這里假設mytex合法有效
mytex->ConditionalBeginDestroy();
mytex = NULL;
GetWorld()->ForceGarbageCollection(true);
創建對象
UE4的對象(即從UObject派生出來的類對象)最好不要用C++的new/delete,而應使用UE4提供的對象生成方法,要不然繼承UObject的垃圾回收能力就無從用處。
創建一般對象
如果有UObject的派生類(非Actor、非Component),那么可使用NewObject()模板函數來創建其實例對象:
UMyObject* MyObject = NewObject<UMyObject>();
創建Actor派生類對象
生成AActor派生類對象不要用NewObject或new,而要用UWorld::SpawnActor()
UWorld* World = GetWorld();
FVector pos(150, 0, 20);
AMyActor* MyActor = World->SpawnActor<AMyActor>(pos,FRotator::ZeroRotator);
注意SpawnActor不能放在構造函數,但是可以放在其他時期的函數里,例如BeginPlay()、Tick()...否則可能會編譯后就crash。
創建Component派生類對象
為Actor創建組件,可使用UObject::CreateDefaultSubobject()模板函數
UCameraComponent* Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera0"));
- CreateDefaultSubobject必須寫在Actor的無參構造函數中,否則crash。
- CreateDefaultSubobject中的TEXT或者FName參數在同一個Actor中不能重復,否則crash。
- 一定要添加RegisterComponent(),否則編輯器不會顯示。
創建藍圖對象
藍圖由於本質是一種腳本,不是直接的C++類,因此往往需要借助動態類型來生成藍圖對象。所有的加載資源並創建到場景中的方式都離不開SpawnActor這一句代碼。
1.通過已確定的父類來生成藍圖對象
AMyActor* spawnActor = GetWorld()->SpawnActor<AMyActor>(AMyActor::StaticClass());
如果你的藍圖派生於某個C++類,那么可以直接訪問該類的StaticClass()並用於SpawnActor來創建藍圖對象。
2.通過UClass生成藍圖對象
UClass* BPClass = LoadClass<AActor>(nullptr, TEXT("/Game/Blueprints/MyBP")); //TSubclassOf<AActor>同理
AActor* spawnActor = GetWorld()->SpawnActor<AActor>(BPClass);
3.通過UObject生成藍圖對象
若得到UObject則需要先轉換成UBlueprint,再通過GeneratedClass獲取UClass來生成藍圖對象
FStringAssetReference asset = "Blueprint'/Game/BluePrint/TestObj.TestObj'";
UObject* itemObj = asset.ResolveObject();
UBlueprint* gen = Cast<UBlueprint>(itemObj);
if (gen != NULL)
{
AActor* spawnActor = GetWorld()->SpawnActor<AActor>(gen->GeneratedClass);
}
參考
[UE4]C++實現動態加載的問題:LoadClass
Unreal Cook Book:創建對象的的幾種姿勢(C++)
系列其他文章:Aery的UE4 C++開發之旅系列文章