UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)

  • 26. 异质链表数据结构
  • 27. 事件节点与队列
  • 28. 调用句柄与方法句柄
  • 29. 注册事件系统整合
    • 验证事件注册系统

26. 异质链表数据结构

下图截取自梁迪老师准备的 DataDriven 文档。

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)_第1张图片
通过 TFunction 实现的函数传递可以在课程第六集进行回顾。值得注意的是,在本系统里面 Handle(句柄)也可以指代接口,只要读者知道这两者是一样的就好理解一点。

为了实现这个注册事件系统,我们需要实现一个可以存储任意类型方法的数据结构。

目前我们先在 ReflectActor 里实现一个可以存储任意类型变量的结构体,当作小型测试。

ReflectActor.h

// 存储任意类型的数据结构
struct AnyElement
{
	// 元素父结构体
	struct BaseElement
	{
	public:
		virtual ~BaseElement(){}	// 确保子类运行析构函数的时候也调用父类的析构函数
	};
	
	// 实际存储值的结构体
	template<typename T>
	struct ValueElement : public BaseElement
	{
	public:
		T Value;
		ValueElement(const T& InValue) : Value(InValue) {}	// In 可以理解为 Insert(插入)
	};
	
	// 父结构体指针,用于存储实例化的子结构体的地址
	BaseElement* ElementPtr;
	
public:

	AnyElement() : ElementPtr(NULL) {}	// 无参构造函数,确保没有参数时也可以创建对象
	
	// 构造函数传入值并且实例化子结构体存储于父结构体指针
	template<typename T>
	AnyElement(const T& InValue) : ElementPtr(new ValueElement<T>(InValue)) {}
	
	~AnyElement() { delete ElementPtr; }
	
	// 获取保存的变量
	template<typename T>
	T& Get()
	{
		// 通过将父类指针强转为子类指针来获取子类指针内保存的值
		ValueElement<T>* SubPtr = static_cast<ValueElement<T>*>(ElementPtr);
		return SubPtr->Value;
	}
};

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

protected:
	
	// 存储任意类型的数组
	TArray<AnyElement*> ElementList;
};

接下来我们在 .cpp 里,将四种不同的类型放进这个结构体数组来测试一下。

ReflectActor.cpp

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

	ElementList.Push(new AnyElement(23333));
	ElementList.Push(new AnyElement(FString("Happy Day")));
	ElementList.Push(new AnyElement(true));
	ElementList.Push(new AnyElement(FVector(1.f, 3.f, 6.f)));

	// 下面 Debug 语句测试完毕后注释掉
	DDH::Debug() << ElementList[0]->Get<int32>() << DDH::Endl();
	DDH::Debug() << ElementList[1]->Get<FString>() << DDH::Endl();
	DDH::Debug() << ElementList[2]->Get<bool>() << DDH::Endl();
	DDH::Debug() << ElementList[3]->Get<FVector>() << DDH::Endl();
}

编译后运行游戏,可以看见左上角输出了传入的数据,说明这个可以存储任何类型变量的结构体写好了。

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)_第2张图片
接下来我们仿照上面的代码,在 DDTypes 写一个能够存储任意类型的方法的数据结构。

DDTypes.h


#pragma region DDAnyFun

// 存储任意类型方法的结构体
struct DDAnyFun
{
	struct BaseFun
	{
	public:
		virtual ~BaseFun() {}
	};
	
	template<typename RetType, typename... VarTypes>
	struct ValFun : public BaseFun
	{
	public:
		TFunction<RetType(VarTypes...)> TarFun;		// 实际存储方法的 TFunction 变量
		// 构造函数,将 TFunction 类型的变量存储进 TarFun,Ins 是 Insert 的缩写
		ValFun(const TFunction<RetType(VarTypes...)> InsFun) : TarFun(InsFun) {}
		// 执行存储的方法
		RetType Execute(VarTypes... Params)
		{
			return TarFun(Params...);
		}
	};
	
	BaseFun* FunPtr;	// 父结构体的指针
	
public:
	DDAnyFun() : FunPtr(NULL) {}
	
	template<typename RetType, typename... VarTypes>
	DDAnyFun(const TFunction<RetType(VarTypes...)> InsFun) : FunPtr(new ValFun<RetType, VarTypes...>(InsFun)) {}
	
	~DDAnyFun() { delete FunPtr; }
	
