UnrealEngine GameplayAbilitySystemをLevelをまたいで使えるように改造してシーケンス制御として使う方法

# はじめに
UEにはGameplayAbilitySystem(以下GAS)という、能力やアトリビュートを実装するためのフレームワークがあります。
アクションゲームであれば主に攻撃アクションなどを実装するのに使われたりしますが、
今回はゲーム全体のシーケンス制御にGASを使ってみようというお話です

基本的な機能しか使わないのでGASの仕組みを理解するのにも良いですし、どんなゲームでも使えるテクニックだと思います。


# 複数人で遊ぶカードゲームを例に考えてみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/bf8dc45e-b00c-b610-dab7-947c3b250551.png)

こういうシーケンスを作ります
- タイトルでボタンを押すとゲームが始まり、終了したらタイトルへ戻る
- ゲーム中は複数のシーケンスがある
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/9fad2edc-3b93-a4e8-8e44-307e1084f222.png)

# 今回使うGASの機能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/e2975013-857b-1b50-a479-e632483eb5f1.png)
- AbilitySystemComponent
　アビリティ（タスクみたいなもの）を登録するマネージャーみたいなクラス
- GameplayAbility
　今回はこのアビリティをシーケンスのタスクとして使います
- GameplayTag
　アビリティに紐づけるタグ
- GameplayEvent
　アビリティにイベントを通知するクラス

# GASはレベルをまたいで存在できない？
![cropmoneychitose.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/a0a9b4ea-a950-dd98-55d2-d6aa70519ff6.jpeg)
実装している中で１つ問題にぶち当たりました。AbilitySystemComponentです。
これはActorComponentで、Actorにくっつけて使う必要があります。

なのでOpenLevelすると消えてしまうんです。
先程考えたシーケンスは「タイトル」と「インゲーム」はレベルを分けたいのでこれでは困ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/f4dcd46a-4a9f-05a0-76c3-4644b7d1d4be.png)

# ActorComponentを延命させる方法
簡単に言うとレベル遷移する時にOuterを無理やり切り替えてやります。

まずは、AbilitySystemComponentを継承したカスタムクラスを作ります。
デフォルトだとUnregisterComponent時にアビリティがDestroyされてしまうのでそれを防ぐためです
```cpp
UCLASS()
class UContinuousAbilitySystemComponent : public UAbilitySystemComponent
{
	GENERATED_BODY()

	virtual void OnUnregister() override
	{
		// UnregisterComponentした時にAbilityがDestroyされないようにする
		UActorComponent::OnUnregister();
	}
};
```

- 適当なGameInstanceSubsystemを作ってそのInitializeでUAbilitySystemComponentのオブジェクトを生成します。
- レベル遷移前と後のDelegateに関数を登録します
```cpp
void UAbilitySystemSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	AbilitySystem = NewObject<UContinuousAbilitySystemComponent>(this);

	// レベル遷移前にGameStateから登録解除する
	FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UAbilitySystemSubsystem::OnPreLoadMap);

	// レベル遷移後にGameStateに登録する
	FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UAbilitySystemSubsystem::OnPostWorldInitialization);
	GetWorld()->GameStateSetEvent.AddUObject(this, &UAbilitySystemSubsystem::RegisterToGameState);
}

```
レベル遷移する直前に呼ばれる関数です。
- Rename関数を使ってOuterをこのSubsystemに切り替えます。
- AbilitySystemComponentと登録されているGameplayAbility両方にやる必要があります
```cpp
void UAbilitySystemSubsystem::OnPreLoadMap(const FString& MapName)
{
	// GameStateと共に削除されてしまわないようにOuterを変更する
	AbilitySystem->UnregisterComponent();
	AbilitySystem->Rename(nullptr, this);

	for (const FGameplayAbilitySpec& AbilitySpec : AbilitySystem->GetActivatableAbilities())
	{
		const TArray<UGameplayAbility*>& AbilityInstances = AbilitySpec.GetAbilityInstances();
		for (UGameplayAbility* Ability : AbilityInstances)
		{
			Ability->Rename(nullptr, this);
		}
	}
}

```
レベル遷移直後に呼ばれる関数です。
- OuterをGameStateにつけ直す関数を呼び出します
- このタイミングでGameStateがまだ生成されていない場合があるのでGameStateSetEventにも登録しておきます
```cpp
void UAbilitySystemSubsystem::OnPostWorldInitialization(UWorld* World, const UWorld::InitializationValues IVS)
{
	if (AGameStateBase* GameState = World->GetGameState())
	{
		RegisterToGameState(GameState);
	}
	else
	{
		World->GameStateSetEvent.AddUObject(this, &UAbilitySystemSubsystem::RegisterToGameState);
	}
}

```
OuterをGameStateにつけ直す関数です。
- Rename関数を使ってOuterをGameStateにします。
- RegisterComponentとInitAbilityActorInfoも呼ぶ必要があります。
```cpp
void UAbilitySystemSubsystem::RegisterToGameState(AGameStateBase* GameState)
{
	// 変更したOuterをGameStateに戻す
	AbilitySystem->Rename(nullptr, GetWorld()->GetGameState());

	for (const FGameplayAbilitySpec& AbilitySpec : AbilitySystem->GetActivatableAbilities())
	{
		const TArray<UGameplayAbility*>& AbilityInstances = AbilitySpec.GetAbilityInstances();
		for (UGameplayAbility* Ability : AbilityInstances)
		{
			Ability->Rename(nullptr, GetWorld()->GetGameState());
		}
	}

	AbilitySystem->RegisterComponent();
	AbilitySystem->InitAbilityActorInfo(GetWorld()->GetGameState(), GetWorld()->GetGameState());
}
```
これで完了です！OpenLevelをしても死ななくなりました。
BPではこのように取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/04551b73-4188-d3c2-1f9c-3fd9a0f8e229.png)

