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

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

  • 40. 多按键绑定
  • 41. 自动生成对象
  • 42. 资源模块数据结构
    • 测试自动生成对象
    • 按资源类型生成对象

40. 多按键绑定

上节课实现了按键绑定系统的 4 种基础绑定,这节课来实现多按键事件的绑定。

我们为多按键绑定额外编写一个类 InputBinder

DDMessage.h

// 多按键输入绑定类
#pragma region InputBinder

DECLARE_DELEGATE(FDDInputEvent)		// 用于绑定多按键的目标方法

UCLASS()
class DATADRIVEN_API UDDInputBinder : public UObject
{
	GENERATED_BODY()

public:

	UDDInputBinder();

	void PressEvent();		// 按下事件(多按键的每个按键都要绑定它)

	void ReleaseEvent();	// 松开事件(同上)
	
public:

	uint8 InputCount;	// 目前正在响应的按键数量

	uint8 TotalCount;	// 触发事件所需要响应的按键数量

	// 游戏暂停时是否执行按键事件
	uint8 bExecuteWhenPause;

	FDDInputEvent InputDele;	// 委托句柄

public:

	// 模板初始化方法
	template<class UserClass>
	void InitBinder(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, uint8 InCount)
	{
		TotalCount = InCount;
		InputDele.BindUObject(UserObj, InMethod);
	}
};

#pragma endregion


UCLASS()
class DATADRIVEN_API UDDMessage : public UObject, public IDDMM
{
	GENERATED_BODY()
	
public:

	// 绑定多个按键
	template<class UserClass>
	UDDInputBinder& BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName);

	// 解绑对象的所有按键事件
	void UnBindInput(FName ObjectName);

protected:

	// 绑定按键事件序列,键是按键事件名,值是按键事件的数组
	TMap<FName, TArray<UDDInputBinder*>> BinderGroup;
};

template<class UserClass>
UDDInputBinder& UDDMessage::BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName)
{
	UDDInputBinder* InputBinder = NewObject<UDDInputBinder>();
	InputBinder->InitBinder(UserObj, InMethod, KeyGroup.Num());
	InputBinder->AddToRoot();	// 避免被 GC
	// 给所有目标按钮绑定按下和抬起的方法
	for (int i = 0; i < KeyGroup.Num(); ++i) {
		PlayerController->InputComponent->BindKey(KeyGroup[i], IE_Pressed, InputBinder, &UDDInputBinder::PressEvent).bExecuteWhenPaused = true;
		PlayerController->InputComponent->BindKey(KeyGroup[i], IE_Released, InputBinder, &UDDInputBinder::ReleaseEvent).bExecuteWhenPaused = true;
	}
	if (!BinderGroup.Contains(ObjectName)) {
		TArray<UDDInputBinder*> BinderList;
		BinderGroup.Add(ObjectName, BinderList);
	}
	BinderGroup.Find(ObjectName)->Push(InputBinder);
	return *InputBinder;
}

DDMessage.cpp

UDDInputBinder::UDDInputBinder()
{
	InputCount = 0;
	bExecuteWhenPause = false;
}

void UDDInputBinder::PressEvent()
{
	InputCount++;
	// 如果 InputCount 与 TotalCount 相等,说明所有按键都按下了
	if (InputCount == TotalCount) {
		// 如果允许在暂停时执行
		if (bExecuteWhenPause)
			InputDele.ExecuteIfBound();
		else if (!bExecuteWhenPause && !UDDCommon::Get()->IsPauseGame())
			InputDele.ExecuteIfBound();
	}
}

void UDDInputBinder::ReleaseEvent()
{
	InputCount--;
}

void UDDMessage::UnBindInput(FName ObjectName)
{
	if (!BinderGroup.Contains(ObjectName))
		return;
	TArray<UDDInputBinder*> BinderList = *BinderGroup.Find(ObjectName);
	for (int i = 0; i < BinderList.Num(); ++i) {
		BinderList[i]->RemoveFromRoot();	// 移出 Root 以便被 GC
		BinderList[i]->ConditionalBeginDestroy();	// 申请销毁
	}
	BinderGroup.Remove(ObjectName);
}

依旧是部署好 DDMessage – DDModule – DDOO – 对象 这条调用链。

