UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)

UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)

  • 13. 模组生命周期
  • 14. 枚举匹配模组结构
  • 15. 注册对象到模组

13. 模组生命周期

从老师准备的 DataDriven 插件的导图中可知,Driver 类是驱动整个框架的主类,其方法会驱动下一层模组类的对应方法,整个框架的运行顺序呈层级结构。

下面图片截取自梁迪老师的 DataDriven 思维导图。
UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)_第1张图片
那先来到 DDDriver,完善其初始化框架的逻辑。

由于 Driver 需要用到其下层的功能模块,有部分变量和方法都在文章的下面才正式实现,建议读者可以了解 Driver 的大体逻辑后,继续看下层功能模块的逻辑,理清下层的逻辑后再回来看 Driver 的实现,方便理清脉络。

在框架里面,某些模块的特定方法里会被放置一些 Debug 输出,便于后面测试,测试完毕后需要删除。

DDDriver.h

#include "DDCenterModule.h"		// 引入头文件
#include "DDCommon/DDCommon.h"	// 引入头文件
#include "DDDriver.generated.h"

UCLASS()
class DATADRIVEN_API ADDDriver : public AActor
{
	GENERATED_BODY()
	
public:	

	ADDDriver();

	// 移到上面来
	virtual void Tick(float DeltaTime) override;

	// 初始化框架
	virtual void PostInitializeComponents() override;

public:

	UPROPERTY(EditAnywhere, NoClear, BlueprintReadOnly, Category = "DataDriven")
	USceneComponent* RootScene;

	// 中央模组,NoClear 代表无法设置为空
	UPROPERTY(EditAnywhere, NoClear, BlueprintReadOnly, Category = "DataDriven")
	UDDCenterModule* Center;

	// 模组对应的枚举名字
	UPROPERTY(EditDefaultsOnly, Category = "DataDriven")
	FName ModuleType;

protected:

	// 是否已经运行 BeginPlay 函数
	bool IsBeginPlay;
};

DDDriver.cpp

ADDDriver::ADDDriver()
{
 	
	RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
	RootComponent = RootScene;

	Center = CreateDefaultSubobject<UDDCenterModule>(TEXT("Center"));
	Center->SetupAttachment(RootComponent);
}

void ADDDriver::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	// 注册 Driver 到 UDDCommon 单例
	UDDCommon::Get()->InitDriver(this);
	// 在游戏运行之前必须进行一次模组 ID 的设定,在这里面会注册子模组到数组
	Center->IterChangeModuleType(Center, ModuleType);

	// 创建所用模组的模块
	Center->IterCreateManager(Center);
}

void ADDDriver::BeginPlay()
{
	Super::BeginPlay();
	
	// 迭代调用 Init 函数
	Center->IterModuleInit(Center);
}

void ADDDriver::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	if (!IsBeginPlay) {
		// 迭代调用 BeginPlay 函数
		Center->IterModuleBeginPlay(Center);
		// 只执行第一帧
		IsBeginPlay = true;
	}
	else {
		// 迭代调用 Tick 函数
		Center->IterModuleTick(Center, DeltaTime);
	}
}

来到通用类,实现单例模式的功能。

为了让它能在框架的各个地方都可以使用,所以需要注册 Driver 给它。再添加一个获取 Driver 的方法。

DDCommon.h

// 提前声明
class ADDDriver;

namespace DDH
{
	// ... 省略
};

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

	static UDDCommon* Get();

	void InitDriver(ADDDriver* InDriver);

	ADDDriver* GetDriver();

private:

	static UDDCommon* DDInst;

	ADDDriver* Driver;
};

DDCommon.cpp

UDDCommon* UDDCommon::DDInst = NULL;

UDDCommon* UDDCommon::Get()
{
	if (!DDInst) {
		DDInst = NewObject<UDDCommon>();
		DDInst->AddToRoot();
	}
	return DDInst;
}

void UDDCommon::InitDriver(ADDDriver* InDriver)
{
	Driver = InDriver;
}