	// 直接执行存储的方法
	template<typename RetType, typename... VarTypes>
	RetType Execute(VarTypes... Params)
	{
		ValFun<RetType, VarTypes...>* SubFunPtr = static_cast<ValFun<RetType, VarTypes...>*>(FunPtr);
		return SubFunPtr->Execute(Params...);
	}

	// 获取存储的方法
	template<typename RetType, typename... VarTypes>
	TFunction<RetType(VarTypes...)>& GetFun()
	{
		ValFun<RetType, VarTypes...>* SubFunPtr = static_cast<ValFun<RetType, VarTypes...>*>(FunPtr);
		return SubFunPtr->TarFun;
	}
};

#pragma endregion

接下来我们来到 ReflectActor 来验证一下上面写的 存储任意类型方法 的数据结构。

更改一下 WealthCall() 的返回类型,以便测试有返回值和没返回值这两种方法的情况。

ReflectActor.h

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

public:

	// 更改返回类型为 int32
	UFUNCTION()
	int32 WealthCall(int32 Counter, FString InfoStr, bool InFlag);

	// 将 TFunction 类型的变量放进存储数组
	template<typename RetType, typename... VarTypes>
	void ReFunList(TFunction<RetType(VarTypes...)> InsertFun);

protected:

	// 存储任意类型方法的数组
	TArray<DDAnyFun*> FunList;
};

template<typename RetType, typename... VarTypes>
void AReflectActor::ReFunList(TFunction<RetType(VarTypes...)> InsertFun)
{
	FunList.Push(new DDAnyFun(InsertFun));
}

ReflectActor.cpp

void AReflectActor::DDEnable()
{
	

	// 利用 lambda 表达式,间接将本地的方法存储进数组
	ReFunList<void, FString>([this](FString InfoStr) { AcceptCall(InfoStr); });
	ReFunList<int32, int32, FString, bool>([this](int32 Counter, FString InfoStr, bool InFlag) { return WealthCall(Counter, InfoStr, InFlag); });

	// 执行存储数组里面的方法(需传入对应实参),测试完毕后记得注释掉下面两行
	FunList[0]->Execute<void, FString>(FString("Happy New Year"));
	DDH::Debug() << FunList[1]->Execute<int32, int32, FString, bool>(2333, FString("No Way"), false) << DDH::Endl();
}

// 更改返回类型为 int32 并添加返回值
int32 AReflectActor::WealthCall(int32 Counter, FString InfoStr, bool InFlag)
{
	DDH::Debug() << Counter << " --> " << InfoStr << " --> " << InFlag << DDH::Endl();
	return 7689;
}

编译后运行,可见左上角输出如下,说明存储任意类型方法的数据结构实现成功:

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)_第3张图片

测试完毕后注释掉调用语句和 Debug 语句。

27. 事件节点与队列

下图截取自梁迪老师准备的 DataDriven 文档。

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)_第4张图片
我们上面实现了能存储任意类型方法的结构体,接下来就要利用它来实现事件节点和事件队列。

事件节点 有一个记录自己被注册调用了多少次的 int32 类型变量(如果为 0 则销毁自身);有一个以 int32 为键、DDAnyFun* 为值的 TMap。

尽管 DDAnyFun* 可以指向任何类型的方法,但是能存在于同一个事件节点实例内的方法都是同类型的。并且每当一个方法注册进事件节点的时候,会自动寻找一个没有用到的序号来标识它,并且返回这个序号。

事件队列 有一个以 FName 为键、DDMsgNode(事件节点)为值的 TMap,意味着我们可以通过标识名来获取对应的事件节点。

DDTypes.h

#pragma region DDMsgNode

// 事件节点
struct DDMsgNode
{
	// 被调用的接口数量
	int32 CallCount;
	// 方法列表
	TMap<int32, DDAnyFun*> FunQuene;
	// 注册方法
	template<typename RetType, typename... VarTypes>
	int32 RegisterFun(TFunction<RetType(VarTypes...)> InsFun);
	// 注销方法
	void UnRegisterFun(int32 FunID)
	{
		// 从列表移除对象
		DDAnyFun* DesPtr = *FunQuene.Find(FunID);
		FunQuene.Remove(FunID);
		delete DesPtr;
	}
	// 清空节点
	void ClearNode()
	{
		for (TMap<int32, DDAnyFun*>::TIterator It(FunQuene); It; ++It) {
			delete It.Value();
		}
	}
	// 执行方法,目前默认返回第一个函数返回的值
	template<typename RetType, typename... VarTypes>
	RetType Execute(VarTypes... Params);
	// 判断是否有绑定的函数
	bool IsBound() { return FunQuene.Num() > 0; }
	// 如果有绑定函数就去执行
	template<typename RetType, typename... VarTypes>
	bool ExecuteIfBound(VarTypes... Params);
	// 构造函数,初始化 CallCount 为 0
	DDMsgNode() : CallCount(0) {}
};