DDModule.h

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DATADRIVEN_API UDDModule : public USceneComponent
{
	GENERATED_BODY()

public:

	// 绑定多个按键
	template<class UserClass>
	UDDInputBinder& BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName);

	// 解绑对象的所有按键事件
	void UnBindInput(FName ObjectName);
};

template<class UserClass>
UDDInputBinder& UDDModule::BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName)
{
	return Message->BindInput(UserObj, InMethod, KeyGroup, ObjectName);
}

DDModule.cpp

void UDDModule::UnBindInput(FName ObjectName)
{
	Message->UnBindInput(ObjectName);
}

DDOO.h

class DATADRIVEN_API IDDOO
{
	GENERATED_BODY()

protected:

	// 给之前的绑定方法的名字都添加 DD 前缀,避免与 UE4 原生的方法名重复
	template<class UserClass>
	FInputAxisBinding& DDBindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName);

	template<class UserClass>
	FInputTouchBinding& DDBindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent);

	template<class UserClass>
	FInputActionBinding& DDBindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent);

	template<class UserClass>
	FInputKeyBinding& DDBindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent);


	// 绑定 2 个按键(注意绑定多个按键时传入的形参不一样了)
	template<class UserClass>
	UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II);

	// 绑定 3 个按键,下面的也差不多,就是多传了一两个按键形参
	template<class UserClass>
	UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III);

	template<class UserClass>
	UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV);

	template<class UserClass>
	UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV, FKey Key_V);

	// 解绑该对象的所有多个按键
	void UnBindInput();
};


// 模板方法的实现里,名字也同步添加前缀
template<class UserClass>
FInputAxisBinding& IDDOO::DDBindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName)
{
	return IModule->BindAxis(UserObj, InMethod, AxisName);
}

template<class UserClass>
FInputTouchBinding& IDDOO::DDBindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent)
{
	return IModule->BindTouch(UserObj, InMethod, KeyEvent);
}

template<class UserClass>
FInputActionBinding& IDDOO::DDBindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent)
{
	return IModule->BindAction(UserObj, InMethod, ActionName, KeyEvent);
}

template<class UserClass>
FInputKeyBinding& IDDOO::DDBindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent)
{
	// 绑定单个按钮和多按钮时,调用的 BindInput() 不一样
	return IModule->BindInput(UserObj, InMethod, Key, KeyEvent);
}

// 下面的方法都是绑定多按键的模板方法
template<class UserClass>
UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II)
{
	TArray<FKey> KeyGroup;
	KeyGroup.Push(Key_I);
	KeyGroup.Push(Key_II);
	// 从传入的参数可以看出来
	return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName());
}

template<class UserClass>
UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III)
{
	TArray<FKey> KeyGroup;
	KeyGroup.Push(Key_I);
	KeyGroup.Push(Key_II);
	KeyGroup.Push(Key_III);
	return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName());
}

template<class UserClass>
UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV)
{
	TArray<FKey> KeyGroup;
	KeyGroup.Push(Key_I);
	KeyGroup.Push(Key_II);
	KeyGroup.Push(Key_III);
	KeyGroup.Push(Key_IV);
	return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName());
}

template<class UserClass>
UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV, FKey Key_V)
{
	TArray<FKey> KeyGroup;
	KeyGroup.Push(Key_I);
	KeyGroup.Push(Key_II);
	KeyGroup.Push(Key_III);
	KeyGroup.Push(Key_IV);
	KeyGroup.Push(Key_V);
	return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName());
}

为了避免出现忘记注销事件指针导致访问错误位置的情况,我们在 DDRelease() 里注销前面写的三个系统的所有事件。

DDOO.cpp

void IDDOO::DDRelease()
{
	// 注销所有协程,延时以及按键事件(也可以分别在其他地方调用)
	StopAllCoroutine();
	StopAllInvoke();
	UnBindInput();
}

void IDDOO::UnBindInput()
{
	IModule->UnBindInput(GetObjectName());
}

最后我们来验证一下多按键事件绑定是否正常运作。

CoroActor.h

protected:

	void MultiKeyEvent();

CoroActor.cpp

void ACoroActor::DDEnable()
{

	// 加前缀,但不运行这个单按键绑定
	//DDBindInput(this, &ACoroActor::BKeyEvent, EKeys::B, IE_Pressed);

	DDBindInput(this, &ACoroActor::MultiKeyEvent, EKeys::J, EKeys::K, EKeys::L);
}

