【UE4】Singlecast Dynamic Delegateで、なんちゃってステートマシンを作ってみた

[1]:https://qiita.com/Rinderon/items/947b3a918ef383e65271
[2]:https://www.unrealengine.com/marketplace/en-US/product/combat-systems-constructor
[3]:https://www.unrealengine.com/ja/blog/featured-free-marketplace-content---february-2020

[前回の記事][1]の Singlecast Dynamic Delegate に Blueprint イベントをバインドする方法を応用(悪用?)して、なんちゃってステートマシンを作ってみた。

# ステートマシンというと…

ステートマシンというと、シンプルなものから大仰なものまで色々なパターンを漠然と指してしまうので、今回の目的を小さく絞っておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/0cc85924-b6a7-7907-c318-216fbc08ce29.png)

上図のような「enum で定義された複数個のステート(実質、整数のインデックス)に応じて、関数を呼び分けること」を目的とする。
これ、ステートの数が多くなると、Switch が妙に長くなり、イベントグラフがみるみるスパゲティになっていって厄介なので、何とかしたいと思っている人も多いと思う。

# まずは、完成品から

![20200704_2245_8fps.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/6357f08a-bdd0-0039-068a-5382be41b10a.gif)

> 使用したアセット: [Combat Systems - Constructor][2] ([2020年2月の無料コンテンツ][3])

このロボは以下のようなステート遷移で動いている：

- **Idleステート**
    - その場で待機。Idle モーションをする。
    - １秒たったら、次のステート(Walk)に遷移する。
- **Walkステート**
    - ひたすらまっすぐ歩く。Blendspace 1Dで作った歩きモーションをする。
    - ロボの前方にある下向きコーン状の何かが、床から外れたらブレーキをかけ停止する。
    - 速度がゼロになったら、次のステート(Turn)へ遷移する。
- **Turnステート**
    - 愚直に左に90度回る。左に90度しか回れない。
    - ２秒かけて、左に90度回り終わったら、次のステート(Idle)に遷移する。
    - 以下、繰り返し(♩)

これらのステート遷移を、Unreal C++ 作ったなんちゃってステートマシンを使って実装している。

# Blueprint のようす

## コンポーネント

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/cba0953d-017c-1c7f-1bb5-386b7da76424.png)

State Binder というのが、なんちゃってステートマシンの役割を担う。(ソースコードなどは後述)
Unreal C++ 製、Actor Component の派生、`UStateBinderComponent`。

## イベントグラフ： Event BeginPlay

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/e81f233a-e854-4e3f-a7b3-538dfdd58b4d.png)

- **BeginEvent BindStates**： Event BeginPlay から呼び出されるカスタムイベント。ただ単にスパゲティ化を避けるためだけに別イベントにわけた。
- **Sequence Then 0**： State Binder を総ステート数を指定して初期化。内部では、指定されたステートの数だけ Singlecast Dynamic Delegate を用意する。
- **Sequence Then 1**： State Binder に、Idleステート用のカスタムイベントをバインドする。２つバインドしているのは、ステート切り替え直後に呼び出される `OnEnter` デリゲートと、そのステート中毎フレーム呼び出される `OnUpdate` デリゲートがあるから。
    - このほかに、ステート切り替えで終了を迎えるステートに送られる `OnExit` デリゲートも用意したが、今回は使っていない。
- **Sequence Then 2**： Walkステート用のカスタムイベントのバインド。
- **Sequence Then 3**： Turnステート用のカスタムイベントのバインド。

## イベントグラフ： Event Tick

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/646375e0-8be5-bf0f-e390-b651248ac328.png)

State Binder の `Update` を呼び出している。
この `Update` が、現在のステートに応じたカスタムイベント(上記 BeginEvent BindStates でバインドされたもの)を呼び出している。

> State Binder (UStateBinderComponent) は Tick しない。どうせ Tick するはずのアクタから明示的に呼びだしてもらっている。（こういうやり方でチビチビ Tick の数を削っている）

