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

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

  • 34. 协程宏定义分块
  • 35. 协程宏定义封装
  • 36. 整合协程到框架
    • 挂起条件封装到宏定义
    • 整合到框架

34. 协程宏定义分块

我们前面已经在一个类里面实现了一套可行的协程系统,接下来我们需要通过宏来将它们变得更加方便可用,不必每次都写这么多代码。

将 CoroActor 头文件里的委托声明语句以及两个结构体全复制到 DDTypes 下,改成通用的结构。下面只列出需要更改的代码。

DDTypes.h

#pragma region Coroutine

DECLARE_DELEGATE_RetVal(bool, FCoroCondition)

struct DDCoroNode	// 添加 DD 前缀
{
	
	DDCoroNode() : IsActive(false) {}
	
};

struct DDCoroTask	// 添加 DD 前缀
{

	TArray<DDCoroNode*> CoroStack;

	DDCoroTask(int32 CoroCount)
	{
		for (int i = 0; i <= CoroCount; ++i)
			CoroStack.Push(new DDCoroNode());
	}

	virtual ~DDCoroTask()
	{
	}
};

#pragma endregion

来到 CoroActor 的头文件,去掉原来声明的委托,并添加一个用于测试宏的测试方法三。

CoroActor.h

// 删掉这个委托声明,因为 DDTypes 里已经有了
//DECLARE_DELEGATE_RetVal(bool, FCoroCondition)

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

protected:

	DDCoroTask* CoroTestThree();
};

我们需要将先前的协程系统代码按功能来分块,然后再将它们逐个转换为宏定义。

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

UE4运用C++和框架开发坦克大战教程笔记(十一)(第34~36集)_第1张图片

目前我们先不创建 DDCoroBegin.h 和 DDYieldReady.h,留到下一节课再处理。本节课先理解如何分块以及块各自的职责。

CoroActor.cpp

DDCoroTask* ACoroActor::CoroTestThree()
{
// 协程参数区块
#pragma region DDCORO_PARAM
	struct DGCoroTask : public DDCoroTask
	{
		ACoroActor* D;
		DGCoroTask(ACoroActor* Data, int32 CoroCount) : DDCoroTask(CoroCount) { D = Data; }
#pragma endregion


// 此处用来定义类变量,需要保存状态字变量


#define DDYIELD_COUNT -1	// 保存挂起节点数

// Work 方法开头
#pragma region DDCORO_WORK_START
		virtual void Work(float DeltaTime) override
		{
			goto DDCORO_LABEL_PICKER;
		DDCORO_LABEL_START:
#pragma endregion

// 协程方法逻辑
#pragma region CoroFunCode

// 理论上有 n 个协程节点就需要写 3n + 1 行代码,这里只作为演示,只写两个协程节点的情况
#if DDYIELD_COUNT == -1
#define DDYIELD_COUNT 0
		DDCORO_LABEL_0: 
#elif DDYIELD_COUNT == 0
#define DDYIELD_COUNT 1
		DDCORO_LABEL_1:
#endif
		
			if (CoroStack[DDYIELD_COUNT]->UpdateOperate(&(D->IsCoroPause)))
				goto DDCORO_LABEL_END;

// 每个协程节点之前都要添加这一段
#if DDYIELD_COUNT == -1
#define DDYIELD_COUNT 0
		DDCORO_LABEL_0: 
#elif DDYIELD_COUNT == 0
#define DDYIELD_COUNT 1
		DDCORO_LABEL_1:
#endif
			if (CoroStack[DDYIELD_COUNT]->UpdateOperate(10))
				goto DDCORO_LABEL_END;

#pragma endregion

// Work 方法中间,END 承接前面协程方法逻辑,Picker 承接后面跳转逻辑
#pragma region DDCORO_WORK_MIDDLE
			goto DDCORO_LABEL_END;
		DDCORO_LABEL_PICKER:
#pragma endregion

// 协程条件跳转代码
#pragma region CoroPicker
// 理论上有 n 个节点就需要写 n(n+1)/2 行代码,此处为了演示只写两个协程节点的情况
#if DDYIELD_COUNT == 0
			if (CoroStack[0]->IsActive) goto DDCORO_LABEL_0;
			
#elif DDYIELD_COUNT == 1
			if (CoroStack[0]->IsActive) goto DDCORO_LABEL_0;
			if (CoroStack[1]->IsActive) goto DDCORO_LABEL_1;
#endif	
#pragma endregion

// Work 方法结尾
#pragma region DDCORO_WORK_END
			goto DDCORO_LABEL_START;
	
		DDCORO_LABEL_END:
			;
		}
	};

	return new DGCoroTask(this, DDYIELD_COUNT);
#pragma endregion
}

