笔者在 《UE4开发C++沙盒游戏教程笔记(一)》中已经推荐过文章讲述 UE4 反射功能的概要,此处就推荐这篇更加深入的文章:
《UE4反射机制》 (实现原理部分稍有难度)
那如何通过类的名称字符串来生成类的对象呢?有以下几个通过反射获取实例的方法:
所有的基于 UObject 创建的类有两种存在形式,一种是已经加载到 UE4 内存里的,一种是存储在硬盘里的。通过上面的方法,我们可以将已存在内存中的资源加载出来;存储在硬盘里的则加载到内存后再加载出来使用。
UClass 也是一个基于 UObject 的类,不过它负责存储 UObject 的反射数据(元数据,即描述数据属性的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能)。它是 UE4 反射系统的重要组成部分,二者可以通过这两个方法相互获取到对方的实例:
UObject::StaticClass()
和 UClass::GetDefaultObject()
在 Public/Reflect 路径下创建以下 C++ 文件:
新建一个 Object 类,命名为 RefObj,作为需要获取并生成的 UObject。
新建两个 Actor 类,分别命名为 SrcActor 和 DecActor,分别作为被获取方法和变量的 Actor;索取者 Actor。
给通用类的 Debug 方法设置一个默认显示时间,免得每次都要写。
FWCommon.h
namespace FWHelper
{
FORCEINLINE void Debug(FString Message, float Duration = 500.f) // 默认持续时间为 500
{
if (GEngine) GEngine->AddOnScreenDebugMessage(-1, Duration, FColor::Yellow, Message);
}
}
接下来我们希望通过 RefObj
这个 UObject 类的名字来实例化一个它的对象。
先给它里面定义一个方法用于输出 Debug 语句。
RefObj.h
public:
void EchoInfo();
RefObj.cpp
// 引入头文件
#include "Common/FWCommon.h"
void URefObj::EchoInfo()
{
FWHelper::Debug("URefObj --> EchoInfo", 120.f);
}
在 DecActor 这里实例化 RefObj。
DecActor.h
public:
// 反射实例化对象
void ReflectInstance();
DecActor.cpp
// 引入头文件
#include "Reflect/RefObj.h"
#include "Common/FWCommon.h"
void ADecActor::BeginPlay()
{
Super::BeginPlay();
ReflectInstance();
}
void ADecActor::ReflectInstance()
{
// 通过 UClass 获取对象
UClass* RefObjClass = StaticLoadClass(UObject::StaticClass(), nullptr, TEXT("URefObj"));
URefObj* RefObjIns = Cast<URefObj>(RefObjClass);
RefObjIns->EchoInfo();
// 通过 UObject 获取对象
UObject* RefObjPtr = StaticLoadObject(UObject::StaticClass(), nullptr, TEXT("URefObj"));
URefObj* RefObjOth = Cast<URefObj>(RefObjPtr);
RefObjOth->EchoInfo();
// 看看两者获取的对象是不是同一个
if (RefObjIns == RefObjOth) FWHelper::Debug("The Same");
}
运行游戏,将 DecActor 拖到场景里,运行游戏,可以看见 DecActor 的 ReflectInstance()
、RefObj 的 EchoInfo()
里面的内容都正常输出了。
FindObject()
只会从内存里找,而不会到硬盘里加载;而 LoadClass()
在内存里没找到则可以从硬盘里加载。
在 RefObj 里声明一个枚举。
RefObj.h
UENUM()
enum class ERefState : uint8
{
None,
Active,
Disable
};
UCLASS()
class FRAMECOURSE_API URefObj : public UObject
{
};
来到 DecActor,注释掉前面的部分语句;并通过枚举的名字来获得枚举的实例。
DecActor.cpp
void ADecActor::ReflectInstance()
{
UClass* RefObjClass = StaticLoadClass(UObject::StaticClass(), nullptr, TEXT("URefObj"));
URefObj* RefObjIns = Cast<URefObj>(RefObjClass);
//RefObjIns->EchoInfo();
UObject* RefObjPtr = StaticLoadObject(UObject::StaticClass(), nullptr, TEXT("URefObj"));
URefObj* RefObjOth = Cast<URefObj>(RefObjPtr);
//RefObjOth->EchoInfo();
//if (RefObjIns == RefObjOth) FWHelper::Debug("The Same");
// 反射 Enum
UEnum* EnumPtr = FindObject<UEnum>((UObject*)ANY_PACKAGE, *FString("ERefState"), true);
// 返回 1 号枚举(也就是 Active)
FWHelper::Debug(EnumPtr->GetEnumName((int32)1));
}
运行游戏,可以看到左上角输出了 ERefState 的第二个枚举值。
在 Blueprint 文件夹内新建一个叫 Reflect 的文件夹,在它里面创建 Actor 蓝图,取名叫 RefActorBP。接下来就是要通过反射来获取这个蓝图 Actor 并把它生成到场景。
打开蓝图,给它添加一个网格体组件,并随便添加一个模型资源。
在内容管理器选中这个蓝图,按 Ctrl + C 可以获得它的地址,待会填进代码里。
DecActor.cpp
void ADecActor::ReflectInstance()
{
UEnum* EnumPtr = FindObject<UEnum>((UObject*)ANY_PACKAGE, *FString("ERefState"), true);
//FWHelper::Debug(EnumPtr->GetEnumName((int32)1));
// 填进这里
UBlueprint* RefActorBP = LoadObject<UBlueprint>(NULL, TEXT("Blueprint'/Game/Blueprint/Reflect/RefActorBP.RefActorBP'"));
TSubclassOf<AActor> RefActorClass = (UClass*)RefActorBP->GeneratedClass;
GetWorld()->SpawnActor<AActor>(RefActorClass, FVector::ZeroVector + FVector(0.f, 0.f, 60.f), FRotator::ZeroRotator);
}
编译代码后,在场景中把 PlayerStart 的 X 坐标调成 -300,方便观察。运行游戏,可看见刚刚放置 RefActorBP 的位置出现了刚刚设置的网格体模型。
在 SrcActor 声明两个带 UPROPERTY()
宏的变量并且初始化。
SrcActor.h
public:
UPROPERTY(EditAnywhere)
FString ActorName;
UPROPERTY(EditAnywhere)
bool IsActive;
SrcActor.cpp
ASrcActor::ASrcActor()
{
ActorName = FString("ASrcActor");
IsActive = true;
}
DecActor.h
// 提前声明
class ASrcActor;
UCLASS()
class FRAMECOURSE_API ADecActor : public AActor
{
GENERATED_BODY()
public:
// 反射操作 UProperty 对象
void ControlUProperty();
private:
ASrcActor* SrcAct;
UObject* SrcObj;
};
笔者的 4.26 版本,UProperty 已经改成 FProperty 了;并且 Cast<> 强转不适合用于 FProperty,要改成 CastField<>。笔者这里就改一下。
BeginPlay()
里的 ReflectInstance()
注释掉。
DecActor.cpp
// 引入头文件
#include "Kismet/GameplayStatics.h"
#include "Reflect/SrcActor.h"
void ADecActor::BeginPlay()
{
Super::BeginPlay();
//ReflectInstance();
TArray<AActor*> ActArray;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASrcActor::StaticClass(), ActArray);
if (ActArray.Num() > 0) {
SrcAct = Cast<ASrcActor>(ActArray[0]);
SrcObj = (UObject*)SrcAct;
// 反射操作 UProperty 对象
ControlUProperty();
}
}
// 老师的 4.19 版本代码
/*
void ADecActor::ControlUProperty()
{
for (TFieldIterator ProIt(SrcObj->GetClass()); ProIt; ++ProIt) {
UProperty* Property = *ProIt;
// 操作 String 类型
if (Property->GetNameCPP().Equals("ActorName")) {
UStrProperty* StrProperty = Cast(Property);
if (StrProperty) {
void* ValPtr = Property->ContainerPtrToValuePtr(SrcObj);
FWHelper::Debug(FString("ActorName Before --> ") + StrProperty->GetPropertyValue(ValPtr));
StrProperty->SetPropertyValue(ValPtr, FString("New Name"));
}
FWHelper::Debug(FString("ActorName After --> ") + SrcAct->ActorName);
}
// 操作 Bool 类型
if (Property->GetNameCPP().Equals("IsActive")) {
UBoolProperty* BolProperty = Cast(Property);
if (BolProperty) {
void* ValPtr = Property->ContainerPtrToValuePtr(SrcObj);
FWHelper::Debug(FString("IsActive Before --> ") + FString::FromInt(BolProperty->GetPropertyValue(ValPtr)));
BolProperty->SetPropertyValue(ValPtr, false);
}
FWHelper::Debug(FString("IsActive After --> ") + FString::FromInt(SrcAct->IsActive));
}
}
}
*/
// 笔者的 4.26 版本代码
void ADecActor::ControlUProperty()
{
// 迭代所有的 FProperty 变量
for (TFieldIterator<FProperty> ProIt(SrcObj->GetClass()); ProIt; ++ProIt) {
FProperty* Property = *ProIt;
// 操作 String 类型
if (Property->GetNameCPP().Equals("ActorName")) { // 如果当前迭代器指向的变量名为 ActorName
FStrProperty* StrProperty = CastField<FStrProperty>(Property); // 强转为字符 FProperty
if (StrProperty) {
void* ValPtr = Property->ContainerPtrToValuePtr<uint8>(SrcObj); // 获取指向值的指针
FWHelper::Debug(FString("ActorName Before --> ") + StrProperty->GetPropertyValue(ValPtr));
StrProperty->SetPropertyValue(ValPtr, FString("New Name"));
}
FWHelper::Debug(FString("ActorName After --> ") + SrcAct->ActorName);
}
// 操作 Bool 类型
if (Property->GetNameCPP().Equals("IsActive")) {
FBoolProperty* BolProperty = CastField<FBoolProperty>(Property);
if (BolProperty) {
void* ValPtr = Property->ContainerPtrToValuePtr<uint8*>(SrcObj);
FWHelper::Debug(FString("IsActive Before --> ") + FString::FromInt(BolProperty->GetPropertyValue(ValPtr)));
BolProperty->SetPropertyValue(ValPtr, false);
}
FWHelper::Debug(FString("IsActive After --> ") + FString::FromInt(SrcAct->IsActive));
}
}
}
编译后,把 SrcActor 放进场景。运行游戏,可见左上角输出了 DecActor 的 ControlUProperty()
方法里面的内容,可见更改 UProperty 变量成功了。
来到 SrcActor,先声明并定义一个被 UFUNCTION()
宏修饰的方法。
SrcActor.h
public:
UFUNCTION()
void UFunOne();
SrcActor.cpp
// 引入头文件
#include "Common/FWCommon.h"
void ASrcActor::UFunOne()
{
FWHelper::Debug("ASrcActor --> UFunOne");
}
声明一个方法,利用 FScriptDelegate 类型的委托句柄绑定 SrcActor 的 UFunOne()
,随后调用它。
DecActor.h
public:
// 调用 UFunction 方法一:FScriptDelegate
void RunUFunOne();
DecActor.cpp
void ADecActor::BeginPlay()
{
Super::BeginPlay();
if (ActArray.Num() > 0) {
SrcAct = Cast<ASrcActor>(ActArray[0]);
SrcObj = (UObject*)SrcAct;
//ControlUProperty();
// 调用 UFunction 方法一:FScriptDelegate
RunUFunOne();
}
}
void ADecActor::RunUFunOne()
{
FScriptDelegate FunDelOne;
FunDelOne.BindUFunction(SrcObj, FName("UFunOne"));
FunDelOne.ProcessDelegate<UObject>(NULL);
}
通过 FScriptDelegate,就不用总是提前声明一个委托,然后绑定再执行。现在这样就相当于在栈上声明了一个委托。
不过,通过 FScriptDelegate 是无法处理带返回值的方法的。但我们可以采用传引用的方法返回值。
在 SrcActor 重新声明并定义一个带字符串形参和 int32
类型引用的方法。
SrcActor.h
public:
UFUNCTION()
void UFunTwo(FString InfoStr, int32& Count);
SrcActor.cpp
void ASrcActor::UFunTwo(FString InfoStr, int32& Count)
{
FWHelper::Debug(InfoStr + FString(" --> ") + FString::FromInt(Count));
Count = 980;
}
注释掉先前的执行语句。通过声明一个匿名结构体,赋值后作为实参传入委托句柄所绑定的方法。
DecActor.cpp
void ADecActor::RunUFunOne()
{
FScriptDelegate FunDelOne;
FunDelOne.BindUFunction(SrcObj, FName("UFunOne"));
//FunDelOne.ProcessDelegate(NULL);
FScriptDelegate FunDelTwo;
FunDelTwo.BindUFunction(SrcObj, FName("UFunTwo"));
// 匿名结构体
struct
{
FString InfoStr;
int32 Count;
} FunTwoParam;
FunTwoParam.InfoStr = FString("ASrcActor --> UFunTwo");
FunTwoParam.Count = 567;
FunDelTwo.ProcessDelegate<UObject>(&FunTwoParam);
FWHelper::Debug(FString::FromInt(FunTwoParam.Count));
}
编译后运行游戏,左上角正确输出了 DecActor 里 RunUFunOne()
提供的参数 567;后续再输出 int32
类型的 Count 时则显示 980,说明通过引用修改成功了。
在笔者的 4.26 版本,TBaseDelegate 已经准备弃用了,取而代之的是 TDelegate:
// 旧版
TBaseDelegate<ReturnType, ArgTypes...>
// 新版
TDelegate<ReturnType(ArgTypes...)>
与 FScriptDelegate 不同,TBaseDelegate/TDelegate 则支持返回值。
在 SrcActor 声明并定义一个方法,有两个形参并且有返回值。
SrcActor.h
public:
UFUNCTION()
bool UFunThree(FString InfoStr, int32 Count);
SrcActor.cpp
bool ASrcActor::UFunThree(FString InfoStr, int32 Count)
{
FWHelper::Debug(InfoStr + FString(" --> ") + FString::FromInt(Count));
return true;
}
声明一个方法,通过 TBaseDelegate/TDelegate 绑定 UFunction 方法并执行。
DecActor.h
public:
// 调用 UFunction 方法二:TBaseDelegate/TDelegate
void RunUFunTwo();
DecActor.cpp
void ADecActor::BeginPlay()
{
Super::BeginPlay();
if (ActArray.Num() > 0) {
SrcAct = Cast<ASrcActor>(ActArray[0]);
SrcObj = (UObject*)SrcAct;
//RunUFunOne();
// 调用 UFunction 方法二:TBaseDelegate
RunUFunTwo();
}
}
void ADecActor::RunUFunTwo()
{
// 旧版
//TBaseDelegate FunDelThree = TBaseDelegate::CreateUFunction(SrcObj, "UFunThree");
// 新版
TDelegate<bool(FString, int32)> FunDelThree = TDelegate<bool(FString, int32)>::CreateUFunction(SrcObj, "UFunThree");
bool DelResult = FunDelThree.Execute(FString("ASrcActor --> UFunThree"), 789);
if (DelResult) FWHelper::Debug("Return True");
}
编译后运行游戏,可见左上角输出了 DecActor 的 RunUFunTwo()
提供的参数和 Debug 语句。
不过 TBaseDelegate 的缺点是不能传引用,否则会传出一个错误地址。
而 TDelegate 则会不会传出错误地址,但方法里修改传过来的 int32
类型的引用时,不会影响到引用原本的值。笔者水平有限,如果有读者明白是什么原因的话,烦请指教 : ) 不过从此也可以知道它们两个确实是不能够传引用的。
梁迪老师要教的框架也就是基于 UFunction 来获取方法的,上面两种方法的缺点它都没有。
在 SrcActor 声明带返回值和引用参数的一个方法。
SrcActor.h
public:
UFUNCTION()
int32 UFunFour(FString InfoStr, int32& Count);
SrcActor.cpp
int32 ASrcActor::UFunFour(FString InfoStr, int32& Count)
{
FWHelper::Debug(InfoStr + FString(" --> ") + FString::FromInt(Count));
Count = 321;
return 769;
}
步骤其实就跟方法一差不多,不过获取返回值则有所区别。
DecActor.h
public:
// 调用 UFunction 方法三:UFunction
void RunUFunThree();
DecActor.cpp
void ADecActor::BeginPlay()
{
Super::BeginPlay();
if (ActArray.Num() > 0) {
SrcAct = Cast<ASrcActor>(ActArray[0]);
SrcObj = (UObject*)SrcAct;
//RunUFunTwo();
// 调用 UFunction 方法三:UFunction
// 测试完后注释掉该语句
RunUFunThree();
}
}
void ADecActor::RunUFunThree()
{
UFunction* FunFour = SrcObj->FindFunction(FName("UFunFour"));
if (FunFour) {
struct
{
FString InfoStr;
int32 Count;
} FunFourParam;
FunFourParam.InfoStr = FString("ASrcActor --> UFunFour");
FunFourParam.Count = 675;
SrcObj->ProcessEvent(FunFour, &FunFourParam);
uint8* RetValPtr = (uint8*)&FunFourParam + FunFour->ReturnValueOffset; // 通过地址偏移获取返回值的地址
int32* RetVal = (int32*)RetValPtr; // 通过地址获取值
FWHelper::Debug(FString("Return Value --> ") + FString::FromInt(*RetVal));
FWHelper::Debug(FString("Count Value --> ") + FString::FromInt(FunFourParam.Count));
}
}
编译后运行游戏,可见左上角正确输出了 ASrcActor 的 UFunFour()
方法里提供的参数和 Debug 语句,不仅输出了 Count 改变前的值、返回值,也输出了 Count 改变后的值。
如果想通过 UFunction 传指针的话,就只能传以 UObject 为基类的对象的指针。
UE4 的资源加载须知:
资源对象已经在内存中的话,一般直接从内存中提取。如 FindObject()
资源对象不在内存中的话,则进行加载;如果加载路径不存在,则设为 null。如 LoadObject()
在 Public/Wealth 路径下创建以下 C++ 类:
创建两个 Actor 类,分别叫 WealthActor 和 HandleActor。
创建一个 Object 类,命名为 HandleObject。
创建两个 User Widget 类,分别叫 WealthWidget 和 HandleWidget。
来到 WealthActor。声明一个网格体组件,以及声明一个方法来给这个网格体组件来注入模型资源。
WealthActor.h
// 提前声明
class UStaticMeshComponent;
UCLASS()
class FRAMECOURSE_API AWealthActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
UStaticMeshComponent* WorkMesh;
protected:
// 资源在 UE4 的状态,FindObject 和 LoadObject
void WealthState();
};
WealthActor.cpp
// 引入头文件
#include "Common/FWCommon.h"
AWealthActor::AWealthActor()
{
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
WorkMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WorkMesh"));
WorkMesh->SetupAttachment(RootComponent);
}
void AWealthActor::BeginPlay()
{
Super::BeginPlay();
WealthState();
}
void AWealthActor::WealthState()
{
UStaticMesh* BlockMesh = FindObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Game/Resource/SCTanks/Meshes/SK_SCT_Block.SK_SCT_Block'"));
if (!BlockMesh) FWHelper::Debug("FindObject BlockMesh Failed");
FWHelper::Debug("Loading Object BlockMesh");
LoadObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Game/Resource/SCTanks/Meshes/SK_SCT_Block.SK_SCT_Block'"));
BlockMesh = FindObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Game/Resource/SCTanks/Meshes/SK_SCT_Block.SK_SCT_Block'"));
WorkMesh->SetStaticMesh(BlockMesh);
}
在 Blueprint 目录下创建 Wealth 文件夹,基于 WealthActor 创建一个蓝图,取名 WealthActor_BP。随后把它放入到场景,并把关卡的 WorldSettings 的 GameMode 里的 DefaultPawnClass 改成 DefaultPawn,以便运行游戏后可以自由移动。
运行游戏,可以看见左上角显示 “FindObject BlockMesh Failed” ,并且场景内生成了一个模型。说明我们在资源没有在内存里的时候是无法通过 FindObject()
获取到的,要先将资源通过 LoadObject()
加载到内存中。
可以看到,如果每次都要填写完整的资源路径才能获取到资源,未免太麻烦了。所以一般不会通过这种方法来加载资源。
我们可以声明一个变量保存资源地址,将它暴露给蓝图;或者是通过一个资源连接表来加载资源。
DataAsset 是 UE4 数据的基类,它本身是 UObject 的子类,配合 Asset Manager 管理资源。
AssetData 是一种结构体,提供资源交互结构。
下面是梁迪老师的文档里面的内容,这里就直接贴出来好了。
实际上 FStringAssetReference 在 4.18 已经是旧的一个路径类型了,现在它已经被 FSoftObjectPath 取代。详情见这篇文章:
【UE4】加载资源的方式(八)关于资产引用的各种Path和Ptr介绍
不过笔者为了贴合课程内容,依旧会按照老师的代码继续编写,读者在实际开发中应该尽量使用新版的代码逻辑。
接下来来到 WealthActor。先创建一个结构体 FWealthNode
用于存放路径和资源的名字。
然后创建一个继承于 UDataAsset 的数据类,里面存放 FWealthNode
的指针数组以及一个散图资源的指针数组。
在本体声明一个可被蓝图读写的数据类的指针,存放上述数据。然后声明一个方法,利用路径来加载模型资源到网格体组件。
由于等下打算添加多个资源路径,所以我们再声明一个计时器来轮流切换模型资源到网格体;再声明一个 Index 来记录当前的模型是第几个。
WealthActor.h
#include "Engine/DataAsset.h" // 引入头文件
#include "WealthActor.generated.h"
// 提前声明
class UTexture2D;
USTRUCT()
struct FWealthNode
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
FName WealthName;
UPROPERTY(EditAnywhere)
FStringAssetReference WealthPath;
};
UCLASS()
class FRAMECOURSE_API UWealthData : public UDataAsset // 继承数据类
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
TArray<FWealthNode> WealthGroup;
UPROPERTY(EditAnywhere)
TArray<UTexture2D*> WealthTexture;
};
UCLASS()
class FRAMECOURSE_API AWealthActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
UWealthData* WealthData;
protected:
// 循环更新模型方法
void UpdateMesh();
protected:
int32 MeshIndex;
FTimerHandle UpdateMeshHandle;
};
WealthActor.cpp
// 引入头文件
#include "TimerManager.h"
void AWealthActor::BeginPlay()
{
Super::BeginPlay();
//WealthState();
// 启动更新模型函数
FTimerDelegate UpdateMeshDele = FTimerDelegate::CreateUObject(this, &AWealthActor::UpdateMesh);
GetWorld()->GetTimerManager().SetTimer(UpdateMeshHandle, UpdateMeshDele, 1.f, true);
}
void AWealthActor::UpdateMesh()
{
if (WealthData && WealthData->WealthGroup.Num() > 0) {
UStaticMesh* FactMesh = LoadObject<UStaticMesh>(NULL, *WealthData->WealthGroup[MeshIndex].WealthPath.ToString());
WorkMesh->SetStaticMesh(FactMesh);
// 更新数据
MeshIndex = (MeshIndex + 1) >= WealthData->WealthGroup.Num() ? 0 : (MeshIndex + 1);
}
}
在 Blueprint/Wealth 文件夹内创建一个 Data Asset,以 WealthData 为基类,取名 WealthData_BP。打开它并给结构体数组添加元素如下:
打开 WealthActor_BP,给它的 WealthData 指针设置为 WealthData_BP。运行游戏,可以看见场景内原本放置 WealthActor_BP 的位置生成一个模型,并且每秒切换一次模型。
UObjectLibrary 是 UE4 提供的一个便于从某个文件夹下加载某种资源的一个类。
来到 WealthActor,声明 UObjectLibrary 变量和一个存储散图路径的数组;再声明一个方法来加载资源。
WealthActor.h
// 提前声明
class UObjectLibrary;
UCLASS()
class FRAMECOURSE_API AWealthActor : public AActor
{
GENERATED_BODY()
protected:
// UObjectLibrary 操作
void ObjectLibraryOperate();
protected:
UObjectLibrary* ObjectLibrary;
TArray<FSoftObjectPath> TexturePath;
};
WealthActor.cpp
// 引入头文件
#include "Engine/ObjectLibrary.h"
void AWealthActor::BeginPlay()
{
Super::BeginPlay();
//WealthState();
ObjectLibraryOperate();
/*
FTimerDelegate UpdateMeshDele = FTimerDelegate::CreateUObject(this, &AWealthActor::UpdateMesh);
GetWorld()->GetTimerManager().SetTimer(UpdateMeshHandle, UpdateMeshDele, 1.f, true);
*/
}
void AWealthActor::ObjectLibraryOperate()
{
if (!ObjectLibrary) {
ObjectLibrary = UObjectLibrary::CreateLibrary(UObject::StaticClass(), false, false);
ObjectLibrary->AddToRoot(); // 防止被垃圾回收
}
// 搜索目标文件夹下所有 Texture 的路径
ObjectLibrary->LoadAssetDataFromPath(TEXT("/Game/Resource/UI/Texture/MenuTex"));
TArray<FAssetData> TextureData;
ObjectLibrary->GetAssetDataList(TextureData);
for (int32 i = 0; i < TextureData.Num(); ++i) {
// 测试完了把下面的 Debug 语句注释掉
FWHelper::Debug(TextureData[i].ToSoftObjectPath().ToString(), 200.f);
TexturePath.AddUnique(TextureData[i].ToSoftObjectPath());
}
}
运行游戏,可以看到左侧输出了 /Resource/UI/Texture/MenuTex 目录下所有资源的路径。
尽管可以获取路径,但是直接把这些资源从硬盘里读到内存是非常消耗性能的,而异步加载可以缓解这个问题。
关于资源加载的知识点,此处推荐另一位博主的专栏:《UE 资源加载》
该专栏的 4 篇文章讲解了资源加载的完整流程。不过专栏涉及了源码的逻辑,所以难度较大,初学者仅需稍作了解即可。
在 Blueprint/Wealth 目录下创建一个 Widget Blueprint,取名叫 WealthWidget_BP。打开它并且指定其父类为 WealthWidget。然后调整布局如下:
接下来我们打算让 WealthActor 来加载散图资源然后显示到 WealthWidget_BP 上。
首先来到 WealthWidget,声明一个 Image 控件的指针。再声明两个方法,一个用来初始化,另一个用来配置图片资源。
WealthWidget.h
// 提前声明
class UImage;
UCLASS()
class FRAMECOURSE_API UWealthWidget : public UUserWidget
{
GENERATED_BODY()
public:
// Widget 的初始化函数
virtual bool Initialize() override;
void AssignTexture(UTexture2D* InTexture);
public:
UPROPERTY(Meta = (BindWidget))
UImage* TexImage;
};
WealthWidget.cpp
// 引入头文件
#include "Wealth/WealthActor.h"
#include "Kismet/GameplayStatics.h"
#include "Components/Image.h"
bool UWealthWidget::Initialize()
{
// 这里一定要判断一下父类
if (!Super::Initialize()) return false;
TArray<AActor*> ActArray;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AWealthActor::StaticClass(), ActArray);
if (ActArray.Num() > 0) {
AWealthActor* WealthActor = Cast<AWealthActor>(ActArray[0]);
WealthActor->AssignWealthWidget(this); // 等下添加相关代码到 WealthActor,这里是为了指定 WealthActor 的目标界面为当前类
}
return true;
}
void UWealthWidget::AssignTexture(UTexture2D* InTexture)
{
TexImage->SetBrushFromTexture(InTexture);
}
随后来到 WealthActor,我们依旧是打算让图片资源轮流显示在界面上,所以要声明一个计时器和散图数组来轮流切换图片。
余下的方法就是异步加载资源并更新界面的 Image 控件的图片。
WealthActor.h
// 提前声明
class UWealthWidget;
struct FStreamableManager;
struct FStreamableHandle;
UCLASS()
class FRAMECOURSE_API AWealthActor : public AActor
{
GENERATED_BODY()
public:
void AssignWealthWidget(UWealthWidget* InWidget);
protected:
// 循环更新贴图方法
void UpdateTexture();
// 资源异步加载
void StreamableManagerOperate();
// 资源异步加载完成
void StreamableManagerLoadComplete();
protected:
UWealthWidget* WealthWidget;
FStreamableManager* WealthLoader;
TSharedPtr<FStreamableHandle> WealthHandle;
TArray<UTexture2D*> TextureGroup;
int32 TextureIndex;
FTimerHandle UpdateTextureHandle;
};
WealthActor.cpp
// 引入头文件
#include "Wealth/WealthWidget.h"
#include "Engine/StreamableManager.h"
void AWealthActor::AssignWealthWidget(UWealthWidget* InWidget)
{
WealthWidget = InWidget;
}
void AWealthActor::BeginPlay()
{
Super::BeginPlay();
//WealthState();
ObjectLibraryOperate();
// 调用异步加载
StreamableManagerOperate();
}
void AWealthActor::StreamableManagerOperate()
{
// 创建加载管理器
WealthLoader = new FStreamableManager();
// 执行异步加载,添加资源链接数组和加载完成回调数组
WealthHandle = WealthLoader->RequestAsyncLoad(TexturePath, FStreamableDelegate::CreateUObject(this, &AWealthActor::StreamableManagerLoadComplete));
}
void AWealthActor::StreamableManagerLoadComplete()
{
// 加载完成后动态修改图片
TArray<UObject*> OutObjects;
WealthHandle->GetLoadedAssets(OutObjects);
for (int32 i = 0; i < OutObjects.Num(); ++i) {
UTexture2D* WorkTexture = Cast<UTexture2D>(OutObjects[i]);
if (WorkTexture) TextureGroup.Add(WorkTexture);
}
FTimerDelegate UpdateTextureDele = FTimerDelegate::CreateUObject(this, &AWealthActor::UpdateTexture);
GetWorld()->GetTimerManager().SetTimer(UpdateTextureHandle, UpdateTextureDele, 0.5f, true);
}
void AWealthActor::UpdateTexture()
{
if (!WealthWidget) return;
WealthWidget->AssignTexture(TextureGroup[TextureIndex]);
TextureIndex = (TextureIndex + 1) >= TextureGroup.Num() ? 0 : (TextureIndex + 1);
}
编译后打开 FWHUDWidget_BP,将 WealthWidget_BP 添加到界面。
运行游戏,可以看见稍等片刻后 TexImage 的内容每隔 0.5 秒变一次,前面这个等待的时间就是资源加载进图片存储数组的时间。
异步加载适合这种加载大量资源的场合,如果这里使用同步加载的话,可能就会卡顿一段时间。
在梁迪老师准备的文档里还有另外的异步加载方式,读者有兴趣的话可以继续研究。
老师的原意是让 UBlueprint(所有蓝图的基类) 作为 UObject 转化成 UClass 的中介来生成对应的蓝图对象,不过这个方法其实是有问题的,正确的方法会在下一节课讲解,以下内容仅供参考。
打算让 HandleObject 作为一个可以生成到场景的对象,所以给它添加宏的说明符。
HandleObject.h
UCLASS(Blueprintable, BlueprintType) // 加入说明符,以便可以生成蓝图对象
class FRAMECOURSE_API UHandleObject : public UObject
{
GENERATED_BODY()
};
编译后,在 Blueprint/Wealth 目录下新建 HandleObject 和 HandleActor 的蓝图,分别命名为 HandleObject_BP 和 HandleActor_BP。再创建一个 Widget Blueprint,命名为 HandleWidget_BP。
打开 HandleWidget_BP,将其父类改成 HandleWidget。随后更改界面布局如下:(注意 Canvas Panel 改成了 Overlay)
打开 HandleActor_BP,给其添加一个网格体组件,然后赋予一个树的模型。
再打开 WealthWidget_BP,作如下更改,以便让其显示在界面上(注意 “ContentBox” 是 Overlay 控件改名而来):
来到 WealthWidget,声明一个 Overlay 控件的指针,然后添加一个方法用于添加子控件到 Overlay。
WealthWidget.h
// 提前声明
class UOverlay;
UCLASS()
class FRAMECOURSE_API UWealthWidget : public UUserWidget
{
GENERATED_BODY()
public:
void AssignContent(UUserWidget* InWidget);
public:
UPROPERTY(Meta = (BindWidget))
UOverlay* ContentBox;
};
WealthWidget.h
// 引入头文件
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
void UWealthWidget::AssignContent(UUserWidget* InWidget)
{
// OverlaySlot 是一种控件的插槽类型,里面可以放其他的控件
UOverlaySlot* ContentSlot = ContentBox->AddChildToOverlay(InWidget);
ContentSlot->SetPadding(FMargin(0.f, 0.f, 0.f, 0.f));
ContentSlot->SetHorizontalAlignment(HAlign_Fill);
ContentSlot->SetVerticalAlignment(VAlign_Fill);
}
来到 WealthActor,声明三个路径变量,分别是 Object、Actor、Widget 的蓝图对象的路径。然后声明一个方法把这些蓝图对象都加载出来。
WealthActor.h
public:
UPROPERTY(EditAnywhere)
FStringAssetReference HandleObjectPath;
UPROPERTY(EditAnywhere)
FStringAssetReference HandleActorPath;
UPROPERTY(EditAnywhere)
FStringAssetReference HandleWidgetPath;
protected:
// 三种对象的关系与转换
void UObjectUClassUBlueprint();
WealthActor.cpp
void AWealthActor::AssignWealthWidget(UWealthWidget* InWidget)
{
WealthWidget = InWidget;
// 在这调用
UObjectUClassUBlueprint();
}
void AWealthActor::UObjectUClassUBlueprint()
{
// 载入路径到内存
LoadObject<UObject>(NULL, *HandleObjectPath.GetAssetPathString());
LoadObject<AActor>(NULL, *HandleActorPath.GetAssetPathString());
LoadObject<UUserWidget>(NULL, *HandleWidgetPath.GetAssetPathString());
// 获取路径指向的 UObject 资源
UObject* HandleObject = HandleObjectPath.ResolveObject();
UObject* HandleActor = HandleActorPath.ResolveObject();
UObject* HandleWidget = HandleWidgetPath.ResolveObject();
if (HandleObject) FWHelper::Debug("Get HandleObject");
if (HandleActor) FWHelper::Debug("Get HandleActor");
if (HandleWidget) FWHelper::Debug("Get HandleWidget");
// 直接这样 StaticClass() 创建实例是会报错的
//UObject* InstObject = NewObject(this, HandleObject->StaticClass());
//AActor* InstActor = GetWorld()->SpawnActor(HandleActor->StaticClass(), GetActorTransform());
//UUserWidget* InstWidget = CreateWidget(GetWorld(), HandleWidget->StaticClass());
// 转化为 UBlueprint
UBlueprint* BlueObject = Cast<UBlueprint>(HandleObject);
UBlueprint* BlueActor = Cast<UBlueprint>(HandleActor);
UBlueprint* BlueWidget = Cast<UBlueprint>(HandleWidget);
if (BlueObject) FWHelper::Debug("Get BlueObject");
if (BlueActor) FWHelper::Debug("Get BlueActor");
if (BlueWidget) FWHelper::Debug("Get BlueWidget");
// 再从 UBlueprint 生成为对应的 UClass 类型的实例
UObject* InstObject = NewObject<UObject>(this, BlueObject->GeneratedClass);
AActor* InstActor = GetWorld()->SpawnActor<AActor>(BlueActor->GeneratedClass, GetActorTransform());
// 笔者运行此代码失败,此处需要显式转换才能用于生成界面
//UUserWidget* InstWidget = CreateWidget(GetWorld(), BlueWidget->GeneratedClass);
TSubclassOf<UUserWidget> AgentWidget = BlueWidget->GeneratedClass;
UUserWidget* InstWidget = CreateWidget<UUserWidget>(GetWorld(), AgentWidget);
if (InstObject) FWHelper::Debug("Create InstObject");
if (InstActor) FWHelper::Debug("Create InstActor");
if (InstWidget) FWHelper::Debug("Create InstWidget");
if (InstWidget) WealthWidget->AssignContent(InstWidget);
}
编译后,打开 WealthActor_BP,配置数据如图:
如果左上角所有的 Debug 语句(一共 9 句)都输出了,并且界面有刚刚放置的 HandleWidget_BP 界面、场景生成了 HandleActor_BP 的树,则说明没有问题。
然而,上面 AWealthActor::UObjectUClassUBlueprint()
的代码只能在编辑器模式下正常运行,将项目打包成可执行文件后这里会报错,正确的方法将会在下一节课程讲解。