## イベントグラフ／ステートイベント： Idle

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/6694d2a0-bc4b-145b-1b01-a0cec528b5d3.png)

- **OnEnter Idle**： 他のステートからこのステートに遷移した直後、または初回の `Update` 呼び出しで呼び出される。
    - ステートの経過時間をカウントする変数の初期化。
    - アニメーションの更新。Blend 1D に Speed = 0.0 を指定して、Idle モーションを再生させる。
- **OnUpdate Idle**： このステートの毎フレームの処理。単純にステートの経過時間が１秒を超えるのを待ってから、`Request Next State` で次のステートの Enum 値(≒整数値)を指定している。

## イベントグラフ／ステートイベント： Walk

仕組みは Idle ステートと同じなので、イベントグラフのスクショは割愛。

- **OnEnter Walk**： 移動速度を 0.0 に初期化.
- **OnUpdate Walk**： 下向きコーン状の何かの下のコリジョンを `Sphere Trace By Channel` でチェック。床がまだあるなら加速、床がないならブレーキ。速度がゼロ以下になったら、次のステート(Turn)へ。

## イベントグラフ／ステートイベント： Turn

仕組みは Idle ステートと同じなので、イベントグラフのスクショは割愛。

- **OnEnter Turn**： ステート経過時間のゼロクリア。左に90度回転後の角度を算出して変数に保存。その場回転アニメーションを指定。
- **OnUpdate Turn**： ２秒かけてアクタのワールド回転角度を左に90度回す (EaseInOutを使用)。２秒たったら、次のステート(Idle)へ。

# UStateBinderComponent (C++)

[前回の記事][1] ではデリゲート変数がむき出しで、Blueprint からも好き放題触ることができたが、今回は「配列で管理する必要があったこと」「バインド用の関数を用意したこと」で、`UPROPERTY` なしの private 変数として作ることができた。
これなら迂闊な事態は発生しないだろう。

## UStateBinderComponent.h

ちょっと長いのは、ステート値の int32 版と uint8 版の両方を用意しているから。
※int32 だけだと、Blueprint で Enum 値を渡すときに ToInteger 的なノードが一個挟まって邪魔だったのです。