可见,一个 region 就是我们分的一个块,我们目前将协程系统分成了 6 个块,下节课会对这些块继续进行简化和封装。

35. 协程宏定义封装

我们首先在 DDDefine 里封装这 4 个块:

  1. 参数区域 DDCORO_PARAM
  2. Work方法开头 DDCORO_WORK_START
  3. Work方法中部 DDCORO_WORK_MIDDLE
  4. Work方法结尾 DDCORO_WORK_END

并且后面需要嵌套定义宏,所以我们还会创建三个头文件来存放这些嵌套定义宏。

DDDefine.h

// 参数区域宏定义
#define DDCORO_PARAM(UserClass); \
struct DGCoroTask : public DDCoroTask \
{ \
	UserClass* D; \
	DGCoroTask(UserClass* Data, int32 CoroCount) : DDCoroTask(CoroCount) { D = Data; }

// Work 方法开头
#define DDCORO_WORK_START \
virtual void Work(float DeltaTime) override \
{ \
	goto DDCORO_LABEL_PICKER; \
DDCORO_LABEL_START:

// Work 方法中间
#define DDCORO_WORK_MIDDLE \
	goto DDCORO_LABEL_END; \
DDCORO_LABEL_PICKER:

// Work 方法结尾
#define DDCORO_WORK_END \
		goto DDCORO_LABEL_START; \
	DDCORO_LABEL_END: \
		; \
	} \
}; \
return new DGCoroTask(this, DDYIELD_COUNT);

// 直接将头文件路径定义为宏,这样在用的时候可以直接通过 #include 来引入
#define DDCORO_BEGIN() "DataDriven/Public/DDCommon/DDCoroBegin.h" 

#define DDCORO_END() "DataDriven/Public/DDCommon/DDCoroEnd.h"

#define DDYIELD_READY() "DataDriven/Public/DDCommon/DDYieldReady.h"

直接在项目文件夹的 Plugins/DataDriven/Source/DataDriven/Public/DDCommon 路径下创建三个文本文档,分别取名为 DDCoroBegin.hDDCoroEnd.hDDYieldReady.h

随后在 VS 里对着同路径下的 DDCommon 文件夹右键,选择添加现有项,选中我们刚刚新建的三个头文件。

DDCoroBegin 包括了 DDYIELD_COUNT 的初始化以及 DDCORO_WORK_START (Work 方法开头)

DDCoroBegin.h

#define DDYIELD_COUNT -1
#ifdef DDCORO_WORK_START
	DDCORO_WORK_START
#endif

DDCoroEnd 则包括 DDCORO_WORK_MIDDLE、上节课的 region CoroPicker 里面的跳转逻辑(写到存在 64 个协程节点的情况)以及 DDCORO_WORK_END

跳转逻辑部分太多了,建议是在老师写好的 DDCoroEnd.h 里复制过来,此处写小部分用作示例

DDCoroEnd.h

#ifdef DDCORO_WORK_MIDDLE
DDCORO_WORK_MIDDLE
#endif

// 建议此处开始直接复制老师写好的内容
#ifdef DDYIELD_COUNT
#	if DDYIELD_COUNT == -1
#		error DDYIELD_COUNT Is Less Zero	// 不允许存在没有协程节点的情况
#if DDYIELD_COUNT == 0
			if (CoroStack[0]->IsActive) goto DDCORO_LABEL_0;
