【UE4全反射松耦合框架筆記】第一章 UE4框架基礎(下)

第一章 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());

解疑:

  1. 爲什麼加載藍圖時,不直接生成AActor對象?
    對於除藍圖類外的其他資源加載時,我們可以直接使用其C++類型,但對於藍圖類,我們必須全部使用UObject或UBlueprint類型。其實可以這樣理解,通過資源路徑加載進來的藍圖類都是同一種類型即繼承UObject的UBlueprint類,其具體繼承或實現的C++類通過序列化和反射完成。所以我們加載進來的藍圖類就是一種藍圖資源,它的類型是UBlueprint,我們通過它的成員變量GeneratedClass獲取其藍圖繼承的元數據。通過這個UClass類型的元數據,就可以生成我們需要的對象了。

  2. 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));
		}
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章