template<typename RetType, typename... VarTypes>
int32 DDMsgNode::RegisterFun(TFunction<RetType(VarTypes...)> InsFun)
{
	// 获取方法序列里的所有下标
	TArray<int32> FunKeyQuene;
	FunQuene.GenerateKeyArray(FunKeyQuene);
	// 获取新下标
	int32 NewID;
	for (int32 i = FunKeyQuene.Num(); i >= 0; --i) {
		if (!FunKeyQuene.Contains(i)) {
			NewID = i;
			break;
		}
	}
	// 将新方法添加到节点
	FunQuene.Add(NewID, new DDAnyFun(InsFun));
	return NewID;
}

template<typename RetType, typename... VarTypes>
RetType DDMsgNode::Execute(VarTypes... Params)
{
	// 遍历执行第二个到最后一个方法
	TMap<int32, DDAnyFun*>::TIterator It(FunQuene);
	++It;
	for (; It; ++It) {
		// 调用遍历到的 DDAnyFun 结构体自己的 Execute()
		It.Value()->Execute<RetType, VarTypes...>(Params...);
	}
	// 获取序列第一个方法
	TMap<int32, DDAnyFun*>::TIterator IBegin(FunQuene);
	// 只返回序列第一个方法的返回值
	return IBegin.Value()->Execute<RetType, VarTypes...>(Params...);
}

template<typename RetType, typename... VarTypes>
bool DDMsgNode::ExecuteIfBound(VarTypes... Params)
{
	if (!IsBound()) return false;
	for (TMap<int32, DDAnyFun*>::TIterator It(FunQuene); It; ++It) {
		It.Value()->Execute<RetType, VarTypes...>(Params...);
	}
	return true;
}

#pragma endregion

// 接下来写的事件队列,要用到调用句柄和方法句柄,老师测试过
// 要以 “调用句柄->事件队列->方法句柄” 的代码顺序才不会报错

// 提前写好事件队列需要用到的调用句柄,下一节课会详细讲到
#pragma region DDCallHandle

struct DDMsgQuene;

template<typename RetType, typename... VarTypes>
struct DDCallHandle
{
	// 构造函数
	DDCallHandle(DDMsgNode* MQ, FName CN) {
	
	}
};

#pragma endregion



#pragma region DDMsgHandle

struct DDFunHandle;

// 事件队列
struct DDMsgQuene
{
	// 节点序列
	TMap<FName, DDMsgNode> MsgQuene;
	// 注册调用接口
	template<typename RetType, typename... VarTypes>
	DDCallHandle<RetType, VarTypes...> RegisterCallPort(FName CallName);
	// 注册方法接口
	template<typename RetType, typename... VarTypes>
	DDFunHandle RegisterFunPort(FName CallName, TFunction<RetType(VarTypes...)> InsFun);
	// 注销调用接口
	void UnRegisterCallPort(FName CallName)
	{
		// 让对应的节点调用计数器减一,如果计数器小于等于 0,就移除调用接口
		MsgQuene.Find(CallName)->CallCount--;
		if (MsgQuene.Find(CallName)->CallCount <= 0) {
			MsgQuene.Find(CallName)->ClearNode();
			MsgQuene.Remove(CallName);
		}
	}
};

#pragma endregion


// 提前写好事件队列需要用到的方法句柄,下一节课会详细讲到
#pragma region DDFunHandle

struct DDFunHandle
{
	DDFunHandle(DDMsgQuene* MQ, FName CN, int32 FI)
	{
		
	}
};

#pragma endregion

如果编译没问题,那么剩下的内容留到下一节课。(有可能等到后面测试时编译才会发现错误)

28. 调用句柄与方法句柄

下图截取自梁迪老师准备的 DataDriven 文档。

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)_第5张图片
接下来我们补全 调用句柄 DDCallHandle、事件队列 DDMsgQuene 和 方法句柄 DDFunHandle 的代码。