ADDDriver* UDDCommon::GetDriver()
{
	return Driver;
}

DDMM 作为一个接口,其作用是保存 Model、Message 和 Wealth 这三个模块所属的模组(Module),以及保存 Driver 的指针。

DDMM.h

#include "DDCommon/DDCommon.h"	// 引入头文件
#include "DDMM.generated.h"

class UDDModule;	// 提前声明

UINTERFACE(MinimalAPI)
class UDDMM : public UInterface
{
	GENERATED_BODY()
};

class DATADRIVEN_API IDDMM
{
	GENERATED_BODY()

public:

	// 指定模组
	void AssignModule(UDDModule* Mod);

protected:

	// 保存对应的模组
	UDDModule* IModule;

	// 保存驱动器
	ADDDriver* IDriver;
};

DDMM.cpp

void IDDMM::AssignModule(UDDModule* Mod)
{
	IModule = Mod;

	IDriver = UDDCommon::Get()->GetDriver();
}

写好了接口,那么接下来给要继承接口的类添加上各自的生命周期相关的方法。

Model 的作用是保存数据。

DDModel.h

#include "DDMM.h"	// 引入头文件
#include "DDModel.generated.h"

UCLASS()
class DATADRIVEN_API UDDModel : public UObject, public IDDMM	// 继承接口
{
	GENERATED_BODY()
	
public:

	virtual void ModelInit() {}

	virtual void ModelBeginPlay() {}

	virtual void ModelTick(float DeltaSeconds);
};

DDModel.cpp

void UDDModel::ModelTick(float DeltaSeconds)
{
}

Wealth 的作用是保存资源。

DDWealth.h

#include "DDMM.h"	// 引入头文件
#include "DDWealth.generated.h"

UCLASS()
class DATADRIVEN_API UDDWealth : public UObject, public IDDMM	// 继承接口
{
	GENERATED_BODY()

public:

	virtual void WealthInit() {}

	virtual void WealthBeginPlay();

	// 资源的 Tick 函数
	virtual void WealthTick(float DeltaSeconds);
};

DDWealth.cpp

void UDDWealth::WealthBeginPlay()
{
}

void UDDWealth::WealthTick(float DeltaSeconds)
{
}

Message 负责处理消息。

DDMessage.h

#include "DDMM.h"	// 引入头文件
#include "DDMessage.generated.h"

UCLASS()
class DATADRIVEN_API UDDMessage : public UObject, public IDDMM	// 继承接口
{
	GENERATED_BODY()

public:
	
	virtual void MessageInit();

	virtual void MessageBeginPlay();

	virtual void MessageTick(float DeltaSeconds);
};

DDMessage.cpp

void UDDMessage::MessageInit()
{
	DDH::Debug() << "MessageInit" << DDH::Endl();
}

void UDDMessage::MessageBeginPlay()
{
	DDH::Debug() << "MessageBeginPlay" << DDH::Endl();
}

void UDDMessage::MessageTick(float DeltaSeconds)
{
	DDH::Debug(0.f) << "MessageTick" << DDH::Endl();
}

处理了最顶层的 Driver,则继续来完善它下一级的 Module。

给 DDModule 去掉 TickComponent()BeginPlay() 相关的方法和内容,因为用不上。我们会添加自己的类似方法。

Module 除了自身的生命周期相关方法,还负责实例化自己所拥有的模块(Model、Message、Wealth);并且它还需要保存自己的子 Module。

DDModule.h

#include "DDMessage.h"	// 引入头文件
#include "DDModule.generated.h"

// 提前声明
class UDDModel;
class UDDWealth;

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

public:

	// 实例化模组底下的模块
	void CreateManager();

	// Module 的 Init 函数
	virtual void ModuleInit();

	// Module 的 BeginPlay 函数
	virtual void ModuleBeginPlay();

	// Module 的 Tick 函数
	virtual void ModuleTick(float DeltaSeconds);

// 去掉 TickComponent() 和 BeginPlay()

public:

	// 存放子模组的数组
	TArray<UDDModule*> ChildrenModule;