#elif DDYIELD_COUNT == 1
			if (CoroStack[0]->IsActive) goto DDCORO_LABEL_0;
			if (CoroStack[1]->IsActive) goto DDCORO_LABEL_1;
#elif DDYIELD_COUNT == 2
			if (CoroStack[0]->IsActive) goto DDCORO_LABEL_0;
			if (CoroStack[1]->IsActive) goto DDCORO_LABEL_1;
			if (CoroStack[2]->IsActive) goto DDCORO_LABEL_2;			
// ... 复制老师写好的内容到大概2153行

// 在这个 error 之前必须要已经引入协程系统的开头,所以此处的作用是确保协程系统的代码按顺序排列
#error No Include DDCORO_BEGIN()
#endif
#ifdef DDCORO_WORK_END
DDCORO_WORK_END
#endif
#undef DDYIELD_COUNT	// 起保险作用,毕竟前面已经到达协程系统的末尾了

DDYieldReady 包括上一节课 region CoroFunCode 内定义标签(写到存在 64 个协程节点的情况)。

这里也最好复制老师已经写好的部分过来,此处就写小部分用于示例

DDYieldReady.h

// 从此处开始复制
#ifdef DDYIELD_COUNT
#if DDYIELD_COUNT == -1
#define DDYIELD_COUNT 0
		DDCORO_LABEL_0: 
#elif DDYIELD_COUNT == 0
#define DDYIELD_COUNT 1
		DDCORO_LABEL_1:
// ...复制到这里是 195 行

#error No Include DDCORO_BEGIN()	// 确保协程系统的代码按顺序排列
#endif

本节课的最后,来到 CoroActor.cpp。先将原来的 CoroTestThree() 方法复制一份放到底下,用宏隐藏起来,后续可用于参考。

然后将之前写的协程系统相关内容用刚刚写好的嵌套宏来替换掉。

CoroActor.cpp

DDCoroTask* ACoroActor::CoroTestThree()
{
	// 协程参数区块
	DDCORO_PARAM(ACoroActor);

// 此处用来定义类变量,需要保存状态字变量

#include DDCORO_BEGIN()

// 协程方法逻辑
#pragma region CoroFunCode

#include DDYIELD_READY()
			if (CoroStack[DDYIELD_COUNT]->UpdateOperate(&(D->IsCoroPause)))
				goto DDCORO_LABEL_END;

#include DDYIELD_READY()
			if (CoroStack[DDYIELD_COUNT]->UpdateOperate(10))
				goto DDCORO_LABEL_END;

#pragma endregion

#include DDCORO_END()
}


#if 0

	// CoroTestThree() 原本的复制代码

#endif

可见,替换完毕后,整个协程系统分为了可读性比较好的 4 个部分。

笔者将老师在本集课程讲的内容按自己的理解进行了排列,实际上结合老师的课程视频一起看会更加易懂一些。

36. 整合协程到框架

挂起条件封装到宏定义

我们已经将协程系统的流程用宏定义封装好了,还剩下最后的挂起条件需要封装。(挂起条件对应第 33 集课程)

DDDefine.h

// 挂起帧
#define DDYIELD_RETURN_TICK(Tick); \
if (CoroStack[DDYIELD_COUNT]->UpdateOperate(Tick)) \
	goto DDCORO_LABEL_END;

// 挂起秒
#define DDYIELD_RETURN_SECOND(Time); \
if (CoroStack[DDYIELD_COUNT]->UpdateOperate(DeltaTime, Time)) \
	goto DDCORO_LABEL_END;

// 挂起到 Bool 指针为 false
#define DDYIELD_RETURN_BOOL(Param); \
if (CoroStack[DDYIELD_COUNT]->UpdateOperate(Param)) \
	goto DDCORO_LABEL_END;

