第一章 UE4框架基礎(下)
文章目錄
六、反射應用詳解
常用反射宏
- UCLASS
- USTRUCT
- UENUM
- UPROPERTY
- UFUNCTION
通過反射獲取實例的函數
- StaticLoadClass
- StaticLoadObject
- LoadClass
- LoadObject
- FObjectFinder
- FClassFinder
- FindObject
UObject和UClass的關係
UObject是繼承UObject實例對象的父類,UE4的大部分核心功能和實例對象均繼承此類。UObject也是UClass的基類,UClass保存有對象的反射數據(元數據)。每個類對應一個UClass對象,如果同一個類有多個實例,也是隻有一個UClass反射對象,這個UClass反射對象中存儲這個類所有實例的反射數據。
UObject及其子類可以通過(UClass*)將其強轉爲UClass類型
這裏解釋一下Cast<>和()兩種強轉的區別:
Cast<>只能轉換有邏輯的轉換關係,一般爲父類轉子類,轉換成功返回有效指針,轉換失敗返回NULL;
而()這種強轉是真的強行轉換,若兩個類之間沒有父子(多層)繼承關係,即使轉換成功,很有可能會在調用時出錯。
獲取UENUM反射的枚舉對象
UENUM()
enum class ERefState : uint8
{
None,
Active,
Disable
}
/*通過反射獲取枚舉類的對象,
第一個表示在所有包中查找,
第二個爲枚舉類名(將FString字符串轉換爲TCHAR指針),
第三個參數是否與傳入的類完全匹配*/
UEnum* EnumPtr = FindObject<UEnum>((UObject*)ANY_PACKAGE, *FString("ERefState"), true);
EnumPtr->GetEnumName((int32)1); //返回枚舉第2個值-Active
獲取藍圖反射對象
//獲取指定藍圖對象
UBlueprint* ActorBP = LoadObject<UBlueprint>(NULL, TEXT("Blueprint'/Game/Blueprint/GameFrame/NewBlueprint.NewBlueprint'"));
//獲取藍圖對象對應的UClass,UClass保存類的元數據,可以通過其生成對象
UClass* ActorClass = (UClass*)ActorBP->GeneratedClass;
GetWorld()->SpawnActor<AActor>(ActorClass, FVector::ZeroVector, FRotator::ZeroRotator);
獲取UPROPERTY反射的屬性對象
//先聲明一個擁有UPROPERTY屬性反射的類
UCLASS()
class XXX_API AMyActorClass : public AActor
{
...
public:
UPROPERTY(EditAnywhere)
FString ActorName;
UPROPERTY(EditAnywhere)
bool IsActive;
...
}
//先獲取到AMyActorClass的實例對象
TArray<AActor*> Actors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyActorClass::StaticClass(), Actors);
if(Actors.Num() > 0)
{
AMyActorClass* MyActor = Cast<AMyActorClass>(Actors[0]);
UObject* MyObject = (UObject*)MyActor;
//迭代UPROPERTY反射的字段對象,通過GetClass函數獲取到類的元數據,其中包含類的反射信息
for(TFieldIterator<UProperty> ProIt(MyObject->GetClass()); ProIt; ++ProIt)
{
UProperty* Property = *ProIt; //獲取當前指向的UProperty對象
//操作FString類型
if(Property->GetNameCPP().Equals("ActorName")) //獲取UPROPERTY反射的屬性變量名並判斷是否與ActorName字符串一致
{
//如果與ActorName一致,則此對象保存的是FString類型屬性的反射信息,將其轉換爲UStrProperty對象
UStrProperty* StrProperty = Cast<UStrProperty>(Property);
if(StrProperty) //如果轉換成功
{
//獲取反射對象包含的值的地址
void* ValPtr = Property->ContainerPtrToValuePtr<uint8>(MyObject);
//void* ValPtr = Property->ContainerPtrToValuePtr<void*>(MyObject); //這個也可以
//獲取到反射對象包含的值
FString ActorName = StrProperty->GetPropertyValue(ValPtr);
//修改爲新的值
StrProperty->SetPropertyValue(ValPtr, FString("New Value"));
}
}
//操作bool類型
if(Property->GetNameCPP().Equals("IsActive"))
{
UBoolProperty* BoolProperty = Cast<UBoolProperty>(Property);
if(BoolProperty)
{
void* BoolValPtr = Property->ContainerPtrToValuePtr<uint8>(MyObject);
bool IsActive = BoolValPtr->GetPropertyValue(BoolValPtr);
BoolValPtr->SetPropertyValue(BoolValPtr, false);
}
}
}
}
獲取UFUNCTION反射的對象
方法一:FScriptDelegate
//聲明一個包含UFUNCTION反射的類
UCLASS()
class XXX_API AMyActor : public AActor
{
...
public:
//FScriptDelegate方法調用的函數不能有返回值,如果要修改,只能通過引用參數修改
UFUNCTION()
void MyFuncOne();
UFUNCTION()
void MyFuncTwo(FString Info, int32 Count);
...
}
//調用函數
AMyActor* MyActor; //獲取AMyActor指針對象,此處定義省略
FScriptDelegate MyDelegate;
//綁定並調用無參函數
MyDelegate.BindUFunction(MyActor, FName("MyFuncOne"));
MyDelegate.ProcessDelegate<AMyActor>(NULL);
//綁定並調用有參函數
MyDelegate.BindUFunction(MyActor, FName("MyFuncTwo"));
//聲明並定義結構體用於傳參
struct {
FString InfoStr;
int32 Count;
} FuncTwoParam;
FuncTwoParam.InfoStr = FString("Hello");
FuncTwoParam.Count = 1024;
MyDelegate.ProcessDelegate<AMyActor>(&FuncTwoParam);
方法二:TBaseDelegate
//聲明一個包含UFUNCTION反射的類
UCLASS()
class XXX_API AMyActor : public AActor
{
...
public:
//此方法不能使用引用類型
UFUNCTION()
bool MyFunc(FString InfoStr, int32 Count);
...
}
//調用函數
AMyActor* MyActor; //獲取AMyActor指針對象,此處定義省略
TBaseDelegate<bool, FString, int32> FuncDelegate = TBaseDelegate<bool, FString, int32>::CreateUFunction(MyActor, "MyFunc");
bool DelegateResult = FuncDelegate.Execute(FString("Hello"), 1024);
方法三:UFunction
//聲明一個包含UFUNCTION反射的類
UCLASS()
class XXX_API AMyActor : public AActor
{
...
public:
//此方法不能使用引用類型
UFUNCTION()
int32 MyFunc(FString InfoStr, int32& Count);
...
}
//調用函數
AMyActor* MyActor; //獲取AMyActor指針對象,此處定義省略
UFunction* Func = MyActor->FindFunction(FName("MyFunc"));
if(Func)
{
struct{
FString InfoStr;
int32 Count;
} FuncParam;
FuncParam.InfoStr = FString("Hello");
FuncParam.Count = 1024;
MyActor->ProcessEvent(Func, &FuncParam);
//獲取返回值
uint8* RetValPtr = (uint8*)&FuncParam + Func->ReturnValueOffset; //參數地址(指向參數的指針)+返回值位移,獲得返回值地址
//此處也可以是 void* RetValPtr = (uint8*)&FuncParam + Func->ReturnValueOffset;
int32* RetVal = (int32*)RetValPtr; //RetVal是返回值的指針,其值爲*RetVal
//int32 RetVal = (int32)*RetValPtr; //地址取值(即返回值)轉換爲int32類型
}
七、資源同步、異步加載
資源一般有兩種狀態,一種是已經加載到內存;另一種就是未加載到內存,即是在磁盤中。
FindObject模板函數可以從內存中查找資源對象,當資源未加載到內存時,將返回nullptr。
UStaticMesh* Mesh = FindObject<UStaticMesh>(NULL, TEXT("SkeletalMesh'/Game/Resource/SCTanks/Meshes/SK_TankPzIV.SK_TankPzIV'"));
LoadObject則可以將資源從磁盤中加載到內存,只要調用函數,即可加載到內存。
LoadObject<UStaticMesh>(NULL, TEXT("SkeletalMesh'/Game/Resource/SCTanks/Meshes/SK_TankPzIV.SK_TankPzIV'"));
如果要加載多個資源對象到內存,如果每個資源都通過LoadObject加載會很繁雜。所以我們可以通過創建一個資源表的方法,在藍圖中設置資源路徑,然後在C++中通過資源表加載資源對象。
//創建保存資源路徑的結構體
//如果添加BlueprintType標識符,則使用此類型的變量可以聲明爲BlueprintReadWrite,即可以在藍圖中獲取或設置變量
//struct類型如果要在藍圖中使用,聲明變量時不要聲明爲結構體指針
USTRUCT()
struct FWealthNode
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
FName WealthName;
UPROPERTY(EditAnywhere)
FStringAssetReference WealthPath; //資源的路徑引用,FStringAssetReference是FSoftObjectPath的別名
};
//創建繼承自DataAsset的類,用於保存資源的數據
//UDataAsset是一種用於保存數據的藍圖,如資源的引用等
UCLASS()
class XXX_API UWealthAssetData : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
TArray<FWealthNode> WealthNode;
UPROPERTY(EditAnywhere)
TArray<UTexture2D*> WealthTexture;
};
//保存資源數據並加載到內存, 我們這個例子將不斷循環獲取對象並設置爲組件顯示
UCLASS()
class XXX_API AWealthActor : public AActor
{
...
public:
void UpdateMesh();
public:
UPROPERTY(EditAnywhere)
UWealthAssetData* WealthAssetData;
private:
FTimerHandle UpdateMeshHandle;
int32 MeshIndex = 0;
...
}
void AWealthActor::BeginPlay()
{
Super::BeginPlay();
FTimerDelegate TimerDelegate = FTimerDelegate::CreateUObject(this, &AWealthActor::UpdateMesh); //創建計時器委託
GetWorld()->GetTimerManager().SetTimer(UpdateMeshHandle, TimerDelegate, 1.f, true); //循環每1秒執行一次函數
}
void AWealthActor::UpdateMesh()
{
if(WealthAssetData && WealthAssetData->WealthNode.Num()>0)
{
for(int i = 0; i < WealthAssetData->WealthNode.Num(); ++i)
{
//如果LoadObject已經加載到內存,就直接獲取內存的資源對象,否則,加載到內存。只加載一次。
UStaticMesh* Mesh = LoadObject<UStaticMesh>(NULL, WealthAssetData->WealthNode[MeshIndex].WealthPath.ToString());
StaticMeshComp->SetStaticMesh(Mesh);
MeshIndex = ++MeshIndex % WealthAssetData->WealthNode.Num(); //循環獲取數組下標
}
}
}
使用UObjectLibrary獲取路徑下資源數據的方法:
public:
void ObjectLibraryOperate();
protected: //private:只允許本類成員訪問; protected:允許本類和子類成員訪問; public:允許所有成員訪問。
UObjectLirbary* ObjectLibrary;
//FSoftObjectPath有一個別名爲FStringAssetReference
TArray<FSoftObjectPath> TexturePath;
void XXX::ObjectLibraryOperate()
{
if(!ObjectLibrary) //如果ObjectLibrary值無效
{
/*
@Param 對象庫可以創建的對象類型及其子類
@Param 可以創建的對象類型是否包含藍圖類
@Param 是否是弱引用類型,true的話將允許被GC
*/
ObjectLibrary = UObjectLibrary::CreateLibrary(UObject::StaticClass(), false, false);
ObjectLibrary->AddToRoot(); //註冊到根節點,以免被GC
}
//搜索並獲取路徑下的所有資源,這裏搜索的是貼圖資源
ObjectLibrary->LoadAssetDataFromPath(TEXT("/Game/Resource/UI/Texture/MenuTex"));
/*聲明一個資源數據數組用於存儲資源數據
FAssetData是一個結構體,用於存儲資源寄存器搜索到的資源數據。它一般用於臨時使用,不要將其序列化。
這裏注意一下,有個類叫做UDataAsset,用於在藍圖中保存數據
*/
TArray<FAssetData> TextureData; //將剛剛獲取到的資源數據保存到TextureData
ObjectLibrary->GetAssetDataList(TextureData);
//將資源數據轉換並保存到TexturePath數組
for(int32 i = 0; i < TexturePath.Num(); ++i)
{
TexturePath.AddUnique(TextureData[i].ToSoftObjectPath());
}
}
FStreamableManager類提供異步、同步加載函數:RequestAsyncLoad和RequestSyncLoad
通過FStreamableDelegate傳入回調函數批量異步加載:RequestAsyncLoad
通過FStreamableDelegate傳入回調函數單個異步加載:RequestAsyncLoad
通過TFunction傳入回調函數批量異步加載:RequestAsyncLoad
通過TFunction傳入回調函數單個異步加載:RequestAsyncLoad
批量同步加載,不需要回調函數:RequestSyncLoad
單個同步加載,不需要回調函數:RequestSyncLoad
其它函數和對象說明:
函數或對象 | 說明 |
---|---|
LoadSynchronous | 對RequestSyncLoad封裝的函數,同步調用 |
FStreamableHandle | 同步或異步函數調用返回的句柄 |
bool HasLoadCompleted() const | 是否加載完畢 |
bool IsLoadingInProgress() const | 是否正在加載 |
bool BindCompleteDelegate(FStreamableDelegate NewDelegate) | 綁定加載完成後的回調函數 |
bool BindCancelDelegate(FStreamableDelegate NewDelegate) | 綁定取消加載後的回調函數 |
bool BindUpdateDelegate(FStreamableUpdateDelegate NewDelegate) | 綁定更新毀掉函數時的回調函數 |
void GetRequestedAssets(TArray<FSoftObjectPath>& AssetList) const | 獲取加載的批量資源 |
void GetLoadedAssets(TArray<UObject*>& LoadedAssets) cosnt | 獲取加載的資源 |
UObject* GetLoadedAsset() const | 獲取加載的單個資源 |
void GetLoadedCount(int32& LoadedCount, int32& RequestedCount) const | 獲取加載的數量 |
float GetProgress() const | 獲取加載進度 |
struct FStreamableManager* GetOwningManager() const | 獲取對應的FStreamableManager |
使用FSoftObjectPath(FStringAssetReference)生成藍圖類對象
上邊已經介紹瞭如何通過資源引用將其加載到內存,並獲取其默認對象。
下邊將介紹如何通過資源引用獲取的默認對象生成一個新的對象。
!!視頻中使用此方法打包後的項目會崩潰,我沒有測試!!
UPROPERTY(EditAnywhere)
FStringAssetReference ActorPathRef; //在藍圖中將其引用一個Actor藍圖
UObject* ActorObj = LoadObject<UObject>(NULL, *ActorPathRef.GetAssetPathString()); //解疑見下
//這裏解釋一下Cast和C++的指針類型強轉:
//通過Cast方法進行的轉換,如果轉換成功(一般父類轉子類,子類到父類不需要強轉),返回指針;如果失敗,返回nullptr
//而(Type*)這樣的指針類型強轉,會直接將指針類型進行轉換,但無邏輯的強轉會導致成員調用失敗
UBlueprint* ActorBlueprint = Cast<UBlueprint>ActorObj;
GetWorld()->SpawnActor<AActor>(ActorBlueprint->GeneratedClass, FVector::ZeroVector, FRotator::ZeroRotator);
=========================================================================================================
UBlueprint* ActorBlueprintObj = LoadObject<UBlueprint >(NULL, *ActorPathRef.GetAssetPathString());
解疑:
-
爲什麼加載藍圖時,不直接生成AActor對象?
對於除藍圖類外的其他資源加載時,我們可以直接使用其C++類型,但對於藍圖類,我們必須全部使用UObject或UBlueprint類型。其實可以這樣理解,通過資源路徑加載進來的藍圖類都是同一種類型即繼承UObject的UBlueprint類,其具體繼承或實現的C++類通過序列化和反射完成。所以我們加載進來的藍圖類就是一種藍圖資源,它的類型是UBlueprint,我們通過它的成員變量GeneratedClass獲取其藍圖繼承的元數據。通過這個UClass類型的元數據,就可以生成我們需要的對象了。 -
LoadObject加載進來的是什麼?
通過第一個問題的解釋,我們大概已經清楚,LoadObject方法加載進來的對象是一個藍圖類對象。
通過使用LoadObject加載藍圖的代碼發現,其中一種藍圖就對應一種C++類型,比如藍圖類對應的就是UBlueprint,靜態模型對應的UStaticMesh,數據資源對應的UDataAsset等。而在編輯器通過硬引用的方式設置的對象,都是已經加載到內存的,所以有硬引用的對象在類初始化時,其引用的對象均已加載到內存。
八、異步加載UClass類型
因爲使用UBlueprint獲取UClass元數據的方法會在打包後崩潰(測試和原因待驗證),所以下面介紹其他方法。
這裏使用上面的FStreamableManager和TSoftClassPtr及FSoftObjectPath加載多個資源並實例化。
TArray<TSoftClassPtr<UObject>> ObjectClassPtrs; //在藍圖中設置要加載的資源路徑
FStreamableManager StreamableManager; //資源加載管理器
FStreamableHandle StreamableHandle; //資源加載句柄
//異步加載資源
void XXX::AsyncLoadAsset()
{
//將資源路徑類型轉換
TArray<FSoftObjectPath> ObjectPaths;
for(int i = 0; i < ObjectClassPtrs.Num(); ++i)
{
ObjectPaths.Push(ObjectClassPtrs[i].ToSoftObjectPath());
}
//異步加載資源,設置加載完成回調函數,並返回句柄
StreamableHandle = StreamableManager.ReuqestAsyncLoad(ObjectPaths, FStreamableDelegate::CreateUObject(this, XXX::LoadCompleted));
}
void XXX::LoadCompleted()
{
//返回加載的資源對象
TArray<UObject*> Objects;
StreamableHandle->GetLoadedAssets(Objects);
//將其強轉爲UClass類型,並生成其類型的對象
for(int i = 0; i < Objects.Num(); ++i)
{
UClass* NewClass = Cast<UClass>();
GetWorld()->SpawnActor<AActor>(NewClass, FVector::ZeroVector, FRotator::ZeroRotator); //上邊我們設置的是AActor子類的類型軟引用
}
}
九、LatentAction潛在事件
先按照網上給的Demo實現一個繼承FPendingLatentAction的潛在事件。
這個結點會在Duration計時到一半時,輸出HalfExec;而全部計時結束時,輸出CompleteExec。
class FTwiceDelayAction : public FPendingLatentAction
{
public:
float TotalTime;
float TimeRemaining;
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
DELAY_EXEC& execRef; //DELAY_EXEC枚舉類的聲明定義在下面
bool bHalfTriggered = false;
public:
FTwiceDelayAction(float Duration, const FLatentActionInfo& LatentInfo, DELAY_EXEC& exec)
:TotalTime(Duration)
,TimeRemaining(Duration)
,ExecutionFunction(LatentInfo.ExecutionFunction)
,OutputLink(LatentInfo.Linkage)
,CallbackTarget(LatentInfo.CallbackTarget)
,execeRef(exec)
{}
virtual void UpdateOperation(FLatentResponse& Response) override
{
TimeRemaining -= Response.ElapsedTime();
//當時間剩餘一半時,將execRef設置爲HalfExec,並調用回調函數。針腳將從HalfExec輸出
if(TimeRemaining < TotalTime / 2.0f && !bHalfTriggered)
{
execRef = DELAY_EXEC::HalfExec;
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget); //調用回調函數
bHalfTriggered = true;
}
else if(TimeRemaining < 0.0f)
{
execRef = DELAY_EXEC::CompleteExec;
Response.TriggerLink(ExecutionFunction, Outputlink, CallbackTarget); //調用回調函數
Response.DoneIf(TimeRemaining < 0.0f); //終止Latent
}
}
}
UENUM(BlueprintType)
enum class DELAY_EXEC :uint8
{
HalfExec,
CompleteExec
}
UCLASS()
class XXX_API ULantentActionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/*
標識符解釋:
HidePin="Param" 隱藏腳針,使用此標識符,每個函數只能隱藏一個針腳
DefaultToSelf="Param" 使用結點自身上下文,就是把變量值設置爲self
Latent 指明這個函數時隱式事件,藍圖調用函數時,結點右上角會出現一個時鐘
LatentInfo="param" 隱式事件會有一個FLatentActionInfo類型的參數,用於指出此參數
ExpandEnumAsExecs="param" 將針腳按照枚舉值展開,且枚舉類型必須有UENUM標識符
*/
UFUNCTION(BlueprintCallable, meta=(HidePin="WorldContextObject", DefaultToSelf="WorldContextObject", Latent, LatentInfo="LatentInfo", ExpandEnumAsExecs="exec"))
static void TwiceDelay(UObject* WorldContextObject, struct FLatentActionInfo LatentInfo, float Duration, DELAY_EXEC& exec);
}
void ULantentActionLibrary::TwiceDelay(UObject* WorldContextObject, FLatentActionInfo LatentInfo, float Duration, DELAY_EXEC& exec)
{
//從一個包含上下文的對象中獲取World,這裏的對象就是結點自身。如果獲取的World值有效,將通過World獲取隱式事件管理器對象
if(UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if(LatentActionManager.FindExistingAction<FTwiceDelayAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == NULL)
{
//創建一個LatentAction對象,並交給LantentActionManager來管理
LatentActionManager.AddNewAction(LatentInfo.CallabckTarget, LatentInfo.UUID, new FTwiceDelayAction(Duration, LatentInfo, exec));
}
}
}