protected:

	// 数据模块指针
	UDDModel* Model;
	// 消息模块指针
	UDDMessage* Message;
	// 资源模块指针
	UDDWealth* Wealth;
};

DDModule.cpp

// 引入头文件
#include "DDCore/DDModel.h"
#include "DDCore/DDWealth.h"

UDDModule::UDDModule()
{
	PrimaryComponentTick.bCanEverTick = false;	// 改成 false,避免运行原生 Tick()
}

void UDDModule::CreateManager()
{
	// 实例化组件,这里用 NewObject 实例化组件时不能在括号内填 this,否则编辑器运行游戏退出时会崩溃
	Model = NewObject<UDDModel>();
	Message = NewObject<UDDMessage>();
	Wealth = NewObject<UDDWealth>();
	// 避免被 GC 销毁
	Model->AddToRoot();
	Message->AddToRoot();
	Wealth->AddToRoot();
	// 指定模组
	Model->AssignModule(this);
	Message->AssignModule(this);
	Wealth->AssignModule(this);
}

void UDDModule::ModuleInit()
{
	// 调用 Init 函数
	Model->ModelInit();
	Message->MessageInit();
	Wealth->WealthInit();

	DDH::Debug() << GetFName() << " --> ModuleInit" << DDH::Endl();
}

void UDDModule::ModuleBeginPlay()
{
	// 调用 BeginPlay
	Model->ModelBeginPlay();
	Message->MessageBeginPlay();
	Wealth->WealthBeginPlay();

	DDH::Debug() << GetFName() << " --> ModuleBeginPlay" << DDH::Endl();
}

void UDDModule::ModuleTick(float DeltaSeconds)
{
	// 调用 Tick
	Model->ModelTick(DeltaSeconds);
	Wealth->WealthTick(DeltaSeconds);
	Message->MessageTick(DeltaSeconds);

	DDH::Debug(0.f) << GetFName() << " --> ModuleTick" << DDH::Endl();
}

// 去掉 TickComponent() 和 BeginPlay()

中央模组负责存放和管理所有的子模组,它需要提供方法给 Driver 来启动子模组的生命周期。

DDCenterModule.h

UCLASS()
class DATADRIVEN_API UDDCenterModule : public UDDModule
{
	GENERATED_BODY()
	
public:

	// 迭代调用本模组以及子模组的 EditChangeModuleType 方法(后面课程会继续补充)
	void IterChangeModuleType(UDDModule* Module, FName ModType);
	// 递归创建模块
	void IterCreateManager(UDDModule* Module);
	// 递归初始化
	void IterModuleInit(UDDModule* Module);
	// 递归 BeginPlay
	void IterModuleBeginPlay(UDDModule* Module);
	// 递归 Tick
	void IterModuleTick(UDDModule* Module, float DeltaSeconds);
};

DDCenterModule.cpp

void UDDCenterModule::IterChangeModuleType(UDDModule* Module, FName ModType)
{
	for (int i = 0; i < Module->GetAttachChildren().Num(); ++i) {
		UDDModule* ChildModule = Cast<UDDModule>(Module->GetAttachChildren()[i]);
		if (ChildModule) {
			// 添加这个对象到这个 Module 的 ChildModule
			Module->ChildrenModule.Push(ChildModule);
			IterChangeModuleType(ChildModule, ModType);
		}
	}
}

void UDDCenterModule::IterCreateManager(UDDModule* Module)
{
	Module->CreateManager();
	for (int i = 0; i < Module->ChildrenModule.Num(); ++i) {
		IterCreateManager(Module->ChildrenModule[i]);
	}
}

void UDDCenterModule::IterModuleInit(UDDModule* Module)
{
	Module->ModuleInit();
	for (int i = 0; i < Module->ChildrenModule.Num(); ++i) {
		IterModuleInit(Module->ChildrenModule[i]);
	}
}

void UDDCenterModule::IterModuleBeginPlay(UDDModule* Module)
{
	Module->ModuleBeginPlay();
	for (int i = 0; i < Module->ChildrenModule.Num(); ++i) {
		IterModuleBeginPlay(Module->ChildrenModule[i]);
	}
}