调用句柄 有一个指向 DDMsgQuene(事件队列)的指针,以及一个 FName 类型的调用名,定义一个调用句柄的时候要用到它们。

方法句柄 也类似调用句柄,不过它多出一个 int32 类型的方法序号,实际注册方法句柄时这个方法序号是由事件队列自动搜索出可用序号来提供的。

事件队列提供了对调用句柄、方法句柄的注册与销毁的方法。

DDTypes.h


#pragma region DDCallHandle

struct DDMsgQuene;

// 调用句柄
template<typename RetType, typename... VarTypes>
struct DDCallHandle
{
	// 事件队列
	DDMsgQuene* MsgQuene;
	// 节点名 / 调用名
	FName CallName;
	// 调用句柄是否有效,并且用于重写等于操作符保存状态
	TSharedPtr<bool> IsActived;
	// 执行方法
	RetType Execute(VarTypes... Params);
	// 是否已经绑定
	bool IsBound();
	// 如果绑定就执行
	bool ExecuteIfBound(VarTypes... Params);
	// 注销调用接口
	void UnRegister();
	// 无参构造函数
	DDCallHandle() {}
	// 有参构造函数
	DDCallHandle(DDMsgQuene* MQ, FName CN) {
		MsgQuene = MQ;
		CallName = CN;
		// 构建时状态为激活状态
		IsActived = MakeShareable<bool>(new bool(true));
	}
	// 重写操作符
	DDCallHandle<RetType, VarTypes...>& operator=(const DDCallHandle<RetType, VarTypes...>& Other)
	{
		if (this == &Other)
			return *this;
		MsgQuene = Other.MsgQuene;
		CallName = Other.CallName;
		IsActived = Other.IsActived;
		return *this;
	}
};

template<typename RetType, typename... VarTypes>
void DDCallHandle<RetType, VarTypes...>::UnRegister()
{
	if (*IsActived.Get())
		MsgQuene->UnRegisterCallPort(CallName);
	*IsActived.Get() = false;
}

template<typename RetType, typename... VarTypes>
bool DDCallHandle<RetType, VarTypes...>::ExecuteIfBound(VarTypes... Params)
{
	if (!IsBound() || !*IsActived.Get())
		return false;
	MsgQuene->Execute<RetType, VarTypes...>(CallName, Params...);
	return true;
}

template<typename RetType, typename... VarTypes>
bool DDCallHandle<RetType, VarTypes...>::IsBound()
{
	if (!*IsActived.Get())
		return false;
	return MsgQuene->IsBound(CallName);
	
}

template<typename RetType, typename... VarTypes>
RetType DDCallHandle<RetType, VarTypes...>::Execute(VarTypes... Params)
{
	if (!IsBound() || !*IsActived.Get())
		return NULL;
	return MsgQuene->Execute<RetType, VarTypes...>(CallName, Params...);
}

#pragma endregion

#pragma region DDMsgHandle

struct DDFunHandle;

struct DDMsgQuene
{
	


	void UnRegisterCallPort(FName CallName)
	{
		// 优化一下此处的代码,用 Find() 会比较耗性能
		// 获取事件节点
		DDMsgNode* MsgNode = MsgQuene.Find(CallName);
		MsgNode->CallCount--;
		if (MsgNode->CallCount <= 0) {
			MsgNode->ClearNode();
			MsgQuene.Remove(CallName);
		}
	}
	// 注销方法接口
	void UnRegisterFunPort(FName CallName, int32 FunID)
	{
		MsgQuene.Find(CallName)->UnRegisterFun(FunID);
	}
	// 执行方法接口
	template<typename RetType, typename... VarTypes>
	RetType Execute(FName CallName, VarTypes... Params);
	// 是否已经绑定方法
	bool IsBound(FName CallName) { return MsgQuene.Find(CallName)->IsBound(); }
};

template<typename RetType, typename... VarTypes> 
DDCallHandle<RetType, VarTypes...> DDMsgQuene::RegisterCallPort(FName CallName)
{
	// 如果已经存在对应 CallName 的调用接口,就把调用计数器 + 1
	if (MsgQuene.Contains(CallName)) {
		MsgQuene.Find(CallName)->CallCount++;
	}
	else {
		// 创建新的事件节点并且添加到队列
		MsgQuene.Add(CallName, DDMsgNode());
		// 计数器加 1
		MsgQuene.Find(CallName)->CallCount++;
	}
	// 返回调用句柄
	return DDCallHandle<RetType, VarTypes...>(this, CallName);
}

