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