UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)

  • 7. 反射应用详解
    • 通过反射获取 UObject
    • 通过反射来获取 UENUM
    • 通过反射获取蓝图对象并生成到场景
    • 通过反射获取 UProperty 并修改
    • 通过反射获取 UFunction 并执行
      • 方法一:FScriptDelegate
      • 方法二:TBaseDelegate (旧) / TDelegate (新)
      • 方法三:UFunction
  • 8. 资源同异步加载
    • 通过 DataAsset 加载资源
    • 通过 UObjectLibrary 加载资源
    • 通过 FStreamableManager 异步加载资源
    • 通过路径获取继承 C++ 的蓝图对象并加入到场景(弃用)

7. 反射应用详解

笔者在 《UE4开发C++沙盒游戏教程笔记(一)》中已经推荐过文章讲述 UE4 反射功能的概要,此处就推荐这篇更加深入的文章:

《UE4反射机制》 (实现原理部分稍有难度)

那如何通过类的名称字符串来生成类的对象呢?有以下几个通过反射获取实例的方法:

  1. StaticLoadClass、StaticLoadObject
  2. LoadClass、LoadObject
  3. FObjectFinder、FClassFinder
  4. FindObject、FindClass

所有的基于 UObject 创建的类有两种存在形式,一种是已经加载到 UE4 内存里的,一种是存储在硬盘里的。通过上面的方法,我们可以将已存在内存中的资源加载出来;存储在硬盘里的则加载到内存后再加载出来使用。

UClass 也是一个基于 UObject 的类,不过它负责存储 UObject 的反射数据(元数据,即描述数据属性的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能)。它是 UE4 反射系统的重要组成部分,二者可以通过这两个方法相互获取到对方的实例:
UObject::StaticClass()UClass::GetDefaultObject()

通过反射获取 UObject

在 Public/Reflect 路径下创建以下 C++ 文件:

新建一个 Object 类,命名为 RefObj,作为需要获取并生成的 UObject。

新建两个 Actor 类,分别命名为 SrcActorDecActor,分别作为被获取方法和变量的 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() 里面的内容都正常输出了。

通过反射来获取 UENUM

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 并把它生成到场景。

打开蓝图,给它添加一个网格体组件,并随便添加一个模型资源。

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第1张图片
在内容管理器选中这个蓝图,按 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 的位置出现了刚刚设置的网格体模型。

通过反射获取 UProperty 并修改

在 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 变量成功了。

通过反射获取 UFunction 并执行

方法一:FScriptDelegate

来到 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,说明通过引用修改成功了。

方法二:TBaseDelegate (旧) / TDelegate (新)

在笔者的 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

梁迪老师要教的框架也就是基于 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 为基类的对象的指针。

8. 资源同异步加载

UE4 的资源加载须知:

资源对象已经在内存中的话,一般直接从内存中提取。如 FindObject()
资源对象不在内存中的话,则进行加载;如果加载路径不存在,则设为 null。如 LoadObject()

在 Public/Wealth 路径下创建以下 C++ 类:

创建两个 Actor 类,分别叫 WealthActorHandleActor

创建一个 Object 类,命名为 HandleObject

创建两个 User Widget 类,分别叫 WealthWidgetHandleWidget

来到 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 加载资源

可以看到,如果每次都要填写完整的资源路径才能获取到资源,未免太麻烦了。所以一般不会通过这种方法来加载资源。

我们可以声明一个变量保存资源地址,将它暴露给蓝图;或者是通过一个资源连接表来加载资源。

DataAsset 是 UE4 数据的基类,它本身是 UObject 的子类,配合 Asset Manager 管理资源。

AssetData 是一种结构体,提供资源交互结构。

下面是梁迪老师的文档里面的内容,这里就直接贴出来好了。

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第2张图片

实际上 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。打开它并给结构体数组添加元素如下:

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第3张图片
打开 WealthActor_BP,给它的 WealthData 指针设置为 WealthData_BP。运行游戏,可以看见场景内原本放置 WealthActor_BP 的位置生成一个模型,并且每秒切换一次模型。

通过 UObjectLibrary 加载资源

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 篇文章讲解了资源加载的完整流程。不过专栏涉及了源码的逻辑,所以难度较大,初学者仅需稍作了解即可。

通过 FStreamableManager 异步加载资源

在 Blueprint/Wealth 目录下创建一个 Widget Blueprint,取名叫 WealthWidget_BP。打开它并且指定其父类为 WealthWidget。然后调整布局如下:

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第4张图片
接下来我们打算让 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 添加到界面。

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第5张图片

运行游戏,可以看见稍等片刻后 TexImage 的内容每隔 0.5 秒变一次,前面这个等待的时间就是资源加载进图片存储数组的时间。

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第6张图片

异步加载适合这种加载大量资源的场合,如果这里使用同步加载的话,可能就会卡顿一段时间。

在梁迪老师准备的文档里还有另外的异步加载方式,读者有兴趣的话可以继续研究。

通过路径获取继承 C++ 的蓝图对象并加入到场景(弃用)

老师的原意是让 UBlueprint(所有蓝图的基类) 作为 UObject 转化成 UClass 的中介来生成对应的蓝图对象,不过这个方法其实是有问题的,正确的方法会在下一节课讲解,以下内容仅供参考。

打算让 HandleObject 作为一个可以生成到场景的对象,所以给它添加宏的说明符。

HandleObject.h

UCLASS(Blueprintable, BlueprintType)	// 加入说明符,以便可以生成蓝图对象
class FRAMECOURSE_API UHandleObject : public UObject
{
	GENERATED_BODY()
	
};

编译后,在 Blueprint/Wealth 目录下新建 HandleObject 和 HandleActor 的蓝图,分别命名为 HandleObject_BPHandleActor_BP。再创建一个 Widget Blueprint,命名为 HandleWidget_BP

打开 HandleWidget_BP,将其父类改成 HandleWidget。随后更改界面布局如下:(注意 Canvas Panel 改成了 Overlay)

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第7张图片
打开 HandleActor_BP,给其添加一个网格体组件,然后赋予一个树的模型。

再打开 WealthWidget_BP,作如下更改,以便让其显示在界面上(注意 “ContentBox” 是 Overlay 控件改名而来):

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第8张图片

来到 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,配置数据如图:

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第9张图片
如果左上角所有的 Debug 语句(一共 9 句)都输出了,并且界面有刚刚放置的 HandleWidget_BP 界面、场景生成了 HandleActor_BP 的树,则说明没有问题。

UE4运用C++和框架开发坦克大战教程笔记(三)(第7~8集)_第10张图片

然而,上面 AWealthActor::UObjectUClassUBlueprint() 的代码只能在编辑器模式下正常运行,将项目打包成可执行文件后这里会报错,正确的方法将会在下一节课程讲解。

你可能感兴趣的:(UE4/5,的学习笔记,ue4,c++,笔记)