void UDDCenterModule::IterModuleTick(UDDModule* Module, float DeltaSeconds)
{
	Module->ModuleTick(DeltaSeconds);
	for (int i = 0; i < Module->ChildrenModule.Num(); ++i) {
		IterModuleTick(Module->ChildrenModule[i], DeltaSeconds);
	}
}

这时候再回头看一下 Driver 类,有利于梳理脉络。

下面的验证功能部分在下一节课才会讲到,为了连贯性我把它放到这里。

在 Content 下创建 Blueprint 文件夹,创建一个基于 DDDrive 的蓝图,取名叫 GameDriver_BP

打开蓝图,在 Center 模组下添加两个 DDModule 并改名如下:

UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)_第2张图片
将蓝图放入场景中。运行游戏,可看见左上角按顺序输出 Debug 语句:

UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)_第3张图片
可见,Driver 启动生命周期,中央模组先进行模组的初始化,然后再初始化其下的子模组,并按照生命周期的顺序执行后续方法。

测试完毕后要将 DDModule.cpp 和 DDMessage.cpp 的 Debug 语句删掉。

14. 枚举匹配模组结构

这节课要探讨的是如何在 Driver 里准确地获得模组(即 DDModule 的实例)。

下图截取自梁迪老师的 DataDriven 思维导图和说明文档。

UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)_第4张图片

新建两个 C++ 的 Object 类,选择 DataDriven 模块,位于 Public/DDGame 路径下,分别取名叫 DGCommonDGTypes

来到 DGTypes,定义两个枚举,一个用于游戏场景内,一个用于菜单界面。

DGTypes.h

#pragma region GamePart

/**************************************************************************/
/* 开发的游戏内容放这里            Begin             开发的游戏内容放这里 */
/**************************************************************************/

// 模组约定,如果要使用 DataDriven 本身的 UI 框架,请一定要把 HUD 模组放在第二位,UIFrame 框架管理器始终在 HUD 模组下
UENUM()
enum class ERCGameModule : uint8
{
	Center = 0,		// 中央模组
	HUD,			// UI 模组,约定放在第二位,UI 框架会直接使用序号 1 去获取 HUD 模组,如果不使用 UI 框架,不需要强制在第二位
	Player,			// 玩家模组
};

UENUM()
enum class ERCMenuModule : uint8
{
	Center = 0,
	HUD,
};

/**************************************************************************/
/* 开发的游戏内容放这里             End              开发的游戏内容放这里 */
/**************************************************************************/

#pragma endregion

给 DGCommon 引入枚举的头文件。

DGCommon.h

#include "DGTypes.h"	// 引入头文件
#include "DGCommon.generated.h"

然后又给 DDCommon 引入头文件,这样三者就连起来了。

然后添加几个方法,用于枚举名与枚举值、序号和文本类型的互相转换。

FName 在创建实例对象的时候,会自动注册到一个专门的哈希表,所以它的查找以及对比的效率比 FString 要高。

DDCommon.h

#include "DDGame/DGCommon.h"	// 引入头文件
#include "DDCommon.generated.h"

namespace DDH
{

	// 将传入的 Enum 值对应的 FString 输出
	template<typename TEnum>
	FORCEINLINE FString GetEnumValueAsString(const FString& Name, TEnum Value)
	{
		const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, *Name, true);
		if (!EnumPtr) return FString("InValid");
		return EnumPtr->GetEnumName((int32)Value);
	}

	// 将传入的 Enum 值对应的 FName 输出
	template<typename TEnum>
	FORCEINLINE FName GetEnumValueAsName(const FString& Name, TEnum Value)
	{
		const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, *Name, true);
		if (!EnumPtr) return FName("InValid");
		return FName(*EnumPtr->GetEnumName((int32)Value));
	}

	// 将传入的 FName 对应的 Enum 输出
	template<typename TEnum>
	FORCEINLINE TEnum GetEnumValueFromName(const FString& Name, FName Value)
	{
		const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, *Name, true);
		if (!EnumPtr) return TEnum(0);
		return (TEnum)EnumPtr->GetIndexByName(Value);
	}

	// 将传入的 FName 对应的 Enum 的序号输出
	FORCEINLINE int32 GetEnumIndexFromName(const FString& Name, FName Value)
	{
		const UEnum* EnumPtr = FindObject<UEnum>((UObject*)ANY_PACKAGE, *Name, true);
		if (!EnumPtr) {
			return -1;
		}
		return EnumPtr->GetIndexByName(Value);
	}
}