template<typename RetType, typename... VarTypes>
DDFunHandle DDMsgQuene::RegisterFunPort(FName CallName, TFunction<RetType(VarTypes...)> InsFun)
{
	// 获取新的方法下标
	int32 FunID;
	// 如果不存在 CallName 对应的节点
	if (!MsgQuene.Contains(CallName)) {
		// 创建新的事件节点并且添加到队列
		MsgQuene.Add(CallName, DDMsgNode());
	}
	// 直接将新的方法注册到节点
	FunID = MsgQuene.Find(CallName)->RegisterFun(InsFun);
	// 返回方法句柄
	return DDFunHandle(this, CallName, FunID);
}

template<typename RetType, typename... VarTypes>
RetType DDMsgQuene::Execute(FName CallName, VarTypes... Params)
{
	return MsgQuene.Find(CallName)->Execute<RetType, VarTypes...>(Params...);
}

#pragma endregion

#pragma region DDFunHandle

struct DDFunHandle
{
	// 消息队列
	DDMsgQuene* MsgQuene;
	// 调用名字
	FName CallName;
	// 方法 ID
	int32 FunID;
	// 是否有效
	TSharedPtr<bool> IsActived;
	// 注销方法
	void UnRegister()
	{
		if (*IsActived.Get())
			MsgQuene->UnRegisterFunPort(CallName, FunID);
		// 设置失活
		*IsActived.Get() = false;
	}
	// 无参构造函数
	DDFunHandle() {}
	// 有参构造函数
	DDFunHandle(DDMsgQuene* MQ, FName CN, int32 FI)
	{
		MsgQuene = MQ;
		CallName = CN;
		FunID = FI;
		// 设置状态为激活
		IsActived = MakeShareable<bool>(new bool(true));
	}
	// 重写 = 操作符
	DDFunHandle& operator=(const DDFunHandle& Other)
	{
		if (this == &Other)
			return *this;
		MsgQuene = Other.MsgQuene;
		CallName = Other.CallName;
		FunID = Other.FunID;
		IsActived = Other.IsActived;
		return *this;
	}
};

#pragma endregion

如果编译成功则没问题,下一节课我们将其整合到框架。

29. 注册事件系统整合

下图截取自梁迪老师准备的 DataDriven 文档。

UE4运用C++和框架开发坦克大战教程笔记(九)(第26~29集)_第6张图片
我们打算将一个事件队列置于 Message 模块,之后安排注册 调用句柄 和 方法句柄 的逻辑从 Message 模块沿着模组到 DDOO 接口类。

需要注意的是,对象如果要注册调用句柄,它只能指向所属模组下 Message 模块的这个事件队列;而对象要注册方法句柄,则可以指向指定模组下 Message 模块的事件队列。按照先前系统的思路,我们也要提供一个 DDOO -> DDDriver -> DDCenterModule -> 指定模组 的调用路线。

DDMessage.h

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

public:

	// 声明构造函数
	UDDMessage();

	// 注册调用接口
	template<typename RetType, typename... VarTypes>
	DDCallHandle<RetType, VarTypes...> RegisterCallPort(FName CallName);

	// 注册方法接口
	template<typename RetType, typename... VarTypes>
	DDFunHandle RegisterFunPort(FName CallName, TFunction<RetType(VarTypes...)> InsFun);

protected:

	// 事件队列
	DDMsgQuene* MsgQuene;
};

template<typename RetType, typename... VarTypes>
DDCallHandle<RetType, VarTypes...> UDDMessage::RegisterCallPort(FName CallName)
{
	return MsgQuene->RegisterCallPort<RetType, VarTypes...>(CallName);
}

template<typename RetType, typename... VarTypes>
DDFunHandle UDDMessage::RegisterFunPort(FName CallName, TFunction<RetType(VarTypes...)> InsFun)
{
	return MsgQuene->RegisterFunPort<RetType, VarTypes...>(CallName, InsFun);
}

DDMessage.cpp

UDDMessage::UDDMessage()
{
	MsgQuene = new DDMsgQuene();
}

DDModule.h

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

public:

	// 注册调用接口
	template<typename RetType, typename... VarTypes>
	DDCallHandle<RetType, VarTypes...> RegisterCallPort(FName CallName);

	// 注册方法接口
	template<typename RetType, typename... VarTypes>
	DDFunHandle RegisterFunPort(FName CallName, TFunction<RetType(VarTypes...)> InsFun);

};

