Let's Enjoy Unreal Engine

Unreal Engineを使って遊んでみましょう

UE5 コードフローの流れをわかりやすくするControlFlows

UE5.0から新規実装されているプラグインに『ControlFlows』と呼ばれるものがあります。

このプラグインの使い方について、どこにも全く情報がなく使い方もわかりませんでしたが、実はLyra Gameサンプル内で使われていることがわかり、個人的に使い方を調べてみました。Lyraについては公式にも解説がありますので、そちらを御参考ください。

docs.unrealengine.com

このプラグインC++で使うことが前提となっており、わかりやすく説明すると
『同期または非同期に実装された関数をキューに入れて管理、実行するシステム』とのことです。
このシステムは今のところ非純粋(non-const)な関数しかサポートしていません。

ControlFlowsを使うことで、複雑なステップやステートを持つようなロジックを作る際に、コードを読み易くシンプルな作りを可能にし、同期処理や非同期処理に関係なく、ステップを明確にすることができるようになるため後からでも構造の変更に対応しやすいというのが最大のメリットかもしれません。今回はシンプルな使い方をしていますが、Lyraの中に実際に使われているサンプルコードがありますので、そちらも参考にしてみるといいと思います。

ControlFlowsの使い方

まずはControlFlowsプラグイン自体を有効にしてからエディターを再起動しておきましょう。

次にプロジェクトの『***.Build.cs』ファイルにControlFlowsのモジュールを追加しておきます。

public class ControlFlowTest : ModuleRules
{
	public ControlFlowTest(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		// ControlFlowsを追加
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "ControlFlows" });

		PrivateDependencyModuleNames.AddRange(new string[] {  });
	}
}

これでControlFlowsがC++コード内で使えるようになりました。次に通常のAActorを継承したクラスを作成します。AActor以外のクラスで作ってしまっても問題ありません。今回はControlFlowActorという名前でコードを追加しています。

まずは基本的な使い方を解説します。

void AControlFlowActor::BasicControlFlow()
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("BasicFlow"))
		.QueueStep(this, &ThisClass::FlowStep_FirstStep)
		.QueueStep(this, &ThisClass::FlowStep_SecondStep)
		.QueueStep(this, &ThisClass::FlowStep_ThirdStep);

	Flow.ExecuteFlow();
}

FControlFlowを作成するために、FControlFlowStatics::Createという関数でインスタンスを作成します。そのインスタンスから直接QueueStep関数を呼び出して、第2引数にはステップする関数を指定します。QueueStep関数は入れ子のように使うことができるので、好きな数だけ関数を追加します。

この関数の引数は以下のように指定が可能です。

void MyFunction(...);
void MyFunction(FControlFlowNodeRef FlowHandle, ...);
void MyFunction(TSharedRef<FControlFlow> Subflow, ...);
void MyFunction(int32(TSharedRef<FControlFlowBranch> Branch, ...);

それぞれのQueueStepに追加された関数はどのように実装することも可能ですが、引数で受け取ったFControlFlowNodeRefを使ってContinueFlow関数やCancelFlow関数でフローを自由に進めることができます。以下は実際に実装してみた例となります。

void AControlFlowActor::FlowStep_FirstStep(FControlFlowNodeRef SubFlow)
{
	UE_LOG(LogTemp, Log, TEXT("FirstStep"));
	SubFlow->ContinueFlow();
}

void AControlFlowActor::FlowStep_SecondStep(FControlFlowNodeRef SubFlow)
{
	UE_LOG(LogTemp, Log, TEXT("SecondStep"));
	SubFlow->CancelFlow();
}

void AControlFlowActor::FlowStep_ThirdStep(FControlFlowNodeRef SubFlow)
{
	UE_LOG(LogTemp, Log, TEXT("ThirdStep"));
	SubFlow->ContinueFlow();
}

BasicControlFlowを実行すると、アウトプットログには以下のように出力されます。

LogTemp: FirstStep
LogTemp: SecondStep

最初のFirstStepはContinueFlowが実行されますので、次のステップが実行されますが、SecondStepではCancelFlowが実行されることになりますので、その次のThirdStepは実行されません。一度実行するとQueueから関数が削除されますので、もう一度同じ関数から再開といった使い方はできません。

またステップを進めずに一度ステップ用の関数を出てからContinueFlowを呼び出したい場合には、FControlFlowNodeRefの値をメンバー変数に保存しておき、別の関数でContinueFlowを呼び出すことでステップを進めることも可能です。実際Lyraではそのように実装されています。

おおまかな使い方ではそんなところとなります。次は少し応用した使い方です。

BranchFlow

FControlFlowBranchクラスのAddOrGetBranch関数でキーとなるインデックスを渡して、QueueStep関数キューを追加します。そしてFControlFlowクラスがもつBranchFlowクラスのラムダ式内のreturn値で返す値を実行する関数として指定することができます。

以下が具体的なコードとなります。

void AControlFlowActor::BranchControlFlow()
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("BranchFlow"));
	Flow.BranchFlow([this](TSharedRef<FControlFlowBranch> Branch)
	{
		Branch->AddOrGetBranch(1).QueueStep(this, &ThisClass::FlowStep_FirstStep);
		Branch->AddOrGetBranch(2).QueueStep(this, &ThisClass::FlowStep_SecondStep);
		Branch->AddOrGetBranch(3).QueueStep(this, &ThisClass::FlowStep_ThirdStep);

		return 3;
	});

	Flow.ExecuteFlow();
}

この関数を実行した場合以下のような出力となります。

LogTemp: ThirdStep