void ACoroActor::MultiKeyEvent()
{
	DDH::Debug() << "MultiKeyEvent" << DDH::Endl();
}

编译运行,同时按 J、K、L 三个键,左上角才会输出 “MultiKeyEvent”。说明多按键绑定功能也编写好了。

41. 自动生成对象

下图截取自梁迪老师的 DataDriven 说明文档:

资源加载系统概述

关于资源加载方式,读者可以回顾课程的第 8 集和第 9 集,里面讲到了资源的同异步加载。

开发资源加载系统,我们首先要定义一个专用于保存资源数据的数据类,以及与之配套的结构体。目前我们先考虑生成以下 3 种类型的对象:Object、Actor 和 Widget。

DDTypes.h

#include "Engine/DataAsset.h"	// 引入头文件
#include "DDTypes.generated.h"



#pragma region Wealth

USTRUCT()	// 用这个宏说明结构体可与蓝图交互
struct FWealthItem	// 资源基类结构体
{
	GENERATED_BODY()

public:

	// 对象名
	UPROPERTY(EditAnywhere)
	FName ObjectName;

	// 类名
	UPROPERTY(EditAnywhere)
	FName ClassName;
};

USTRUCT()
struct FWealthObject : public FWealthItem	// Object 类型资源的结构体
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere)
	TSubclassOf<UObject> WealthClass;
};

USTRUCT()
struct FWealthActor : public FWealthItem	// Actor 类型资源的结构体
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere)
	TSubclassOf<AActor> WealthClass;

	UPROPERTY(EditAnywhere)
	FTransform Transform;
};

USTRUCT()
struct FWealthWidget : public FWealthItem	// Widget 类型资源的结构体
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere)
	TSubclassOf<UUserWidget> WealthClass;
};

UCLASS()
class DATADRIVEN_API UWealthData : public UDataAsset	// 继承自原生的数据类
{
	GENERATED_BODY()

public:

	// 模组名字,这个 DataAsset 下的资源生成的对象默认注册到 ModuleName 对应的模组
	// 如果为空(None)则说明该 Asset 使用于多个模组下,自动生成的对象注册到该 Asset 放置的模组下
	UPROPERTY(EditAnywhere)
	FName ModuleName;

	// 自动生成的 Object
	UPROPERTY(EditAnywhere)
	TArray<FWealthObject> AutoObjectData;
	
	// 自动生成的 Actor
	UPROPERTY(EditAnywhere)
	TArray<FWealthActor> AutoActorData;

	// 自动生成的 Widget
	UPROPERTY(EditAnywhere)
	TArray<FWealthWidget> AutoWidgetData;
};

#pragma endregion

要在 C++ 内获取到蓝图里的内容,则需要让 DDModule 拥有一个暴露给蓝图的资源组,这样就可以在编辑器内对资源组配置目标对象。

DDModule.h

public:

	// 暴露给蓝图的资源组
	UPROPERTY(EditAnywhere, Category = "DataDriven")
	TArray<UWealthData*> WealthData;

	TArray<UDDModule*> ChildrenModule;

	// 不再将 ModuleIndex 暴露给蓝图
	int32 ModuleIndex;

自动生成资源的逻辑放在 DDWealth 里,所以需要将资源组从 DDModule 赋值给 DDWealth。

DDWealth.h

public:

	// 指定资源组
	void AssignData(TArray<UWealthData*>& InWealthData);

protected:

	// 资源组
	TArray<UWealthData*> WealthData;

	// Widget 类型对象专属,保存 Widget 指针,放止被 GC
	UPROPERTY()
	TArray<UUserWidget*> GCWidgetGroup;

由于生成 Actor 对象需要获取世界,所以我们在 DDMM 里添加一个获取世界的方法。

DDMM.h

protected:

	// 获取世界
	UWorld* GetDDWorld() const;

DDMM.cpp

UWorld* IDDMM::GetDDWorld() const
{
	if (IDriver)
		return IDriver->GetWorld();
	return NULL;
}

在 DDWealth.cpp 里补全资源生成逻辑,即遍历所配置的资源组并生成里面的 3 种类型的所有对象。

DDWealth.cpp

// 引入头文件
#include "DDObject/DDOO.h"
#include "Blueprint/UserWidget.h"