// 挂起到函数指针返回 false
#define DDYIELD_RETURN_FUNC(UserClass, UserFunc); \
if (CoroStack[DDYIELD_COUNT]->UpdateOperate(UserClass, UserFunc)) \
	goto DDCORO_LABEL_END;

// 挂起到 Lambda 表达式返回 false
#define DDYIELD_RETURN_LAMB(Expression); \
if (CoroStack[DDYIELD_COUNT]->UpdateOperate([this](){ return Expression; })) \
	goto DDCORO_LABEL_END;

// 直接终止协程
#define DDYIELD_RETURN_STOP(); \
if (CoroStack[DDYIELD_COUNT]->UpdateOperate()) \
	goto DDCORO_LABEL_END;

然后我们来测试一下用宏封装完的挂起条件是否可以正常使用。

之前测试用的代码统统都要依葫芦画瓢地使用在 DDTypes.h 里写好的通用代码再写一遍。

CoroActor.h

protected:

	// 给原来的方法添加一个 Temp 前缀,原名会在以后整合的时候使用到
	void TempStartCoroutine(CoroTask* InTask);

	void DDStartCoroutine(DDCoroTask* InTask);

protected:

	TArray<DDCoroTask*> DDTaskList;

CoroActor.cpp

void ACoroActor::DDEnable()
{
	
	

	// 同步改名,不过不运行
	//TempStartCoroutine(CoroTestTwo());

	DDStartCoroutine(CoroTestThree());
}

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

	// 注释原来的逻辑便于参考
	/*
	TArray TempTask;

	for (int i = 0; i < TaskList.Num(); ++i) {
		TaskList[i]->Work(DeltaSeconds);
		if (TaskList[i]->IsFinish())
			TempTask.Push(TaskList[i]);
	}
	for (int i = 0; i < TempTask.Num(); ++i) {
		TaskList.Remove(TempTask[i]);
		delete TempTask[i];
	}
	*/

	// 复制一份 DDCoroTask 专属的逻辑
	TArray<DDCoroTask*> TempTask;

	for (int i = 0; i < DDTaskList.Num(); ++i) {
		DDTaskList[i]->Work(DeltaSeconds);
		if (DDTaskList[i]->IsFinish())
			TempTask.Push(DDTaskList[i]);
	}
	for (int i = 0; i < TempTask.Num(); ++i) {
		DDTaskList.Remove(TempTask[i]);
		delete TempTask[i];
	}

	TimeCounter++;
	if (TimeCounter == 500)
		IsCoroPause = false;
}

DDCoroTask* ACoroActor::CoroTestThree()
{
	// 协程参数区
	DDCORO_PARAM(ACoroActor);


	// 协程方法主体开始
#include DDCORO_BEGIN()

	DDH::Debug() << 0 << DDH::Endl();

#include DDYIELD_READY()
	DDYIELD_RETURN_BOOL(&(D->IsCoroPause));		// bool 挂起

	DDH::Debug() << 1 << DDH::Endl();
			
#include DDYIELD_READY()
	DDYIELD_RETURN_TICK(300);		// 帧挂起

	DDH::Debug() << 2 << DDH::Endl();

#include DDYIELD_READY()
	DDYIELD_RETURN_SECOND(5.f);		// 秒挂起

	DDH::Debug() << 3 << DDH::Endl();

	// 协程方法主体结束
#include DDCORO_END()
}

// 同步改名
void ACoroActor::TempStartCoroutine(CoroTask* InTask)
{
	TaskList.Push(InTask);
}

void ACoroActor::DDStartCoroutine(DDCoroTask* InTask)
{
	DDTaskList.Push(InTask);
}

编译运行,左上角输出如下,由于帧挂起的时长取决于各人电脑的帧数,就不列出来了。此时说明协程系统用宏定义封装成功。

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

整合到框架

依旧是将协程系统放到消息模块。

DDMessage.h