再回到模组类,解决上节课未编写完整的 ModuleType 相关方法。

DDModule.h

public:

	// 根据模组名字对应模组序号
	void ChangeModuleType(FName ModuleType);

public:

	// 对应枚举内的模组序号
	UPROPERTY(VisibleAnywhere, Category = "DataDriven")
	int32 ModuleIndex;

DDModule.cpp

void UDDModule::ChangeModuleType(FName ModuleType)
{
	ModuleIndex = DDH::GetEnumIndexFromName(ModuleType.ToString(), GetFName());

	if (ModuleIndex < 0) {
		DDH::Debug() << "Generate Module Index Error --> " << GetName() << DDH::Endl();
	}
}

来到中央模组类,给 IterChangeModuleType() 添加对模组名转换序号方法的调用。

声明一个保存模组的数组,方便取用。再配套两个方法来填充这个数组。

DDCenterModule.h

public:

	// 提取所有模组到模组数组,传入枚举名字
	void TotalGatherModule(FName ModType);
	// 迭代提取所有模组到数组
	void IterGatherModule(UDDModule* Module, TArray<UDDModule*>& GatherGroup);

protected:

	// 保存模组的数组,顺序与枚举相同
	TArray<UDDModule*> ModuleGroup;

DDCenterModule.cpp

void UDDCenterModule::IterChangeModuleType(UDDModule* Module, FName ModType)
{
	Module->ChangeModuleType(ModType);
	
}


void UDDCenterModule::TotalGatherModule(FName ModType)
{
	if (ModType.IsNone()) return;

	// 先获取所有的模组到 GatherGroup
	TArray<UDDModule*> GatherGroup;
	IterGatherModule(this, GatherGroup);

	// 获取枚举的元素数量
	int32 ModuleNum = FindObject<UEnum>((UObject*)ANY_PACKAGE, *(ModType.ToString()), true)->GetMaxEnumValue();
	// 填充空对象到 ModuleGroup
	for (int i = 0; i < ModuleNum; ++i) {
		ModuleGroup.Push(NULL);
	}
	// 按模组 ID 填充模组到 ModuleGroup
	for (int i = 0; i < GatherGroup.Num(); ++i) {
		ModuleGroup[GatherGroup[i]->ModuleIndex] = GatherGroup[i];
	}
}

void UDDCenterModule::IterGatherModule(UDDModule* Module, TArray<UDDModule*>& GatherGroup)
{
	GatherGroup.Push(Module);
	for (int i = 0; i < Module->ChildrenModule.Num(); ++i) {
		IterGatherModule(Module->ChildrenModule[i], GatherGroup);
	}
}

给 Driver 添加一个只要 ModuleType 变了就迭代更新模组序号的方法。以及在初始化框架的时候让中央模组填充模组数组。

DDDriver.h

public:

#if WITH_EDITOR
	// 属性修改方法
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;

#endif

DDDriver.cpp

void ADDDriver::PostInitializeComponents()
{
	
	// 指定完模组 ID 后收集模组到总数组
	Center->TotalGatherModule(ModuleType);
	
	Center->IterCreateManager(Center);
}


#if WITH_EDITOR
void ADDDriver::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);

	if (PropertyChangedEvent.Property && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(ADDDriver, ModuleType)) {
		Center->IterChangeModuleType(Center, ModuleType);
	}
}
#endif

编译后,运行游戏,可以看到右上角 Debug 语句报告生成模组序列错误。

Debug 语句报错

