UE4运用C++和框架开发坦克大战教程笔记(十一)(第34~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 文档:
目前我们先不创建 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 个块:
- 参数区域
DDCORO_PARAM
- Work方法开头
DDCORO_WORK_START
- Work方法中部
DDCORO_WORK_MIDDLE
- 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.h、DDCoroEnd.h 和 DDYieldReady.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<CoroTask*> 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);
}
编译运行,左上角输出如下,由于帧挂起的时长取决于各人电脑的帧数,就不列出来了。此时说明协程系统用宏定义封装成功。
整合到框架
依旧是将协程系统放到消息模块。
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 语句输出如下:
最后再简单地示范下如何使用这个协程系统。
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()
}