public:

	// 开启一个协程,返回 true 说明开启成功;返回 false 说明已经有同对象名同协程任务名的协程存在
	bool StartCoroutine(FName ObjectName, FName CoroName, DDCoroTask* CoroTask);

	// 停止一个协程,返回 true 说明停止协程成功;返回 false 说明协程早已停止
	bool StopCoroutine(FName ObjectName, FName CoroName);

	// 停止该对象的所有协程(老师把方法名字拼写错了)
	void StopAllCoroutine(FName ObjectName);
	
protected:

	// 协程序列,键 1 保存对象名,值的键 FName 对应的是协程任务的名字
	TMap<FName, TMap<FName, DDCoroTask*>> CoroStack;

DDMessage.cpp

void UDDMessage::MessageTick(float DeltaSeconds)
{
	// 处理协程
	TArray<FName> CompleteTask;		// 保存完成了的协程任务
	for (TMap<FName, TMap<FName, DDCoroTask*>>::TIterator It(CoroStack); It; ++It) {
		TArray<FName> CompleteNode;		// 保存完成了的协程名
		for (TMap<FName, DDCoroTask*>::TIterator Ih(It->Value); Ih; ++Ih) {
			Ih->Value->Work(DeltaSeconds);
			if (Ih->Value->IsFinish()) {
				delete Ih->Value;
				CompleteNode.Push(Ih->Key);
			}
		}
		for (int i = 0; i < CompleteNode.Num(); ++i)
			It->Value.Remove(CompleteNode[i]);
		if (It->Value.Num() == 0)
			CompleteTask.Push(It->Key);
	}
	
	for (int i = 0; i < CompleteTask.Num(); ++i)
		CoroStack.Remove(CompleteTask[i]);
}

bool UDDMessage::StartCoroutine(FName ObjectName, FName CoroName, DDCoroTask* CoroTask)
{
	// 如果协程序列没有这个对象名,则添加一个
	if (!CoroStack.Contains(ObjectName)) {
		TMap<FName, DDCoroTask*> NewTaskStack;
		CoroStack.Add(ObjectName, NewTaskStack);
	}
	// 如果协程序列里面对应的对象名没有协程名字,就添加目前的这个协程名字和协程任务进去
	if (!(CoroStack.Find(ObjectName)->Contains(CoroName))) {
		CoroStack.Find(ObjectName)->Add(CoroName, CoroTask);
		return true;
	}
	delete CoroTask;
	return false;
}

bool UDDMessage::StopCoroutine(FName ObjectName, FName CoroName)
{
	if (CoroStack.Contains(ObjectName) && CoroStack.Find(ObjectName)->Find(CoroName)) {
		DDCoroTask* CoroTask = *(CoroStack.Find(ObjectName)->Find(CoroName));
		CoroStack.Find(ObjectName)->Remove(CoroName);
		if (CoroStack.Find(ObjectName)->Num() == 0)
			CoroStack.Remove(ObjectName);
		delete CoroTask;
		return true;
	}
	return false;
}

void UDDMessage::StopAllCoroutine(FName ObjectName)
{
	if (CoroStack.Contains(ObjectName)) {
		for (TMap<FName, DDCoroTask*>::TIterator It(*CoroStack.Find(ObjectName)); It; ++It) 
			delete It->Value;
		CoroStack.Remove(ObjectName);
	}
}

我们希望协程系统跟对象的交互路线是 DDMessage – DDModule – DDOO – 对象,所以在这条路线上要建立一条调用链。

DDModule.h

public:

	// 开启一个协程,返回 true 说明开启成功;返回 false 说明已经有同对象名同协程任务名的协程存在
	bool StartCoroutine(FName ObjectName, FName CoroName, DDCoroTask* CoroTask);

	// 停止一个协程,返回 true 说明停止协程成功;返回 false 说明协程早已停止
	bool StopCoroutine(FName ObjectName, FName CoroName);

	// 停止该对象的所有协程
	void StopAllCoroutine(FName ObjectName);

DDModule.cpp

bool UDDModule::StartCoroutine(FName ObjectName, FName CoroName, DDCoroTask* CoroTask)
{
	return Message->StartCoroutine(ObjectName, CoroName, CoroTask);
}

