下图截取自梁迪老师准备的框架文档。
有关协程系统的参考链接:
【Unity】协程的调用停止及yield return的使用
Unity——协程(暂停功能)
稍微了解了 Unity 的协程功能后,要想在 Ue4 用 C++ 实现,则有以下要点:
其实先前的第 10 课讲的 LatentAction 潜在事件跟本框架的协程系统也有一些类似之处,读者可以回去复习一下概念。
本节课的目标是先实现一个能够延时 3 秒的延时节点,作为示例。
创建一个以 DDActor 为基类的 C++ 类,目标模块为项目,直接在默认路径下创建,取名为 CoroActor。Coro 就是 Coroutine(协同程序)的缩写。
CoroActor.h
// 延时节点
struct CoroNode
{
// 激活状态
bool IsActive;
// 剩余时间
float RemainTime;
// 构造函数
CoroNode() : IsActive(false) {}
// 延迟秒条件,帧更新函数,返回 true,就继续挂起;返回 false,执行后续代码
bool UpdateOperate(float DeltaTime, float SpaceTime)
{
if (!IsActive) {
RemainTime = SpaceTime;
IsActive = true;
return true;
}
else {
RemainTime -= DeltaTime;
if (RemainTime > 0)
return true;
else {
IsActive = false;
return false;
}
}
}
};
UCLASS()
class RACECARFRAME_API ACoroActor : public ADDActor
{
GENERATED_BODY()
public:
ACoroActor();
virtual void DDEnable() override;
virtual void DDTick(float DeltaSeconds) override;
protected:
void CoroTestOne(float DeltaSeconds);
protected:
CoroNode TimeNode;
};
CoroActor.cpp
ACoroActor::ACoroActor()
{
IsAllowTickEvent = true;
}
void ACoroActor::DDEnable()
{
Super::DDEnable();
}
void ACoroActor::DDTick(float DeltaSeconds)
{
Super::DDTick(DeltaSeconds);
// 测试完毕后记得注释掉
CoroTestOne(DeltaSeconds);
}
void ACoroActor::CoroTestOne(float DeltaSeconds)
{
// 等待 3 秒
if (TimeNode.UpdateOperate(DeltaSeconds, 3.f))
goto LABEL_END;
// 运行逻辑代码
DDH::Debug() << "CoroTestOne" << DDH::Endl();
LABEL_END:
;
}
编译后,在 Blueprint 目录下创建 CoroActor 的蓝图,命名为 CoroActor_BP。将其细节面版的 ModuleName 设置为 Player,ObjectName 设置为 CoroActor。
随后将其放到场景中,运行游戏,可看见左上角每隔 3 秒就会输出一条 Debug 语句,说明延时节点实现成功。
很显然,我们前面实现的延时节点是每帧都调用目标方法,运行其中的 if 判断的,这未免太粗糙了。更理想的状态是:只需要传入一次方法名,延时 n 秒后直接运行目标方法。所以我们这节课来实现一下。
CoroActor.h
// 协程任务结构体,包含协程节点
struct CoroTask
{
CoroNode Node;
// 实际运行的帧函数
virtual void Work(float DeltaTime) {}
// 是否完结状态
bool IsFinish()
{
return !Node.IsActive;
}
};
UCLASS()
class RACECARFRAME_API ACoroActor : public ADDActor
{
GENERATED_BODY()
protected:
CoroTask* CoroTestTwo();
// 开启协程
void StartCoroutine(CoroTask* InTask);
protected:
// 协程任务数组,参与到帧函数中进行遍历
TArray<CoroTask*> TaskList;
};
CoroActor.cpp
void ACoroActor::DDEnable()
{
StartCoroutine(CoroTestTwo());
}
void ACoroActor::DDTick(float 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];
}
}
CoroTask* ACoroActor::CoroTestTwo()
{
// 在方法内定义一个协程任务结构体的子结构体,重写 Work() 并在里面加入目标代码
struct ValueTask : public CoroTask
{
int32 HValue;
virtual void Work(float DeltaTime) override
{
// 运行节点内的倒计时方法
if (Node.UpdateOperate(DeltaTime, 3.f))
goto LABEL_END;
DDH::Debug() << "CoroTestTwo" << DDH::Endl();
LABEL_END:
;
}
};
return new ValueTask();
}
void ACoroActor::StartCoroutine(CoroTask* InTask)
{
TaskList.Push(InTask);
}
编译后运行游戏,3 秒后左上角输出 Debug 语句:
目前我们只有一个协程节点,所以没法实现类似 “延时 n 秒后执行协程 1,随后再延时 n 秒后执行协程 2” 的操作。所以我们接下来改写一下协程任务结构体,让它拥有多个协程节点。
CoroActor.h
struct CoroTask
{
// 多个协程节点,将原来的单个节点改成节点数组
TArray<CoroNode*> CoroStack;
// 构造函数,生成指定数量的节点
CoroTask(int32 CoroCount)
{
for (int i = 0; i <= CoroCount; ++i)
CoroStack.Push(new CoroNode());
}
// 析构函数,销毁所有节点
virtual ~CoroTask()
{
for (int i = 0; i < CoroStack.Num(); ++i)
delete CoroStack[i];
}
virtual void Work(float DeltaTime) {}
bool IsFinish()
{
// 判定所有的协程节点是否都已经结束
bool Flag = true;
for (int i = 0; i < CoroStack.Num(); ++i) {
if (CoroStack[i]->IsActive) {
Flag = false;
break;
}
}
return Flag;
}
};
CoroActor.cpp
CoroTask* ACoroActor::CoroTestTwo()
{
struct ValueTask : public CoroTask
{
// 构造函数
ValueTask(int32 CoroCount) : CoroTask(CoroCount) {}
int32 HValue;
virtual void Work(float DeltaTime) override
{
// 删掉原来 Work() 内的代码
goto LABEL_PICKER; // Picker 标签负责指派运行顺序
LABEL_START:
DDH::Debug() << 0 << DDH::Endl();
LABEL_0:
if (CoroStack[0]->UpdateOperate(DeltaTime, 2.f))
goto LABEL_END;
DDH::Debug() << 1 << DDH::Endl();
LABEL_1:
if (CoroStack[1]->UpdateOperate(DeltaTime, 5.f))
goto LABEL_END;
DDH::Debug() << 2 << DDH::Endl();
LABEL_2:
if (CoroStack[2]->UpdateOperate(DeltaTime, 3.f))
goto LABEL_END;
DDH::Debug() << 3 << DDH::Endl();
goto LABEL_END; // 在末尾添加跳转 END 标签,确保正确运行
LABEL_PICKER:
if (CoroStack[0]->IsActive) goto LABEL_0;
if (CoroStack[1]->IsActive) goto LABEL_1;
if (CoroStack[2]->IsActive) goto LABEL_2;
goto LABEL_START;
LABEL_END:
;
}
};
// 传个 2,生成 3 个协程节点
return new ValueTask(2);
}
编译后运行游戏,可见左上角按顺序和一定的时间间隔分别输出了 4 个数字,输出顺序为:开始标签 -> 节点 1 -> 节点 2 -> 节点 3
本集开始的时候梁迪老师会讲解上节课代码的运行顺序,笔者觉得只需要多看两遍 30 和 31 集的代码内容就能理清了,此处就不再赘述。
目前的协程系统已经初见雏形了,不过跟第 30 集介绍的 Unity 的协程代码依旧有些差距。接下来我们先测试一下我们目前的协程系统是否可以跳过某个节点也能够完整执行整个协程序列。
CoroActor.cpp
CoroTask* ACoroActor::CoroTestTwo()
{
// ... 省略
if (true) // 添加一个 if 语句进行测试
{
LABEL_0:
if (CoroStack[0]->UpdateOperate(DeltaTime, 2.f))
goto LABEL_END;
DDH::Debug() << 1 << DDH::Endl();
}
else
{
LABEL_1:
if (CoroStack[1]->UpdateOperate(DeltaTime, 5.f))
goto LABEL_END;
DDH::Debug() << 2 << DDH::Endl();
}
// ... 省略
}
编译后运行,可见左上角不再输出 2,说明即使跳过了一个协程节点,整个协程序列也会按顺序执行的。
同时上面的结果也说明,我们可以在协程任务结构体内通过安排好的代码逻辑,来安排协程节点序列以期望的顺序执行,包括多次执行同一个协程节点。
CoroActor.cpp
CoroTask* ACoroActor::CoroTestTwo()
{
struct ValueTask : public CoroTask
{
ValueTask(int32 CoroCount) : CoroTask(CoroCount) {}
// 所有需要保存状态的变量都需要定义成类变量
// 所有循环判断的变量都需要定义成类变量
// 更改一下
int32 i;
int32 j;
virtual void Work(float DeltaTime) override
{
goto LABEL_PICKER;
LABEL_START:
if (false) // 暂时关掉这里的 if 语句,即不执行节点 1 和 节点 2
{
LABEL_0:
if (CoroStack[0]->UpdateOperate(DeltaTime, 2.f))
goto LABEL_END;
DDH::Debug() << 1 << DDH::Endl();
//去掉此处的 } else {
LABEL_1:
if (CoroStack[1]->UpdateOperate(DeltaTime, 5.f))
goto LABEL_END;
DDH::Debug() << 2 << DDH::Endl();
}
for (i = 0; i < 5; ++i) {
for (j = 0; j < 7; ++j) {
LABEL_2:
if (CoroStack[2]->UpdateOperate(DeltaTime, 0.3f))
goto LABEL_END;
DDH::Debug() << i << j << DDH::Endl();
}
}
goto LABEL_END;
// ... 省略
}
编译后运行,可见输出如下
如果不将 i 和 j 声明为结构体局部变量,则每次输出都是 00,并且这段逻辑不会停止。因为帧执行的过程是一直调用 Work()
函数,仅在 for 语句的 init 部分里定义的 i 和 j 每次都会是 0。
接下来我们希望协程节点里的逻辑可以任意地访问所属类里面的变量和方法。不过协程任务结构体里面是没办法直接访问到所属类的变量和方法的,所以我们可以在结构体的构造函数里传入所属类的对象的指针。
我们先来到头文件定义一个方法和变量。
CoroActor.h
protected:
void EchoCoroInfo();
protected:
FString CoroStr;
CoroActor.cpp
void ACoroActor::DDEnable()
{
Super::DDEnable();
CoroStr = FString("CoroStr"); // 初始化 FString 变量
StartCoroutine(CoroTestTwo());
}
CoroTask* ACoroActor::CoroTestTwo()
{
struct ValueTask : public CoroTask
{
// 添加一个所属类的指针
ACoroActor* D;
// 初始化的同时给指针赋值
ValueTask(ACoroActor* Data, int32 CoroCount) : CoroTask(CoroCount) { D = Data; }
// ... 省略
for (i = 0; i < 2; ++i) { // 更改一下 i 和 j 的最大值,以免输出太多
for (j = 0; j < 2; ++j) {
LABEL_2:
if (CoroStack[2]->UpdateOperate(DeltaTime, 0.3f))
goto LABEL_END;
// 通过指针 D 来访问所属类的变量和方法
DDH::Debug() << D->CoroStr << DDH::Endl();
D->EchoCoroInfo();
}
}
// ... 省略
return new ValueTask(this, 2); // 补充所属类作为实参
}
void ACoroActor::EchoCoroInfo()
{
DDH::Debug() << "EchoCoroInfo" << DDH::Endl();
}
编译后运行游戏,可见输出如下,说明协程节点访问所属类的变量和方法成功了:
我们前面实现的协程节点只能根据指定秒数挂起执行,接下来我们添加更多的挂起条件,包括:
最后我们添加一个不需要参数的方法,代表它会无条件地停止这个协程序列。
CoroActor.h
// 判断条件委托
DECLARE_DELEGATE_RetVal(bool, FCoroCondition)
struct CoroNode
{
bool IsActive;
// 剩余时间,剩余帧
float RemainTime;
// 条件委托
FCoroCondition ConditionDele;
CoroNode() : IsActive(false) {}
// 延迟多少帧继续执行
bool UpdateOperate(int32 SpaceTick)
{
if (!IsActive) {
RemainTime = SpaceTick;
IsActive = true;
return true;
}
else {
RemainTime -= 1;
if (RemainTime > 0)
return true;
else {
IsActive = false;
return false;
}
}
}
// 延迟秒条件,帧更新函数,返回 true,就继续挂起;返回 false,执行后续代码
bool UpdateOperate(float DeltaTime, float SpaceTime)
{
// ... 省略
}
// bool 变量指针挂起,变量为 true 则继续;返回 false 则执行后续代码
bool UpdateOperate(bool* Condition)
{
if (!IsActive) {
IsActive = true;
return true;
}
else {
if (*Condition)
return true;
else {
IsActive = false;
return false;
}
}
}
// 委托函数挂起,函数返回 true 则继续;返回 false 则执行后续代码
// 此处的写法可参考第 6 集的 FMethodPtr 通过泛型来扩展函数传递功能
template<typename UserClass>
bool UpdateOperate(UserClass* UserObj, typename FCoroCondition::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod)
{
if (!IsActive) {
if (!ConditionDele.IsBound())
ConditionDele.BindUObject(UserObj, InMethod);
IsActive = true;
return true;
}
else {
if (ConditionDele.Execute())
return true;
else {
IsActive = false;
return false;
}
}
}
// Lambda 表达式挂起
bool UpdateOperate(TFunction<bool()> InFunction)
{
if (!IsActive) {
IsActive = true;
return true;
}
else {
if (InFunction())
return true;
else {
IsActive = false;
return false;
}
}
}
// 停止协程
bool UpdateOperate()
{
IsActive = false;
return true;
}
};
UCLASS()
class RACECARFRAME_API ACoroActor : public ADDActor
{
GENERATED_BODY()
protected:
// “根据委托绑定函数挂起” 要用到的方法
bool PauseFunPtr();
// “根据 Lambda 表达式挂起” 要用到的方法,此处老师把函数名拼写错了
bool PauseLambda();
protected:
// 判定是否挂起
bool IsCoroPause;
// 计时器
int32 TimeCounter;
};
CoroActor.cpp
void ACoroActor::DDEnable()
{
IsCoroPause = true; // 初始化
StartCoroutine(CoroTestTwo());
}
void ACoroActor::DDTick(float DeltaSeconds)
{
// 时间到了自动取消挂起
TimeCounter++;
if(TimeCounter == 500)
IsCoroPause = false;
}
CoroTask* ACoroActor::CoroTestTwo()
{
// ... 省略
// 逻辑更改如下:
virtual void Work(float DeltaTime) override
{
goto LABEL_PICKER;
LABEL_START:
DDH::Debug() << 0 << DDH::Endl();
LABEL_0:
// 测试 直接访问所属类的 bool 变量来决定挂起
if (CoroStack[0]->UpdateOperate(&(D->IsCoroPause)))
goto LABEL_END;
DDH::Debug() << 1 << DDH::Endl();
LABEL_1:
// 测试 根据委托绑定函数来决定挂起
if (CoroStack[1]->UpdateOperate(D, &ACoroActor::PauseFunPtr))
goto LABEL_END;
DDH::Debug() << 2 << DDH::Endl();
// 去掉 for
LABEL_2:
// 测试 根据 Lambda 表达式来决定挂起
if (CoroStack[2]->UpdateOperate([this]() { return D->PauseLambda(); }))
goto LABEL_END;
DDH::Debug() << 3 << DDH::Endl();
goto LABEL_END;
LABEL_PICKER:
if (CoroStack[0]->IsActive) goto LABEL_0;
if (CoroStack[1]->IsActive) goto LABEL_1;
if (CoroStack[2]->IsActive) goto LABEL_2;
goto LABEL_START;
LABEL_END:
;
}
};
return new ValueTask(this, 2);
}
bool ACoroActor::PauseFunPtr()
{
static int32 FunPtrTime = 0;
FunPtrTime++;
if (FunPtrTime == 300)
return false;
return true;
}
bool ACoroActor::PauseLambda()
{
static int32 LambdaTime = 0;
LambdaTime++;
if (LambdaTime == 600)
return false;
return true;
}
编译运行,左上角按顺序分别输出 0 1 2 3(不同帧数的电脑等待的时间不一样,但 4 个输出的中间 3 个间隔时间比例为 5:3:6,对应设置的等待帧数)
最后测试一下直接停止协程序列的方法。
CoroActor.cpp
CoroTask* ACoroActor::CoroTestTwo()
{
// ... 省略
LABEL_1:
if (CoroStack[1]->UpdateOperate()) // 去掉里面的参数,即直接停止协程
goto LABEL_END;
DDH::Debug() << 2 << DDH::Endl();
// ... 省略
}
编译运行,分别输出 0 1,说明直接停止协程序列的方法是有效的。