void UDDWealth::WealthBeginPlay()
{
	// 遍历自动生成对象
	for (int i = 0; i < WealthData.Num(); ++i) {
		// 生成 Object 对象
		for (int j = 0; j < WealthData[i]->AutoObjectData.Num(); ++j) {
			// 根据获取到的 UClass 生成指定的对象
			UObject* NewObj = NewObject<UObject>(this, WealthData[i]->AutoObjectData[j].WealthClass);
			NewObj->AddToRoot();
			IDDOO* InstPtr = Cast<IDDOO>(NewObj);
			// 注册到框架
			if (InstPtr) {
				InstPtr->RegisterToModule(
					WealthData[i]->ModuleName.IsNone() ? IModule->GetFName() : WealthData[i]->ModuleName, 
					WealthData[i]->AutoObjectData[j].ObjectName,
					WealthData[i]->AutoObjectData[j].ClassName
				);
			}
		}
		// 生成 Actor 对象
		for (int j = 0; j < WealthData[i]->AutoActorData.Num(); ++j) {
			AActor* NewAct = GetDDWorld()->SpawnActor<AActor>(WealthData[i]->AutoActorData[j].WealthClass, WealthData[i]->AutoActorData[j].Transform);
			IDDOO* InstPtr = Cast<IDDOO>(NewAct);
			if (InstPtr) {
				InstPtr->RegisterToModule(
					WealthData[i]->ModuleName.IsNone() ? IModule->GetFName() : WealthData[i]->ModuleName, 
					WealthData[i]->AutoActorData[j].ObjectName,
					WealthData[i]->AutoActorData[j].ClassName
				);
			}
		}
		// 生成 Widget 对象
		for (int j = 0; j < WealthData[i]->AutoWidgetData.Num(); ++j) {
			UUserWidget* NewWidget = CreateWidget<UUserWidget>(GetDDWorld(), WealthData[i]->AutoWidgetData[j].WealthClass);
			// 避免回收(AddToRoot() 不适用于 Widget,即便加了也会被回收)
			GCWidgetGroup.Push(NewWidget);
			IDDOO* InstPtr = Cast<IDDOO>(NewWidget);
			if (InstPtr) {
				InstPtr->RegisterToModule(
					WealthData[i]->ModuleName.IsNone() ? IModule->GetFName() : WealthData[i]->ModuleName, 
					WealthData[i]->AutoWidgetData[j].ObjectName,
					WealthData[i]->AutoWidgetData[j].ClassName
				);
			}
		}
	}
}

void UDDWealth::AssignData(TArray<UWealthData*>& InWealthData)
{
	WealthData = InWealthData;
}

在 DDModule 的 BeginPlay() 里给 DDWealth 指定资源组。

DDModule.cpp

void UDDModule::ModuleBeginPlay()
{
	// 给 Wealth 指定资源
	Wealth->AssignData(WealthData);
	
	// ... 省略
}

编译如果没有问题的话,我们将验证部分留到下一节课。

42. 资源模块数据结构

测试自动生成对象

接下来准备一下将要生成在场景中的资源。我们打算生成资源的种类有:Object、Actor、Pawn(实际上也是 Actor)和 Widget。由于 Object 是不允许直接生成在场景中的,我们需要创建它的蓝图然后将其生成在场景中。

创建一个以 DDObject 为基类的 C++ 类,目标模组选项目,命名为 WealthCallObject,直接在默认路径创建。显示自动热重载失败,不用管,直接点 No。
创建一个以 DDActor 为基类的 C++ 类,命名为 TestWealthActor,负责调用被生成对象带有的,用于输出 Debug 语句的蓝图方法。

WealthCallObject.h

UCLASS(Blueprintable, BlueprintType)	// 添加两个说明符,以便能跟蓝图交互
class RACECARFRAME_API UWealthCallObject : public UDDObject
{
	GENERATED_BODY()
};

TestWealthActor.h

UCLASS()
class RACECARFRAME_API ATestWealthActor : public ADDActor
{
	GENERATED_BODY()
	
public:

	virtual void DDEnable() override;

protected:

	DDOBJFUNC(CallWealth);
};

TestWealthActor.cpp

void ATestWealthActor::DDEnable()
{
	Super::DDEnable();
}