bool UDDModule::StopCoroutine(FName ObjectName, FName CoroName)
{
	return Message->StopCoroutine(ObjectName, CoroName);
}

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

DDOO 的代码稍有不同,因为 DDOO 本身就保存有 ObjectName,所以不需要通过参数传递。

DDOO.h

protected:

	// 开启一个协程,返回 true 说明开启成功;返回 false 说明已经有同对象名同协程任务名的协程存在
	bool StartCoroutine(FName CoroName, DDCoroTask* CoroTask);

	// 停止一个协程,返回 true 说明停止协程成功;返回 false 说明协程早已停止
	bool StopCoroutine(FName CoroName);

	// 停止该对象的所有协程
	void StopAllCoroutine();

DDOO.cpp

bool IDDOO::StartCoroutine(FName CoroName, DDCoroTask* CoroTask)
{
	return IModule->StartCoroutine(GetObjectName(), CoroName, CoroTask);
}

bool IDDOO::StopCoroutine(FName CoroName)
{
	return IModule->StopCoroutine(GetObjectName(), CoroName);
}

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

最后我们测试一下整合好的协程系统是否能正常使用。

CoroActor.cpp

void ACoroActor::DDEnable()
{


	// 修改原来的调用语句如下
	DDH::Debug() << "StartCoroutine --> " << StartCoroutine("CoroTestThree", CoroTestThree()) << DDH::Endl();
}

DDCoroTask* ACoroActor::CoroTestThree()
{
	
	DDCORO_PARAM(ACoroActor);

	// 定义
	int32 i;
	int32 j;

#include DDCORO_BEGIN()

	DDH::Debug() << 0 << DDH::Endl();

#include DDYIELD_READY()
	DDYIELD_RETURN_BOOL(&(D->IsCoroPause));	// bool 挂起

	DDH::Debug() << 1 << DDH::Endl();
			
#include DDYIELD_READY()
	DDYIELD_RETURN_LAMB(D->PauseLambda());	// Lambda 返回的 bool 挂起

	DDH::Debug() << 2 << DDH::Endl();

#include DDYIELD_READY()
	DDYIELD_RETURN_FUNC(D, &ACoroActor::PauseFunPtr);	// 委托方法返回的 bool 挂起

	DDH::Debug() << 3 << DDH::Endl();

	// 添加一段 for 循环	
	for (i = 0; i < 10; i++) {
		for (j = 0; j < 5; j++) {
#include DDYIELD_READY()
			DDYIELD_RETURN_TICK(20);	// 帧挂起

			DDH::Debug() << i << j << DDH::Endl();

			if (i * 10 + j > 30) {
#include DDYIELD_READY()
				DDYIELD_RETURN_STOP();	// 停止协程
			}
		}
	}

// 下面这里不会运行,因为上面存在停止协程的语句
#include DDYIELD_READY()
	DDYIELD_RETURN_SECOND(5.f);		// 秒挂起

	DDH::Debug() << 4 << DDH::Endl();

#include DDCORO_END()
}

编译后运行,左上角 Debug 语句输出如下:

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

最后再简单地示范下如何使用这个协程系统。

CoroActor.h

protected:

	DDCoroTask* CoroFunc();

CoroActor.cpp

void ACoroActor::DDEnable()
{


	// 启动协程需要传入名字和指定协程方法
	//DDH::Debug() << "StartCoroutine --> " << StartCoroutine("CoroFunc", CoroFunc()) << DDH::Endl();
}

DDCoroTask* ACoroActor::CoroFunc()
{
	// 协程参数区
	DDCORO_PARAM(ACoroActor);

	// 协程方法变量

	// 协程方法主体开始
#include DDCORO_BEGIN()

	// 协程方法主体逻辑代码
#include DDYIELD_READY()
	DDYIELD_RETURN_SECOND(5.f);		// 挂起 5 秒

	// 协程方法主体结束
#include DDCORO_END()
}

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