template<typename RetType, typename... VarTypes>
DDCallHandle<RetType, VarTypes...> UDDModule::RegisterCallPort(FName CallName)
{
	return Message->RegisterCallPort<RetType, VarTypes...>(CallName);
}

template<typename RetType, typename... VarTypes>
DDFunHandle UDDModule::RegisterFunPort(FName CallName, TFunction<RetType(VarTypes...)> InsFun)
{
	return Message->RegisterFunPort<RetType, VarTypes...>(CallName, InsFun);
}

DDOO.h

class DATADRIVEN_API IDDOO
{
	GENERATED_BODY()

protected:

	// 注册调用接口
	template<typename RetType, typename... VarTypes>
	DDCallHandle<RetType, VarTypes...> RegisterCallPort(FName CallName);

	// 注册方法接口
	template<typename RetType, typename... VarTypes>
	DDFunHandle RegisterFunPort(int32 ModuleID, FName CallName, TFunction<RetType(VarTypes...)> InsFun);
};

template<typename RetType, typename... VarTypes>
DDCallHandle<RetType, VarTypes...> IDDOO::RegisterCallPort(FName CallName)
{
	return IModule->RegisterCallPort<RetType, VarTypes...>(CallName);
}

template<typename RetType, typename... VarTypes>
DDFunHandle IDDOO::RegisterFunPort(int32 ModuleID, FName CallName, TFunction<RetType(VarTypes...)> InsFun)
{
	// 如果是对象所属的模组,直接调用所属模组的 RegisterFunPort()
	if (ModuleIndex == ModuleID)
		return IModule->RegisterFunPort<RetType, VarTypes...>(CallName, InsFun);
	// 否则通过 DDDriver->CenterModule->所属模组 这个路线调用 RegisterFunPort()
	else
		return IDriver->RegisterFunPort<RetType, VarTypes...>(ModuleID, CallName, InsFun);
}

DDCenterModule.h

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

public:

	// 注册调用接口,为了跟父类 UDDModule 的注册方法区别开来,添加前缀 Allot
	template<typename RetType, typename... VarTypes>
	DDFunHandle AllotRegisterFunPort(int32 ModuleID, FName CallName, TFunction<RetType(VarTypes...)> InsFun);
};

template<typename RetType, typename... VarTypes>
DDFunHandle UDDCenterModule::AllotRegisterFunPort(int32 ModuleID, FName CallName, TFunction<RetType(VarTypes...)> InsFun)
{
	if (ModuleGroup[ModuleID])
		return ModuleGroup[ModuleID]->RegisterFunPort<RetType, VarTypes...>(CallName, InsFun);
	return DDFunHandle();
}

DDDriver.h

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

public:

	// 注册方法接口
	template<typename RetType, typename... VarTypes>
	DDFunHandle RegisterFunPort(int32 ModuleID, FName CallName, TFunction<RetType(VarTypes...)> InsFun);
};

template<typename RetType, typename... VarTypes>
DDFunHandle ADDDriver::RegisterFunPort(int32 ModuleID, FName CallName, TFunction<RetType(VarTypes...)> InsFun)
{
	return Center->AllotRegisterFunPort<RetType, VarTypes...>(ModuleID, CallName, InsFun);
}

至此,我们已经将事件注册系统整合到了框架。本系统代码繁多,如果读者暂时不太能理清其中脉络,希望读者可以将下面的测试和上面的全部内容多阅读几次,这样对理解这一系统有很大的裨益。

验证事件注册系统

接下来开始验证。

我们目前将 MsgQuene 安排在 DDMessage 模块,所以打算让 ReflectActor 在注册到框架时也同样注册一个调用句柄(调用句柄的 MsgQuene* 指向所属模组的 DDMessage 下的 MsgQuene),随后在 Tick() 里调用 LifeCallActor 注册的的一个方法句柄。

LifeCallActor 通过指定模组名、调用名、调用本地方法的 Lambda 表达式来注册方法句柄,指定模组下的 DDMessage 模块里的 MsgQuene 会存储这个 Lambda 表达式到对应调用名下的 MsgNode。这样,ReflectActor 通过调用句柄的调用名,就可以从 MsgQuene 里找到对应的 MsgNode,以此来执行这个 MsgNode 下绑定的所有方法。