编译后,在 Blueprint 文件夹内创建蓝图,它们都是待会要生成的对象:
以 DDActor 为基类创建一个蓝图,命名为 WealthCallActor
以 DDPawn 为基类创建一个蓝图,命名为 WealthCallPawn
以 DDUserWidget 为基类创建一个蓝图,命名为 WealthCallWidget
以 WealthCallObject 为基类创建一个蓝图,命名为 WealthCallObject

随后给它们添加一些方便观察是否正常生成的模型或者输出 Debug 语句的方法:(图片可能会有些糊,两个模型也可以随便选,重要的是 4 个输出的方法)

在这里插入图片描述

在 Blueprint 文件夹下创建两个 DataAsset,都以 WealthData 为基类,分别命名为 PlayerDataHUDData

然后对两个 DataAsset 设置一下内容,再配置到 GameDriver_BP 的两个模组上:

在这里插入图片描述
在 TestWealthActor.cpp 里的 DDEnable() 里补充对 4 种资源输出方法的调用。

TestWealthActor.cpp

void ATestWealthActor::DDEnable()
{
	Super::DDEnable();

	// 调用 4 个生成对象的输出方法(测试完了需要注释掉)
	CallWealth((int32)ERCGameModule::Player, "WealthCallObject", "CallObject");
	CallWealth((int32)ERCGameModule::Player, "WealthCallActor", "CallActor");
	CallWealth((int32)ERCGameModule::Player, "WealthCallPawn", "CallPawn");

	CallWealth((int32)ERCGameModule::HUD, "WealthCallWidget", "CallWidget");
}

编译后,在 Blueprint 文件夹下创建一个以 TestWealthActor 为基类的蓝图,命名为 TestWealthActor_BP。给其细节面板修改如下:

在这里插入图片描述

将它放到场景中,运行游戏,可见左上角输出了 4 条 Debug 语句,并且场景中也生成了目标 Actor 和 Pawn,界面上也正确生成了两个按钮。

在这里插入图片描述
我们来梳理下脉络:GameDriver_BP 下的 Center 模组内有 Player 模组和 HUD 模组,这两个模组内分别配置了 PlayerData 和 HUDData,里面存储了要生成的对象。两个模组将各自的 DataAsset 传给各自的 DDWealth 来执行生成逻辑。

资源对象生成后,Object、Actor、Pawn 这三个对象属于 Player 模组,Widget 属于 HUD 模组,而已经出现在场景内的 TestWealthActor_BP (属于 Center 模组)通过框架的反射事件系统调用这 4 个对象各自的蓝图 Debug 方法。由此已经可以看出梁迪老师的框架功能是比较齐全的。

按资源类型生成对象

前面截取的图片里提到,按资源类型分类也有两种情况:

在这里插入图片描述
所以我们接下来准备写通过 UObject 和 UClass 这两种方式生成资源的逻辑。

下图截取自梁迪老师的 DataDriven 文档:

在这里插入图片描述
我们先从数据结构开始写起:

DDTypes.h

#pragma region Wealth



// Object 资源结构体
USTRUCT()
struct FObjectWealthEntry
{
	GENERATED_BODY()

public:

	// 资源名
	UPROPERTY(EditAnywhere)
	FName WealthName;

	// 资源种类名
	UPROPERTY(EditAnywhere)
	FName WealthKind;

	// 资源链接
	UPROPERTY(EditAnywhere)
	FStringAssetReference WealthPath;

	// 加载出来的对象,如果有重复生成的情况就直接引用它,而不是生成多个
	UPROPERTY()
	UObject* WealthObject;
};

// UClass 类型枚举
UENUM()
enum class EWealthType : uint8 {
	Object,
	Actor,
	Widget
};

// Class 资源结构体
USTRUCT()
struct FClassWealthEntry
{
	GENERATED_BODY()

public:

	// 资源类别
	UPROPERTY(EditAnywhere)
	EWealthType WealthType;
	
	// 资源名
	UPROPERTY(EditAnywhere)
	FName WealthName;

	// 资源种类名
	UPROPERTY(EditAnywhere)
	FName WealthKind;

	// 资源链接
	UPROPERTY(EditAnywhere)
	TSoftClassPtr<UObject> WealthPtr;

	// 加载出来的对象
	UPROPERTY()
	UClass* WealthClass;
};

// 纯获取链接结构体,不进行同异步加载
USTRUCT()
struct FWealthURL
{
	GENERATED_BODY()

public:

	// 资源名
	UPROPERTY(EditAnywhere)
	FName WealthName;