QueueStepに追加した3番目の関数のみ実行されます。ContinueFlowを呼び出してもその次のステップ関数は呼び出しません。Branchということで分岐可能なフローを構築できることを期待しますが、ちょっと意味合いとしては違うような気がします。どうにも使い方は難しい気がします。

ForkFlow

こちらもFConcurrentControlFlowsクラスのAddOrGetFlow関数でキーとなるインデックスを渡して、QueueStep関数でキューを追加します。そしてFControlFlowクラスがもつForkFlowクラスのラムダ式内で関数をキューに追加すると、追加された関数全て同時に実行します。名前にConcurrentとついていて、並列実行されることを期待してしまいますがこの関数はあくまでもシングルスレッドで動作するようです。TODOコメントでは将来的にマルチスレッドライクのように動作させるようになるとのことです。

以下が具体的なコードとなります。

void AControlFlowActor::ForkControlFlow()
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("ForkFlow"));
	Flow.ForkFlow([this](TSharedRef<FConcurrentControlFlows> Concurrent)
	{
		Concurrent->AddOrGetFlow(1).QueueStep(this, &ThisClass::FlowStep_FirstStep);
		Concurrent->AddOrGetFlow(2).QueueStep(this, &ThisClass::FlowStep_SecondStep);
		Concurrent->AddOrGetFlow(3).QueueStep(this, &ThisClass::FlowStep_ThirdStep);
	});

	Flow.ExecuteFlow();
}

これを実行すると以下のような出力となります。

LogTemp: FirstStep
LogTemp: SecondStep
LogTemp: ThirdStep

全てのステップ関数が実行されていることがわかります。ただBranch同様、もう少し直感的に使える方が嬉しいですね。TODOが色々あるようなので、今後の進化に期待したいと思います。

まとめ

今回はControlFlowsと呼ばれるシステムを色々と触ってみました。正直ネットにも全く情報がなくて、どうやって全く使うのかがわかりませんでしたが、たまたまLyraの中で使われていたので興味を持ち触ったみたことが切っ掛けです。もしもっと良い使い方があるよという方がいましたらぜひ教えてください。個人的にはもう少しわかりやすく使えるようにしてもらいたいです。

最後に今回作った使ったコードをまとめておきますので、参考にしてください。

ControlFlowActor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ControlFlowNode.h"

#include "ControlFlowActor.generated.h"

class FControlFlow;

UCLASS()
class CONTROLFLOWTEST_API AControlFlowActor : public AActor
{
	GENERATED_BODY()
	
public:	
	AControlFlowActor();

	UFUNCTION(BlueprintCallable)
	void BasicControlFlow();

	UFUNCTION(BlueprintCallable)
	void BranchControlFlow();

	UFUNCTION(BlueprintCallable)
	void ForkControlFlow();

protected:
	virtual void BeginPlay() override;

	void FlowStep_FirstStep(FControlFlowNodeRef SubFlow);
	void FlowStep_SecondStep(FControlFlowNodeRef SubFlow);
	void FlowStep_ThirdStep(FControlFlowNodeRef SubFlow);

public:	
	virtual void Tick(float DeltaTime) override;

};

ControlFlowActor.cpp

#include "ControlFlowActor.h"

#include "ControlFlow.h"
#include "ControlFlowBranch.h"
#include "ControlFlowConcurrency.h"
#include "ControlFlowManager.h"

AControlFlowActor::AControlFlowActor()
{
	PrimaryActorTick.bCanEverTick = false;
}

void AControlFlowActor::BeginPlay()
{
	Super::BeginPlay();
}

void AControlFlowActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void AControlFlowActor::BasicControlFlow()
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("BasicFlow"))
		.QueueStep(this, &ThisClass::FlowStep_FirstStep)
		.QueueStep(this, &ThisClass::FlowStep_SecondStep)
		.QueueStep(this, &ThisClass::FlowStep_ThirdStep);

	Flow.ExecuteFlow();
}

void AControlFlowActor::BranchControlFlow()
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("BranchFlow"));
	Flow.BranchFlow([this](TSharedRef<FControlFlowBranch> Branch)
	{
		Branch->AddOrGetBranch(1).QueueStep(this, &ThisClass::FlowStep_FirstStep);
		Branch->AddOrGetBranch(2).QueueStep(this, &ThisClass::FlowStep_SecondStep);
		Branch->AddOrGetBranch(3).QueueStep(this, &ThisClass::FlowStep_ThirdStep);

		return 3;
	});

	Flow.ExecuteFlow();
}

void AControlFlowActor::ForkControlFlow()
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("ForkFlow"));
	Flow.ForkFlow([this](TSharedRef<FConcurrentControlFlows> Concurrent)
	{
		Concurrent->AddOrGetFlow(1).QueueStep(this, &ThisClass::FlowStep_FirstStep);
		Concurrent->AddOrGetFlow(2).QueueStep(this, &ThisClass::FlowStep_SecondStep);
		Concurrent->AddOrGetFlow(3).QueueStep(this, &ThisClass::FlowStep_ThirdStep);
	});

	Flow.ExecuteFlow();
}

void AControlFlowActor::FlowStep_FirstStep(FControlFlowNodeRef SubFlow)
{
	UE_LOG(LogTemp, Log, TEXT("FirstStep"));
	SubFlow->ContinueFlow();
}

void AControlFlowActor::FlowStep_SecondStep(FControlFlowNodeRef SubFlow)
{
	UE_LOG(LogTemp, Log, TEXT("SecondStep"));
	SubFlow->CancelFlow();
}

void AControlFlowActor::FlowStep_ThirdStep(FControlFlowNodeRef SubFlow)
{
	UE_LOG(LogTemp, Log, TEXT("ThirdStep"));
	SubFlow->ContinueFlow();
}