最后我们打算在运行一段时间过后注销这个调用句柄,看看是否会正确地中断这个过程。

ReflectActor.h

public:

	AReflectActor();

	virtual void DDRegister() override;

	virtual void DDTick(float DeltaSeconds) override;

protected:

	// 老师的代码将句柄名拼写错了
	DDCallHandle<int32, FString> RegCallHandle;

	int32 TimeCounter;

ReflectActor.cpp

AReflectActor::AReflectActor()
{
	IsAllowTickEvent = true;	// 开启 DDTick()
}

void AReflectActor::DDRegister()
{
	Super::DDRegister();

	// 指定调用句柄的目标方法的签名(返回值和形参的类型),以及目标调用名为 RegCall
	RegCallHandle = RegisterCallPort<int32, FString>("RegCall");	
}

void AReflectActor::DDTick(float DeltaSeconds)
{
	Super::DDTick(DeltaSeconds);

	// 输出调用句柄执行后的返回值
	DDH::Debug(0.f) << RegCallHandle.Execute(FString::FromInt(TimeCounter++)) << DDH::Endl();

	// 在计数器为 450 的时候注销调用句柄
	if (TimeCounter == 450)
		RegCallHandle.UnRegister();
}

LifeCallActor.h

public:

	int32 RegTest(FString InfoStr);

protected:

	DDFunHandle RegFunHandle;

LifeCallActor.cpp

void ALifeCallActor::DDTick(float DeltaSeconds)
{
	
	
	if (TimeCounter < 3) {

	}
	else if (TimeCounter == 3) {
		// 注释掉,否则没法正常测试
		//DDDestroy();

	}
}

void ALifeCallActor::DDRegister()
{
	Super::DDRegister();

	// 注册方法句柄,目标为 Player 模组下的 DDMessage 下的 MsgQuene,调用名为 RegCall
	RegFunHandle = RegisterFunPort<int32, FString>((int32)ERCGameModule::Player, "RegCall", [this](FString InfoStr) { return RegTest(InfoStr); });
}

int32 ALifeCallActor::RegTest(FString InfoStr)
{
	DDH::Debug(0.f) << "RegCall --> " << InfoStr << DDH::Endl();
	return 123;
}

编译后,打开 ReflectActor_BP,将其细节面板的 ModuleName 改成 Player(确保调用句柄和方法句柄注册的是同一个 DDMessage)。运行游戏,可以看到左上输出如下,说明我们的注册事件系统编写好了。输出三条的原因是场景内有三个 LifeCallActor_BP 的实例。

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

接下来我们再试一下注销方法句柄。

ReflectActor.cpp

void AReflectActor::DDTick(float DeltaSeconds)
{
	Super::DDTick(DeltaSeconds);

	// 测试完毕后注释掉
	DDH::Debug(0.f) << RegCallHandle.Execute(FString::FromInt(TimeCounter++)) << DDH::Endl();

	/*
	if (TimeCounter == 450)
		RegCallHandle.UnRegister();
	*/
}

LifeCallActor.cpp

void ALifeCallActor::DDTick(float DeltaSeconds)
{
	Super::DDTick(DeltaSeconds);

	TimeCounter++;

	if (TimeCounter < 3) {

	}
	else if (TimeCounter == 450) {
		
		// 测试完毕后注释掉
		RegFunHandle.UnRegister();

	}
}

编译后运行游戏,如果跟上面的动图一样,那就说明没有问题。

最后我们来完善一下边边角角的逻辑。

LifeCallActor.cpp

void ALifeCallActor::DDUnRegister()
{
	
	// 如果有设置调用接口与方法接口,建议在 DDUnRegister 写上注销方法
	RegFunHandle.UnRegister();
}

如果有注册到调用句柄和方法句柄才注销,否则就跳过。

DDTypes.h


struct DDMsgQuene
{
	
	void UnRegisterCallPort(FName CallName)
	{
		// 添加
		if (!MsgQuene.Contains(CallName))
			return;
		DDMsgNode* MsgNode = MsgQuene.Find(CallName);
		MsgNode->CallCount--;
		if (MsgNode->CallCount <= 0) {
			MsgNode->ClearNode();
			MsgQuene.Remove(CallName);
		}
	}
	void UnRegisterFunPort(FName CallName, int32 FunID)
	{
		// 修改
		if (MsgQuene.Contains(CallName))
			MsgQuene.Find(CallName)->UnRegisterFun(FunID);


	}

};

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