打开 GameDriver_BP,在细节面板将其 Data Driven 目录下的 ModuleType 的值设置为 ERCGameModule。再次运行游戏,可以看到左上角的 Debug 报错没有了。(不要设置成 ERCMenuModule,否则会因为已有的模组多于枚举内模组而报错)

15. 注册对象到模组

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

UE4运用C++和框架开发坦克大战教程笔记(五)(第13~15集)_第5张图片
我们打算让对象通过指定一个 FName 类型的模组名来将其注册到指定的模组底下,并且注册到 DDModel 这个数据模块来方便保存数据。

DDOO 作为一个接口,它的作用是让继承它的类能与框架交互。

DDOO.h

// 引入头文件
#include "DDCore/DDDriver.h"
#include "DDCore/DDModule.h"
// 引入完毕
#include "DDOO.generated.h"


class DATADRIVEN_API IDDOO
{
	GENERATED_BODY()
public:

	// 注册到框架
	void RegisterToModule(FName ModName, FName ObjName = FName(), FName ClsName = FName());

	// 获取对象名
	FName GetObjectName();

	// 获取类名
	FName GetClassName();

	// 获取模组 Index
	int32 GetModuleIndex() const;

	// 获取 Object 主体
	UObject* GetObjectBody() const;

	// 从外部指定模组
	void AssignModule(UDDModule* Mod);

protected:

	// 保存自身的 UObject
	UObject* IBody;

	// 保存对应的模组
	UDDModule* IModule;

	// 保存驱动器
	ADDDriver* IDriver;

	// 对象名字
	FName IObjectName;

	// 对象类名字
	FName IClassName;

	// 对应模组的序号
	int32 ModuleIndex;
};

DDOO.cpp

void IDDOO::RegisterToModule(FName ModName, FName ObjName, FName ClsName)
{
	// 判断是否已经注册到框架了
	if (IDriver && IModule) return;
	// 如果模组名为空,直接返回
	if (ModName.IsNone()) return;
	// 指定对象名和类名
	if (!ObjName.IsNone()) IObjectName = ObjName;
	if (ClsName.IsNone()) IClassName = ClsName;
	// 获取 UObject 主体
	IBody = Cast<UObject>(this);
	// 获取驱动器
	IDriver = UDDCommon::Get()->GetDriver();
	// 注册到模组
	if (IDriver) {
		// 如果获得的 ID 为负直接返回
		ModuleIndex = DDH::GetEnumIndexFromName(IDriver->ModuleType.ToString(), ModName);
		if (ModuleIndex < 0) {
			DDH::Debug() << GetObjectName() << " Get " << ModName << " ModuleIndex Failed!" << DDH::Endl();
			return;
		}
		// 如果注册不成功说明还没有实例化对应的 Module
		if (!IDriver->RegisterToModule(this)) {
			DDH::Debug() << GetObjectName() << " Register To " << ModName << " Failed!" << DDH::Endl();
		}
	}
	else {
		// DDriver 不存在
		DDH::Debug() << GetObjectName() << " Get DDDriver Failed!" << DDH::Endl();
	}
	
}

FName IDDOO::GetObjectName()
{
	if (!IObjectName.IsNone()) return IObjectName;
	IObjectName = IBody->GetFName();
	return IObjectName;
}

FName IDDOO::GetClassName()
{
	if (!IClassName.IsNone()) return IClassName;
	IClassName = IBody->StaticClass()->GetFName();
	return IClassName;
}

int32 IDDOO::GetModuleIndex() const
{
	return ModuleIndex;
}

UObject* IDDOO::GetObjectBody() const
{
	return IBody;
}

void IDDOO::AssignModule(UDDModule* Mod)
{
	IModule = Mod;
}

Driver 通过 DDOO 传递数据给中央模组判定对象指定的模组名所对应的序号是否有效,有效则将该对象注册给对应的模组和 DDModel。

DDDriver.h

class IDDOO;	// 提前声明

UCLASS()
class DATADRIVEN_API ADDDriver : public AActor
{
	GENERATED_BODY()
public:

	// 提供给资源们进行注册
	bool RegisterToModule(IDDOO* ObjectInst);
};

DDDriver.cpp

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

bool ADDDriver::RegisterToModule(IDDOO* ObjectInst)
{
	// 调用中央模组进行注册
	return Center->RegisterToModule(ObjectInst);
}

DDModel 这里先定义空白的注册方法,后续再继续补充。

DDModel.h

class IDDOO;

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

	// 注册对象到数据模块
	void RegisterObject(IDDOO* ObjectInst);
};

DDModel.cpp

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

void UDDModel::RegisterObject(IDDOO* ObjectInst)
{
}

提供一个方法,让传过来的对象注册到 DDModel 里,同时也使模组自己作为对象的模组。

DDModule.h

class IDDOO;	// 提前声明

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

public:

	// 注册对象到数据模块
	void RegisterObject(IDDOO* ObjectInst);
};

DDModule.cpp

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

void UDDModule::RegisterObject(IDDOO* ObjectInst)
{
	// 注册对象到数据组件
	Model->RegisterObject(ObjectInst);
	// 把自己注册到对象的模组
	ObjectInst->AssignModule(this);
}

终于来到中央模组,它要做的就是运用上面的类提供的方法使对象能够注册到模组和 DDModel。

DDCenterModule.h

class IDDOO;	// 声明

UCLASS()
class DATADRIVEN_API UDDCenterModule : public UDDModule
{
	GENERATED_BODY()
	
public:

	// 注册对象到模组
	bool RegisterToModule(IDDOO* ObjectInst);
};

DDCenterModule.cpp

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

bool UDDCenterModule::RegisterToModule(IDDOO* ObjectInst)
{
	// 判断模组 ID 是否有效并且注册
	if (ObjectInst->GetModuleIndex() < ModuleGroup.Num() && ModuleGroup[ObjectInst->GetModuleIndex()]) {
		ModuleGroup[ObjectInst->GetModuleIndex()]->RegisterObject(ObjectInst);
		return true;
	}
	return false;
}

下面的测试内容是下一节课才会进行演示的,为了连贯性,笔者就安排在这里了。

DDActor 用于作为框架内 Actor 的基类,声明三个 FName 变量,指定其目标模组、对象名和类名。

去掉原生的 Tick(),因为框架本身有自己的 Tick。

DDActor.h

#include "DDOO.h"	// 引入头文件
#include "DDActor.generated.h"

UCLASS()
class DATADRIVEN_API ADDActor : public AActor, public IDDOO	// 继承接口
{
	GENERATED_BODY()

public:

	// 模组名字,如果为空,说明要手动指定,不为空就是自动指定
	UPROPERTY(EditAnywhere, Category = "DataDriven")
	FName ModuleName;

	// 对象名字,如果为空,说明要手动指定,不为空就是自动指定
	// 这个名字是独一无二的
	UPROPERTY(EditAnywhere, Category = "DataDriven")
	FName ObjectName;

	// 类名字,如果为空,说明要手动指定,不为空就是自动指定
	UPROPERTY(EditAnywhere, Category = "DataDriven")
	FName ClassName;

// 去掉 Tick()
};

DDActor.cpp

ADDActor::ADDActor()
{
	PrimaryActorTick.bCanEverTick = false;	// 关闭原生 Tick
}

void ADDActor::BeginPlay()
{
	Super::BeginPlay();
	
	// 自动注册到框架
	RegisterToModule(ModuleName, ObjectName, ClassName);
}

// 去掉 Tick()

编译后,在 Blueprint 文件夹下创建 DDActor 的蓝图,取名叫 ShowActor_BP,然后将其放进场景里。

打开蓝图,在其细节面板的 DataDriven 目录下随便修改 ModuleName 的值,如 Nothing。运行游戏,可以看见左上角显示 Debug 语句显示获取模组序列失败。

若把 ModuleName 的值改成 Player 这个有效的枚举值,运行游戏,左上角没有 Debug 提示,则说明对象注册模组和成功了。

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