【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));
		}
	}
}

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