目录
通过一个游戏初始化流程的示例来介绍“ControlFlows”的基本使用。
步骤
一、搭建基本同步框架
1. 勾选“ControlFlows”插件
2. 新建一个空白C++类,这里命名为“ControlFlowSubSystem”
让“ControlFlowSubSystem” 继承“GameInstanceSubsystem”,然后添加反射所需代码
重写父类“ShouldCreateSubsystem”、“Initialize”和“Deinitialize”方法
3. 在Build.cs中添加“ControlFlows”模块
引入所需库
定义一个蓝图可调用的方法“InitLevel”,表示要执行的初始化流程;定义一个布尔类型变量“bIniting”用于表示当前是否处于初始化流程;再定义5个函数用于表示初始化流程的各个步骤。
“InitLevel”实现如下:
首先,通过检查 bIniting
来判断当前是否已经处于初始化过程中。如果 bIniting
为 true
,意味着初始化正在进行或者已经执行过了,此时输出一条日志信息然后直接返回,避免重复执行初始化流程。只有当 bIniting
为 false
时,才会将其设置为 true
,表示即将开始初始化流程。
第31行代码通过调用 FControlFlowStatics::Create
静态函数创建一个 FControlFlow
类型的控制流实例 Flow
。在创建过程中,传入了 this
指针和一个字符串,这个字符串作为控制流的唯一标识符。
第33~37行代码通过多次调用 Flow.QueueStep
函数,向刚创建的控制流实例中依次添加了多个需要按顺序执行的步骤。
第40行代码调用 Flow.ExecuteFlow()
函数启动控制流的执行。此时,FControlFlow
实例会按照之前添加步骤的顺序,依次调用对应的成员函数,确保整个初始化流程按照预定的顺序有条不紊地进行,直到所有步骤都执行完毕,完成整个初始化过程。
用于表示初始化流程步骤的5个函数实现如下,当执行到最后一个步骤时。将 bIniting
改为 false
,表示初始化流程已经结束。
4. 在关卡蓝图中调用“InitLevel”函数
执行结果如下,可以通过日志信息看到完整执行了整个初始化流程。
5. 为了观察每个步骤在哪一帧执行,可以通过添加如下代码实现:
运行结果如下,可以看到所有表示流程步骤的函数都是在同一帧执行的,这可能会造成游戏帧率下降,因此这并不符合我们的需求。
二、添加委托
6. 下面先创建两个委托,通过委托来向外界传递任务进度等信息。
申明两个动态多播委托类型
在第11行代码中,FControlFlows_InitProgress
是要声明的委托类型的名称,FGuid
是UE中用于表示全局唯一标识符(GUID)的类型,在这里它作为委托参数的类型,而 InitAsyncID
是给这个参数起的变量名。当委托被调用时,会传递一个 FGuid
类型的全局唯一标识符。该委托在被调用时,还会传递一个表示进度值的浮点型数据,这个值可以用来直观地展示当前异步初始化任务已经完成的比例或者进度情况。
在第12行代码中,FControlFlows_InitResult
代表所声明的委托类型名称,FGuid
与InitAsyncID
和前面的委托类似,bool
与 bResult
用于指示异步初始化任务最终是成功还是失败,直观地告知结果状态。Message
表示委托调用时还会附带更详细的关于初始化结果的说明。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FControlFlows_InitProgress, FGuid, InitAsyncID, float, ProgressValue); //进度更新
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FControlFlows_InitResult, FGuid, InitAsyncID, bool, bResult, FString, Message); //初始化结果
声明两个委托类型的成员变量,并且通过 UPROPERTY(BlueprintAssignable)
元数据标签对这两个属性进行修饰,使其具备了在蓝图系统中可被绑定具体的回调函数的特性。
7. 为表示初始化步骤的5个成员函数添加进度值输入参数
8. 设置进行“InitLocalAsset”步骤时,初始化进度为10%;进行“InitNetInfo”步骤时,初始化进度为20%;进行“InitUserInfo”步骤时,初始化进度为50%;进行“NotifyMainUI”步骤时,初始化进度为80%;进行“FinishThisInit”步骤时,初始化进度为100%;
9. 声明InitAsyncID
用于唯一标识某次初始化的异步任务
执行“InitLevel”函数进行初始化时为InitAsyncID
赋值
当进行各初始化的步骤时,通过InitProgress
委托广播当前的初始化进度,传入对应的InitAsyncID
和本步骤的当前进度值InProgressValue
,这样外部绑定了该委托的对象就能接收到进度更新情况。
在初始化流程的最后一个步骤对应的函数中,添加如下一行代码,通过InitResult
委托广播当前的初始化结果。
三、添加蓝图互动框架
10. 添加一个控件蓝图,用于显示当前初始化进度,这里命名为“WBP_ControlFlowsMainUI”
打开“WBP_ControlFlowsMainUI”,添加如下控件,主要是添加一个文本控件用于显示进度值,按钮用于调用“InitLevel”从而开始初始化
在事件构造和结构时分别绑定和解绑“InitProgress”、“InitResult”委托
重命名委托绑定的匹配函数为“UpdateProgress”和“InitResult”
匹配函数“UpdateProgress”和“InitResult”函数逻辑如下,将委托传递的GUID、进度值和初始化结果信息打印出来,并显示进度值。如果初始化成功后就将界面隐藏,如果失败就显示进度值为0.00
当按钮点击后调用“InitLevel”开始初始化
11. 在关卡蓝图中让界面显示出来
此时运行后界面如下
点击初始化按钮后打印信息如下:
此时就完成了初始化流程的同步框架实现,接下来我们希望将同步改为异步实现。
四、修改为异步框架
12. 引入所需头文件
13. 更改“InitLocalAsset”、“InitNetInfo”、“InitUserInfo”函数逻辑如下
主要通过使用AsyncTask
函数创建了一个外层的异步任务,并指定其可以在任意线程上执行。在这个异步任务的 lambda 表达式内部首先调用FPlatformProcess::Sleep(0.2)
模拟一个耗时操作,接着又创建了一个内层的异步任务,指定在游戏线程上执行后续的耗时操作,以及向外广播初始化的进度信息。
完善“InitUserInfo”逻辑如下
第81行首先获取位于项目保存目录下的文件名为 ControlFlowsUserInfo.txt
的目标文件相对路径,然后将相对路径转换为绝对路径。
第82行将初始化任务的唯一标识 InitAsyncID
转换为字符串形式赋值给 UserInitAsyncID。
第83行获取当前的日期时间并转换为 HTTP 日期格式赋值给 UserInitDataTime
。
第84~86行创建一个 TArray<FString>
类型的数组 MyStringInfo
,并将前面获取的UserInitAsyncID
和 UserInitDataTime
字符串添加进去,准备将这些信息保存到文件中。
第87行使用 FFileHelper::SaveStringArrayToFile
函数将包含用户初始化相关信息的字符串数组 MyStringInfo
保存到指定路径 UserInfoPath
的 ControlFlowsUserInfo.txt
中,并且指定了编码选项为ForceUTF8WithoutBOM
,确保文件内容以指定的编码格式存储。
编译后运行,此时当我们点击初始化按钮后,看到输出日志信息如下,可以发现初始化流程的步骤不再是一帧内执行的了。
并且在“Saved”文件夹中多了一个名为 ControlFlowsUserInfo.txt
的文件
ControlFlowsUserInfo.txt
的内容如下,包括了初始化任务的唯一标识和文件存储时间。
如果初始化流程中的某一步失败了,通过 InitResult
委托向外广播初始化失败的结果信息,第113行调用 SubFlow->CancelFlow()标识
取消当前正在执行的控制流,从而及时终止整个初始化流程。将 bIniting
变量设置为 false
,表示当前不再处于初始化过程中,同时将 InitAsyncID
重置为默认值,为下一次可能的初始化操作做好准备。
完整代码
“ControlFlowSubSystem.h”
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "ControlFlow.h"
#include "ControlFlowManager.h"
#include "ControlFlowNode.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Async/Async.h"
#include "ControlFlowSubSystem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FControlFlows_InitProgress, FGuid, InitAsyncID, float, ProgressValue); //进度更新
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FControlFlows_InitResult, FGuid, InitAsyncID, bool, bResult, FString, Message); //初始化结果
#define INITRESULT false
UCLASS()
class STUDY_API UControlFlowSubSystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
UFUNCTION(BlueprintCallable)
void InitLevel();
public:
UPROPERTY(BlueprintAssignable)
FControlFlows_InitProgress InitProgress;
UPROPERTY(BlueprintAssignable)
FControlFlows_InitResult InitResult;
protected:
bool bIniting = false;
FGuid InitAsyncID = FGuid();
void InitLocalAsset(FControlFlowNodeRef SubFlow, double InProgressValue);
void InitNetInfo(FControlFlowNodeRef SubFlow, double InProgressValue);
void InitUserInfo(FControlFlowNodeRef SubFlow, double InProgressValue);
void NotifyMainUI(FControlFlowNodeRef SubFlow, double InProgressValue);
void FinishThisInit(FControlFlowNodeRef SubFlow, double InProgressValue);
};
“ControlFlowSubSystem.cpp”
// Fill out your copyright notice in the Description page of Project Settings.
#include "ControlFlowSubSystem.h"
bool UControlFlowSubSystem::ShouldCreateSubsystem(UObject* Outer) const
{
return true;
}
void UControlFlowSubSystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
}
void UControlFlowSubSystem::Deinitialize()
{
Super::Deinitialize();
}
void UControlFlowSubSystem::InitLevel()
{
if (bIniting)
{
UE_LOG(LogTemp, Warning, TEXT("Initing..."));
return;
}
InitAsyncID = FGuid::NewGuid();
bIniting = true;
uint64 FrameIndex = GFrameCounter;
UE_LOG(LogTemp, Warning, TEXT("InitFlow -- FrameIndex: %d"), FrameIndex);
FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("ControlFlow_InitLevel"));
Flow.QueueStep(TEXT("InitLocalAsset"), this, &UControlFlowSubSystem::InitLocalAsset, 0.1);
Flow.QueueStep(TEXT("InitNetInfo"), this, &UControlFlowSubSystem::InitNetInfo, 0.2);
Flow.QueueStep(TEXT("InitUserInfo"), this, &UControlFlowSubSystem::InitUserInfo, 0.5);
Flow.QueueStep(TEXT("NotifyMainUI"), this, &UControlFlowSubSystem::NotifyMainUI, 0.8);
Flow.QueueStep(TEXT("FinishThisInit"), this, &UControlFlowSubSystem::FinishThisInit, 1.0);
UE_LOG(LogTemp, Warning, TEXT("ExecuteFlow -- FrameIndex: %d"), FrameIndex);
Flow.ExecuteFlow();
}
void UControlFlowSubSystem::InitLocalAsset(FControlFlowNodeRef SubFlow, double InProgressValue)
{
uint64 FrameIndex = GFrameCounter;
UE_LOG(LogTemp, Warning, TEXT("InitLocalAsset -- FrameIndex: %d"), FrameIndex);
AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {
FPlatformProcess::Sleep(0.2);
AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {
FPlatformProcess::Sleep(0.2);
InitProgress.Broadcast(InitAsyncID, InProgressValue);
SubFlow->ContinueFlow();
});
});
}
void UControlFlowSubSystem::InitNetInfo(FControlFlowNodeRef SubFlow, double InProgressValue)
{
uint64 FrameIndex = GFrameCounter;
UE_LOG(LogTemp, Warning, TEXT("InitNetInfo -- FrameIndex: %d"), FrameIndex);
AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {
FPlatformProcess::Sleep(0.2);
AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {
FPlatformProcess::Sleep(0.2);
InitProgress.Broadcast(InitAsyncID, InProgressValue);
SubFlow->ContinueFlow();
});
});
}
void UControlFlowSubSystem::InitUserInfo(FControlFlowNodeRef SubFlow, double InProgressValue)
{
uint64 FrameIndex = GFrameCounter;
UE_LOG(LogTemp, Warning, TEXT("InitUserInfo -- FrameIndex: %d"), FrameIndex);
AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {
FPlatformProcess::Sleep(0.2);
FString UserInfoPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()/TEXT("ControlFlowsUserInfo.txt"));
FString UserInitAsyncID = InitAsyncID.ToString();
FString UserInitDataTime = FDateTime::Now().ToHttpDate();
TArray<FString> MyStringInfo;
MyStringInfo.Add(UserInitAsyncID);
MyStringInfo.Add(UserInitDataTime);
FFileHelper::SaveStringArrayToFile(MyStringInfo, *UserInfoPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {
FPlatformProcess::Sleep(0.2);
InitProgress.Broadcast(InitAsyncID, InProgressValue);
SubFlow->ContinueFlow();
});
});
}
void UControlFlowSubSystem::NotifyMainUI(FControlFlowNodeRef SubFlow, double InProgressValue)
{
uint64 FrameIndex = GFrameCounter;
UE_LOG(LogTemp, Warning, TEXT("NotifyMainUI -- FrameIndex: %d"), FrameIndex);
if (INITRESULT)
{
InitProgress.Broadcast(InitAsyncID, InProgressValue);
SubFlow->ContinueFlow();
}
else
{
AsyncTask(ENamedThreads::GameThread, [this]() {
InitResult.Broadcast(InitAsyncID, false, TEXT("Init Failed"));
});
SubFlow->CancelFlow();
bIniting = false;
InitAsyncID = {};
}
}
void UControlFlowSubSystem::FinishThisInit(FControlFlowNodeRef SubFlow, double InProgressValue)
{
uint64 FrameIndex = GFrameCounter;
UE_LOG(LogTemp, Warning, TEXT("FinishThisInit -- FrameIndex: %d"), FrameIndex);
InitProgress.Broadcast(InitAsyncID, InProgressValue);
InitResult.Broadcast(InitAsyncID, true, TEXT("Init Success"));
SubFlow->ContinueFlow();
bIniting = false;
InitAsyncID = {};
}