	// 资源种类名
	UPROPERTY(EditAnywhere)
	FName WealthKind;

	// 资源链接
	UPROPERTY(EditAnywhere)
	FStringAssetReference WealthPath;

	// 资源链接
	UPROPERTY(EditAnywhere)
	TSoftClassPtr<UObject> WealthPtr;
};


// 声明 3 个上面的结构体的数组
UCLASS()
class DATADRIVEN_API UWealthData : public UDataAsset
{
	GENERATED_BODY()

public:

	// Object 资源链接集合
	UPROPERTY(EditAnywhere)
	TArray<FObjectWealthEntry> ObjectWealthData;

	// Class 资源链接集合
	UPROPERTY(EditAnywhere)
	TArray<FClassWealthEntry> ClassWealthData;

	// 资源链接集合
	UPROPERTY(EditAnywhere)
	TArray<FWealthURL> WealthURL;
};

#pragma endregion

剩下的逻辑留到后续课程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/283986.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【大数据面试知识点】Spark的DAGScheduler

Spark数据本地化是在哪个阶段计算首选位置的&#xff1f; 先看一下DAGScheduler的注释&#xff0c;可以看到DAGScheduler除了Stage和Task的划分外&#xff0c;还做了缓存的跟踪和首选运行位置的计算。 DAGScheduler注释&#xff1a; The high-level scheduling layer that i…

畅捷通的 Serverless 探索实践之路

作者&#xff1a;计缘&#xff0c;阿里云云原生架构师 畅捷通介绍 畅捷通是中国领先的小微企业财税及业务云服务提供商&#xff0c;成立于 2010 年。畅捷通在 2021 年中国小微企业云财税市场份额排名第一&#xff0c;在产品前瞻性及行业全覆盖方面领跑市场&#xff0c;位居中…

C:Huffman编码a

【问题描述】 给定一组字符的Huffman编码表&#xff08;从标准输入读取&#xff09;&#xff0c;以及一个用该编码表进行编码的Huffman编码文件&#xff08;存在当前目录下的in.txt中&#xff09;&#xff0c;编写程序实现对Huffman编码文件的解码&#xff0c;并按照后序遍历序…

【UE 截图】 自定义截图路径 文件名

目录 0 引言1 实践 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;UE虚幻引擎专栏&#x1f4a5; 标题&#xff1a;【UE 截图】 自定义截图路径 文件名❣️ 寄语&#xff1a;书到用时方恨少&#xff0c;事非经过不知难&#xff01;&#x1f388; 最…

zlib.decompressFile报错 【Bug已解决-鸿蒙开发】

文章目录 项目场景:问题描述原因分析:解决方案:方案1方案2此Bug解决方案总结寄语项目场景: 最近也是遇到了这个问题,看到网上也有人在询问这个问题,本文总结了自己和其他人的解决经验,解决了zlib.decompressFile报错 的问题。 问题: zlib.decompressFile报错,怎么解…

基于ssm的房屋租赁管理系统

功能介绍 房源信息模块&#xff1a; 房源信息展示、房源信息更新、房源信息增加、房源信息删除 账户管理模块&#xff1a; 账户登录、账户绑定、账户管理 租金结算模块&#xff1a; 每月租金信息、租金交付功能、月租金收入总额统计 房屋租赁合同管理模块&#xff1a; 房屋租赁…

【C语言】函数

函数是什么&#xff1f; “函数”是我们早些年在学习数学的过程中常见的概念&#xff0c;简单回顾一下&#xff1a;比如下图中&#xff0c;你给函数 f(x)2*x3 一个具体的x,这个函数通过一系列的计算来返回给你一个结果(图示如下)。 这就是数学中函数的基本过程和作用。但是你…

1.4 FMEA概述

FMEA适用场景 FMEA在三种基本情形下使用&#xff0c;每种情形都有不同的范围或重点。 情形1&#xff1a;新设计、新技术或新过程 FMEA的范围包括完整的设计、技术或过程。 情形2&#xff1a;现有设计或过程的新应用 FMEA的范围包含新环境、新场地、新应用或使用概况&#…

Servlet见解3

13 Cookie和Session http协议是一个无状态的协议&#xff0c;你每一个跳转到下一个页面的时候都是需要先登录才能使用&#xff0c;这样就很麻烦比如淘宝&#xff0c;没有cookie和session的话&#xff0c;用户在首页已经登录上去了&#xff0c;但是需要再次登录才能选择商品&am…