```C++
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "StateBinderComponent.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogStateBinder, Log, All);

DECLARE_DYNAMIC_DELEGATE(FStateBinderEnterSignature);
DECLARE_DYNAMIC_DELEGATE_OneParam(FStateBinderUpdateSignature, float, InDeltaSeconds);
DECLARE_DYNAMIC_DELEGATE(FStateBinderExitSignature);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class STATEMACHINE01_API UStateBinderComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UStateBinderComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

public: // [RINDERON] blueprint callable interfaces

	// Reserves InNumStates items in state delegate array
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void Initialize(int32 InNumStates);

	// Binds enter state event (integer index version).
	// If you don't bind, just no event will be called.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void BindEnterEventByIntegerIndex(int32 InStateIndex, const FStateBinderEnterSignature& InEnterEvent);

	// Binds update state event (integer index version). (this will be called every time you call Update())
	// If you don't bind, just no event will be called.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void BindUpdateEventByIntegerIndex(int32 InStateIndex, const FStateBinderUpdateSignature& InUpdateEvent);

	// Binds exit state event (integer index version).
	// If you don't bind, just no event will be called.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void BindExitEventByIntegerIndex(int32 InStateIndex, const FStateBinderExitSignature& InExitEvent);

	// Call this function instead of TickComponent, because this component will not tick.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void Update(float InDeltaSeconds);

	// Requests next state.
	// Note that changing state is performed in Update(), not here.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		bool RequestNextStateByIntegerIndex(int32 InNextStateIndex);

	// Stops updates and exit current state.
	// Note that Update() is still running.
	// If you want to PAUSE, you just stop calling Update().
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void Exit() { RequestNextStateByIntegerIndex(InvalidStateIndex); }

	// Returns current state index integer index version
	UFUNCTION(BlueprintPure, Category = "State Binder")
		int32 GetCurrentStateIndexByIntegerIndex() const { return CurrentStateIndex; }

	// Returns number of states
	UFUNCTION(BlueprintPure, Category = "State Binder")
		int32 GetNumStates() const { return NumStates; }

	// Checks if current state index equals to InStateIndex
	UFUNCTION(BlueprintPure, Category = "State Binder")
		bool IsStateIndexEqualsByIntegerIndex(int32 InStateIndex) const { return CurrentStateIndex == InStateIndex; }

	// Returns (InStateIndex>= 0 && InStateIndex < NumStates);
	UFUNCTION(BlueprintPure, Category = "State Binder")
		bool IsValidStateIndexByIntegerIndex(int32 InStateIndex) const { return InStateIndex >= 0 && InStateIndex < NumStates; }

public: // [RINDERON] Byte index version for compatiblility with enum.

		// Binds enter state event (byte (enum) index version).
	// If you don't bind, just no event will be called.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void BindEnterEvent(uint8 InStateIndex, const FStateBinderEnterSignature& InEnterEvent) { BindEnterEventByIntegerIndex((int32)InStateIndex, InEnterEvent); }

	// Binds update state event (byte (enum) index version). (this will be called every time you call Update())
	// If you don't bind, just no event will be called.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void BindUpdateEvent(uint8 InStateIndex, const FStateBinderUpdateSignature& InUpdateEvent) { BindUpdateEventByIntegerIndex((int32)InStateIndex, InUpdateEvent); }

	// Binds exit state event (byte (enum) index version).
	// If you don't bind, just no event will be called.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		void BindExitEvent(uint8 InStateIndex, const FStateBinderExitSignature& InExitEvent) { BindExitEventByIntegerIndex((int32)InStateIndex, InExitEvent); }

	// Requests next state (byte (enum) index version).
	// Note that changing state is performed in Update(), not here.
	UFUNCTION(BlueprintCallable, Category = "State Binder")
		bool RequestNextState(uint8 InNextStateIndex) { return RequestNextStateByIntegerIndex((int32)InNextStateIndex); }

	// Returns current state index (byte (enum) index version).
	UFUNCTION(BlueprintPure, Category = "State Binder")
		uint8 GetCurrentStateIndex() const { return (uint8)GetCurrentStateIndexByIntegerIndex(); }

	// Checks if current state index equals to InStateIndex (byte (enumn) index version).
	UFUNCTION(BlueprintPure, Category = "State Binder")
		bool IsStateIndexEquals(uint8 InStateIndex) const { return IsStateIndexEqualsByIntegerIndex((int32)InStateIndex); }

	// Returns (CurrentStateIndex >= 0 && CurrentStateIndex < NumStates);
	UFUNCTION(BlueprintPure, Category = "State Binder")
		bool IsValidStateIndex(int32 InStateIndex) const { return IsValidStateIndexByIntegerIndex((int32)InStateIndex); }

private: // [RINDERON] internal properties

	static const int32 InvalidStateIndex = -1;

	int32 NumStates = 0;
	int32 CurrentStateIndex = InvalidStateIndex; // initial value must be invalid.
	int32 NextStateIndex = 0; // set zero to fire changing to state 0 at the top of Update().

	TArray<FStateBinderEnterSignature> EnterDelegateArray;
	TArray<FStateBinderUpdateSignature> UpdateDelegateArray;
	TArray<FStateBinderExitSignature> ExitDelegateArray;
};
```

## UStateBinderComponent.cpp

ちょっとした要点としては、ステートの切り替えを `RequestNextState()` 呼び出し内で処理せずに、`Update()` の冒頭で行っていることだろうか。
`RequestNextState()` が Tick の流れで呼び出されるとは限らないので(※)。