# 準備ができたので各画面のシーケンスを作っていきましょう
![cropSAYA0I9A8798.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/147bf7a8-60b3-126c-ce34-65bf81d5088f.jpeg)

# まずはシーケンスに必要なタグを追加する
追加方法など詳しくはこちらの記事
[UnrealEngine GamePlayTagを使おう！](https://qiita.com/okqi/items/1d159de5984a518b8839)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/e2ff6a95-4f77-b7a7-f8d5-7461899c2a93.png)


# タイトルシーケンス用のタスク（GameplayAbility）を作る
ちなみにゲーム画面です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/8f7b8525-2c3f-1f19-def6-5ee304edc53d.png)

右クリックのGamePlayAbilityブループリントから作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/3a09316f-7cea-d536-db0e-e4a1ee6c9d96.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/8bbd22fc-37ca-5c09-8e5e-712cf6206802.png)

BPはこんな感じです。ウィジェットを作成して、ボタンが押されるまで待ちます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/3dfb1308-8d15-156e-f5ee-f12f5360a532.png)

クラスの設定で以下の設定をします。
- Ability Tags　（このAbilityがどのタグに属しているか）
- Activation Ownd Tag　（このAbilityがアクティブになった時に有効になるタグ）

それぞれに同じタグを設定しておきます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/b5c7e88f-ea54-0279-7e92-5d539bcd7b75.png)


# Abilityの登録
タイトル用のレベルを作ったらそのレベルブループリント、または登録されているGameStateのBeginPlayで以下のように登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/43230d41-227e-7cea-3c52-c7e847e1ef52.png)

これで使えるようになりました。
プレイすると、アビリティが動き出し、Widgetが表示されれば成功です。

ボタンが押されたら次に進むようにしてみましょう

# ボタンが押されたらイベントを発行

このようにイベント用のタグを指定することで、待機しているAbilityに通知することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/a686f16b-5974-cfde-3cc4-536a8257cf5e.png)

AbilityはこのWaitノードでイベントがくるのを待っているので、イベントが来ると先に進みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/c67b6e99-b0e8-37d7-fbca-696b7f04f813.png)

# メインシーケンスを作成する
タイトル画面はできました、今度は各画面遷移を管理するメインシーケンス用のアビリティを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/ffd55cf0-1084-bd75-fe16-27f5447395b7.png)
BPはこんな感じ。
- タイトルが終了したらインゲーム用のレベルをオープンして
- インゲームが終了したらタイトルをオープンします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/32fb89e2-6a9a-4fef-a0ad-b11767867ced.png)

登録はGameInstance等で行うと良いでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/32264fd9-b1b7-c1f2-4dfd-3d0e84cba2a3.png)


# インゲーム用のシーケンスも作っていこう
ちなみに、ゲーム中の画面です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/d31c0119-a573-6fe6-625b-5cc6b031ad7f.png)

タイトルと同じように必要なAbilityを追加していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/318bb7db-00b5-51cd-f02c-6261152df583.png)

登録もタイトルと同じようにインゲーム用のレベルを作ってそこで登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/72cecf2c-afed-cf58-67f7-07258a7e823a.png)


# インゲーム中だけのシーケンスを管理するアビリティを作る
インゲーム中は複数のシーケンスに分かれているのでそれらを管理するアビリティを作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/82c3991d-8638-b8a4-7e6b-e39e4c2523f0.png)

初期化して、それぞれの子シーケンスを起動、終了を待つを繰り返します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/613d647f-fd38-3eca-ef51-e4e3a5c02d3e.png)

# インゲームを抜けるときはどうするの？
CancelAbilities関数を使って「InGame」タグのアビリティをキャンセルしてやるだけです。
c++からしか呼べないのでBPへ公開する関数を用意しても良さそうです。
このあたりの機能が標準で備わってるのがGASの良いところですね！
```cpp
//.h
UFUNCTION(BlueprintCallable)
void CancelAbilities(FGameplayTagContainer WithTags, FGameplayTagContainer WithoutTags, UGameplayAbility* Ignore = nullptr);

//.cpp
void UAbilitySystemSubsystem::CancelAbilities(FGameplayTagContainer WithTags, FGameplayTagContainer WithoutTags, UGameplayAbility* Ignore)
{
	AbilitySystem->CancelAbilities(&WithTags, &WithoutTags, Ignore);
}
```

# おわりに
switch caseでもできそうなことをわざわざGASを使って長々とやってきましたが、このやり方のメリットは
- クラスの粒度が小さくなって管理しやすい
- シーケンスの遷移を容易に変えられる（チュートリアルだったら説明のシーケンスを追加とか）
- シーケンスのタスク自体も簡単に切り替えられる（別のゲームモードだと勝利条件が異なるとか）

です。
最初の準備が少し面倒ですが使っていくとシーケンスの作成がかなり楽になりますよ。

以上。