买对好车省钱又防坑,高性价比的买车攻略

一、教程描述 正所谓隔行如隔山&#xff0c;买车这件事情并不简单&#xff0c;买车的内幕还是有不少的&#xff0c;本套教程讲述买车攻略&#xff0c;非常适合准备买车的朋友&#xff0c;可以帮助大家买车少入坑&#xff0c;高性价比买到自己心仪的车。本套买车教程&#xff0…

Android 13 动态启用或禁用IPV6

介绍 客户想要通过APK来控制IPV6的启用和禁用&#xff0c;这里我们通过广播的方式来让客户控制IPV6。 效果展示 adb shell ifconfig 这里我们用debug软件&#xff0c;将下面节点置为1 如图ipv6已被禁用了 echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6 修改 接下来…

十大排序总结之——冒泡排序、插入排序

同样&#xff0c;这两几乎也是被淘汰了的算法&#xff0c;尽管它们是稳定的&#xff0c;但是时间复杂度没人喜欢&#xff0c;了解一下就好&#xff0c;没啥好说的&#xff0c;注意最后一句话就行了 一&#xff0c;冒泡排序 1. 算法步骤 共n-1趟&#xff0c;谁两敢冒泡就换了…

IoT 物联网常用协议

物联网协议是指在物联网环境中用于设备间通信和数据传输的协议。根据不同的作用&#xff0c;物联网协议可分为传输协议、通信协议和行业协议。 传输协议&#xff1a;一般负责子网内设备间的组网及通信。例如 Wi-Fi、Ethernet、NFC、 Zigbee、Bluetooth、GPRS、3G/4G/5G等。这些…

算法基础之能被整除的数

能被整除的数 核心思想&#xff1a; 容斥原理 总面积 1-23-4…. 总集合元素中个数 1-23-4…. #include<iostream>#include<cstring>#include<algorithm>using namespace std;const int N 20;typedef long long LL;int p[N];int main(){int n,m;cin&…

2024年【茶艺师(初级)】报名考试及茶艺师(初级)考试技巧

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【茶艺师&#xff08;初级&#xff09;】报名考试及茶艺师&#xff08;初级&#xff09;考试技巧&#xff0c;包含茶艺师&#xff08;初级&#xff09;报名考试答案和解析及茶艺师&#xff08;初级&#xff09;…

03.QT命名规范及快捷键(部分)

一、命名规范 1.类名 大驼峰规则&#xff1a;首字母大写&#xff0c;单词和单词之间首字母大写。 2.变量名 小驼峰规则&#xff1a;首字母小写&#xff0c;单词和单词之间首字母大写。 二、快捷键 1.代码操作相关 注释&#xff1a;ctrl / 运行&#xff1a;ctrl r 编译…

基于Java SSM框架实现健康管理系统项目【项目源码】

基于java的SSM框架实现健康管理系统演示 JSP技术 JSP是一种跨平台的网页技术&#xff0c;最终实现网页的动态效果&#xff0c;与ASP技术类似&#xff0c;都是在HTML中混合一些程序的相关代码&#xff0c;运用语言引擎来执行代码&#xff0c;JSP能够实现与管理员的交互&#xf…

PyQt5-控件之QDialog(UI-业务分离搭建自定义xDialog)

1.继承QtWidgets.QWidget自定义对话框 继承于QtWidgets.QWidget自定义一个对话框类&#xff1a;SelectingDlg class SelectingDlg(QtWidgets.QWidget): def __init__(self): super(SelectingDlg, self).__init__() self.initUI() def initUI(self):s…

作业--day39

定义一个Person类&#xff0c;私有成员int age&#xff0c;string &name&#xff0c;定义一个Stu类&#xff0c;包含私有成员double *score&#xff0c;写出两个类的构造函数、析构函数、拷贝构造和拷贝赋值函数&#xff0c;完成对Person的运算符重载(算术运算符、条件运算…

十八、任务通知

1、前言 (1)所谓“任务通知”&#xff0c;可以反过来读"通知任务"。我们使用队列、信号量、事件组等等方法时&#xff0c;并不知道对方是谁。使用任务通知时&#xff0c;可以明确指定&#xff1a;通知哪个任务。 (2)使用队列、信号量、事件组时&#xff0c;我们都需…
最新文章