> ※Tick の流れで呼び出されるとは限らない
> コリジョンヒット時に呼び出されたり、アニメーションのNotify経由で呼び出されたり、Game Mode や Game State から呼び出されたり。
> `UStateBinderComponent::Update()` の実行開始～ステートイベントの処理～`Update()` に処理が戻ってくるまでの間 **「以外に」** ステートの変更が発生すると、想定外の動作を引き起こすことが多々ある。これは UE4 に限らず、すべてのゲームプログラムでよくある問題。

```C++
#include "StateBinderComponent.h"

DEFINE_LOG_CATEGORY(LogStateBinder);

// Sets default values for this component's properties
UStateBinderComponent::UStateBinderComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	// [RIDNERON] PrimaryComponentTick.bCanEverTick = true;
	/* [RINDERON] */ PrimaryComponentTick.bCanEverTick = false; // this component will not tick! call Update() manually.
}

// BeginPlay() は無改変なので割愛
// TickComponent() も無改変なので割愛

////////////////////////////////////////////////////////////////////////////////
// [RINDERON]

void UStateBinderComponent::Initialize(int32 InNumStates)
{
	if (InNumStates > 0)
	{
		EnterDelegateArray.SetNum(InNumStates);
		UpdateDelegateArray.SetNum(InNumStates);
		ExitDelegateArray.SetNum(InNumStates);

		NumStates = InNumStates;
		CurrentStateIndex = InvalidStateIndex; // initial value must be invalid.
		NextStateIndex = 0; // set zero to fire changing to state 0 at the top of Update().
	}
	else
	{
		EnterDelegateArray.Empty();
		UpdateDelegateArray.Empty();
		ExitDelegateArray.Empty();

		NumStates = 0;
		CurrentStateIndex = InvalidStateIndex;
		NextStateIndex = InvalidStateIndex;
	}
}

void UStateBinderComponent::BindEnterEventByIntegerIndex(int32 InStateIndex, const FStateBinderEnterSignature& InEnterEvent)
{
	if (EnterDelegateArray.IsValidIndex(InStateIndex))
	{
		EnterDelegateArray[InStateIndex] = InEnterEvent;
	}
}

void UStateBinderComponent::BindUpdateEventByIntegerIndex(int32 InStateIndex, const FStateBinderUpdateSignature& InUpdateEvent)
{
	if (UpdateDelegateArray.IsValidIndex(InStateIndex))
	{
		UpdateDelegateArray[InStateIndex] = InUpdateEvent;
	}
}

void UStateBinderComponent::BindExitEventByIntegerIndex(int32 InStateIndex, const FStateBinderExitSignature& InExitEvent)
{
	if (ExitDelegateArray.IsValidIndex(InStateIndex))
	{
		ExitDelegateArray[InStateIndex] = InExitEvent;
	}
}

void UStateBinderComponent::Update(float InDeltaSeconds)
{
	// change state
	if (CurrentStateIndex != NextStateIndex)
	{
		// exit current state
		if (ExitDelegateArray.IsValidIndex(CurrentStateIndex))
		{
			const FStateBinderExitSignature& onExitState = ExitDelegateArray[CurrentStateIndex];
			onExitState.ExecuteIfBound();
		}

		// enter next state
		if (UpdateDelegateArray.IsValidIndex(NextStateIndex))
		{
			FStateBinderEnterSignature& onEnterState = EnterDelegateArray[NextStateIndex];
			onEnterState.ExecuteIfBound();
		}

		CurrentStateIndex = NextStateIndex;
	}

	// update state
	if (UpdateDelegateArray.IsValidIndex(CurrentStateIndex))
	{
		FStateBinderUpdateSignature& onUpdateState = UpdateDelegateArray[CurrentStateIndex];
		onUpdateState.ExecuteIfBound(InDeltaSeconds);
	}
}

bool UStateBinderComponent::RequestNextStateByIntegerIndex(int32 InNextStateIndex)
{
	if (InNextStateIndex >= 0 && InNextStateIndex < NumStates && CurrentStateIndex != InNextStateIndex)
	{
		NextStateIndex = InNextStateIndex;
		return true;
	}
	return false;
}
```

# 以上です。

自分的には、今後非常に便利に使える気がしている。