[UE] AbilityTask について全部書くよ

# はじめに
AbilityTask は GameplayAbility の実行中に実行できる**自己完結型**のタスクです。タスクは「何かを開始し、イベントを受信、または終了するまで待つ」という非同期処理を提供します。これらはアビリティの実装自由度、柔軟性を高めてくれる非常に重要な要素となります。

## 自己完結型とは
- アビリティの BP グラフ内でイベントをバインドする必要はありません！グラフは非常にシンプルになります。
- ネットワーク処理もタスクに内包されます（対応していないものもあります）。
- 処理がタスク毎に分割され、依存が小さく再利用しやすい設計になります。

# エンジンから提供される AbilityTask
AbilityTask は BP での実装が出来ず、C++ で書くしかありません。GameplayAbility が BP プロジェクトをサポートできないのも、この辺が原因にあるなぁ、と思ったり。しかしプラグイン内には汎用的に使えるタスクが最初から数多く実装されています！今回はそれらについて紹介します。
タスクの作成については [次の記事](https://qiita.com/koorinonaka/items/50b8daed1c4d7f691b62) に続きます。

:::note info
使ったことがないタスクは項目だけ作って説明を省いてます。そのうち使ったら埋める。かも。
:::

## PlayMontageAndWait
![GA_Document-EventGraph_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/75089f98-f713-571e-f024-9dbb55ba3c67.png)
アニメーション（Montage）を再生し、終了を待つ。BlendOut（OnCompleted）/Interrupted/Cancelled から EndAbility に繋ぐことが多い。BlendOut -> OnCompleted と呼ばれる、どちらに繋げるかはゲームデザインによる（片方だけで OK）。
おそらく GAS 使いで最も使用頻度が高いタスクではなかろうか。もはや AbilityTask と意識することなく使っているかもしれない。

:::note warn
AbilitySystem で MontageInfo が管理されているので、アビリティで Montage を再生する場合は（PlayMontage その他ではなく）このノードを使う。アビリティ終了時に再生を停止、再生位置の同期、AbilitySystem,GameplayAbility の Montage 関数での操作、などに影響する。
:::

## WaitGameplayEvent
![GA_Document-EventGraph_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/fbd27aa5-53d5-3b31-3c31-b5d23f5b85ad.png)
指定した EventTag と一致する GameplayEvent を待機する。EventTag は GameplayTag なので、階層をまとめて監視することも可能（その場合は OnlyMatchExact のチェックを外す）。Event.Damage で待ち受けておいて、Event.Damage.Slash, Event.Damage.Blow を受け取るなど。OptionalExternalTarget は他者のイベント（敵のダメージなど）を取得したいときに指定する。

## WaitGameplayTagAdd / WaitGameplayTagRemove
![GA_Document-EventGraph_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/3faa1763-0f19-bfb3-1e89-59dec12b9c05.png)
GameplayTag の付与/削除を通知する。GameplayTag は GameplayAbility.ActivationOwnedTags, GameplayEffect.GrantedTags, AddLooseGameplayTags など、いろんなところで付与されるので多様なイベントが取れる半面、管理は難しいかもしれない。GameplayEffect.GrantedTags は AssetSearch でも検索に引っかからないしな！そうなるともうコールスタック追いかけるしかない。。GameplayTag のご利用は計画的に。

:::note warn
AbilitySystem で管理された GameplayTag はスタックするが、WaitGameplayTagAdd/Remove での通知はスタック状態を無視する（スタック数が 0-1 の時のみ通知される）ので注意。
:::

## WaitInputPress / WaitInputRelease
![GA_Document-EventGraph_31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/31c65019-a801-5486-1a39-fbd4280c408c.png)
アビリティに紐づけられた入力の押下/離上で通知される。中の処理で通信の同期まで取ってくれる結構すごいやつ。
TestAlready~ にチェックを入れると、既に条件を満たした場合そのまま通知してくれます。

<details><summary>入力のセットアップについて補足</summary><div>初期状態ではアビリティに対してイベントがバインドされておらず、入力の通知を受け取ることができません。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://t.co/AgtR3eV8ST">https://t.co/AgtR3eV8ST</a><br>4.6.2 Binding Input to the ASC にInputPressed/Releasedへの実装が書いてあります。ちょっとこの辺は仕様がややこしいですね。。GiveAbilityで渡すFGameplayAbilitySpecに指定するInputIDが一致してればAbilityTaskのInputPressedでイベントが発行されるって感じです。</p>&mdash; あいす (@koorinonaka) <a href="https://twitter.com/koorinonaka/status/1474013534124384257?ref_src=twsrc%5Etfw">December 23, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

UAbilitySystemComponent::BindAbilityActivationToInputComponent の部分は、自分は以下のように簡略しました。

```C++:MyPlayerController.cpp
void AMyPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	InputComponent->BindAxis( TEXT( "MoveForward" ) );
	...

	auto BindPressedAndReleased = [&]( FName ActionName, int32 InputID )
	{
		FInputActionBinding IABPressed( ActionName, IE_Pressed );
		IABPressed.ActionDelegate.GetDelegateForManualSet().BindWeakLambda( this,
			[this, InputID]
			{
				if ( UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor( GetPawn() ) )
				{
					ASC->AbilityLocalInputPressed( InputID );
				}
			} );
		InputComponent->AddActionBinding( IABPressed );

		FInputActionBinding IABReleased( ActionName, IE_Released );
		IABReleased.ActionDelegate.GetDelegateForManualSet().BindWeakLambda( this,
			[this, InputID]
			{
				if ( UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor( GetPawn() ) )
				{
					ASC->AbilityLocalInputReleased( InputID );
				}
			} );
		InputComponent->AddActionBinding( IABReleased );
	};
	// ActionNameはプロジェクト設定->インプット->アクションマッピング、DefaultInput.iniで定義する名前
	BindPressedAndReleased( TEXT( "Jump" ), (int32) EMyAbilitySlot::Jump );
	...
}
```

```C++:MyAbilitySystemComponent.cpp
TSubclassOf<UGameplayAbility> AbilityClass;
int32 AbilityLevel = 1;
// FGameplayAbilitySpecの第3引数にInputIDを渡す
GiveAbility( FGameplayAbilitySpec( AbilityClass, AbilityLevel, (int32) EMyAbilitySlot::Jump, this ) );
```

ActionName と InputID（Enum で定義する）を結び付けます。
※アビリティは単一の入力を受け付けるように設計されています。複数の入力を受け取ることはできません。</div></details>

## WaitConfirm
![GA_Document-EventGraph_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/2f62af0e-a399-c6ac-5605-85cdcf5b597a.png)

## WaitForCancelInput / WaitForConfirmInput
![GA_Document-EventGraph_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/531af0d5-6ff9-8526-9073-b7b451bc4c96.png)

## WaitForAbilityActivate / WaitForAbilityActivate_Query / WaitForAbilityActivateWithTagRequirements
![GA_Document-EventGraph_48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/97171d15-3ff0-0d57-46f7-f5220876ee43.png)
他のアビリティの開始を待つ。ActivatedAbility としてインスタンスも取得できるので、そこからさらにバインドしたり、など。

## WaitForNewAbilityCommit / WaitForAbilityCommit_Query
![GA_Document-EventGraph_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/27c2b7c2-193f-489d-5cc0-d8a83081232c.png)
アビリティのコミット（消費）を待つ。

:::note info
アビリティは発動したときに自動で消費されるわけではなく、CommitAbility を明示的に呼んで初めて消費が実行される。
:::

## StartAbilityState
![GA_Document-EventGraph_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/fda3eef1-7718-9c9b-ede9-05aae1850764.png)
StateName の状態をアビリティ内に作ることができる。EndCurrentState にチェックを入れて新しく State を始める、もしくは EndAbilityState を呼ぶことで OnStateEnded が呼ばれる。排他処理に利用できる。

## WaitForAttributeChange / WaitForAttributeChangeWithComparison
![GA_Document-EventGraph_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/b8ae7e93-1a24-4eed-e2aa-ae6eb89cb609.png)
指定した Attribute（AttributeSet のパラメータ）の値が変化したときに通知される。WithComparison は、ComparisonValue との比較で一致した（ComparisonType で指定、N 値以下、以上など）とき通知が発行される。OptionalExternalOwner から、他者（敵とか）を指定することもできる。

### Wait for Attribute Changed は別物なので注意
![GA_Document-EventGraph_42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/04a42cd8-7e34-16a3-60bf-34445747cf24.png)
こっちは AbilityTask ではなく AbilityAsync ノード。アビリティ外部（UI など）でも呼び出し可能。アビリティでは WaitForAttributeChange を使いましょう。

## WaitForAttributeChangeThreshold / WaitForAttributeChangeRatioThreshold
![GA_Document-EventGraph_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/11df688c-c7e7-1cce-c43d-231e1c23bf31.png)
閾値（ComparisonValue）と比較して条件が一致したときに通知される。
ChangeThreshold は タスクの実行時に比較が実行され通知される点を除いて WaitForAttributeChangeWithComparison とほとんど同じ。
ChangeRatioThreshold は比率値（Numerator / Denominator）との比較で通知される。（現在 HP / 最大 HP）が N 以下になったとき、などを想定。

## WaitGameplayEffectAppliedToSelf / WaitGameplayEffectAppliedToTarget
![GA_Document-EventGraph_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/4e41f549-dc12-83e8-fac7-6979dcb14917.png)
GameplayEffect の適用が通知される。
WaitGameplayEffectAppliedToSelf_Query, WaitGameplayEffectAppliedToTarget_Query も存在する。

## WaitForGameplayEffectRemoved
![GA_Document-EventGraph_27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/376dd56a-8576-71d1-51f1-d42d0470a47a.png)
GameplayEffect の削除が通知される。引数には FActiveGameplayEffectHandle を指定する必要があるので、その場で取得できない場合は WaitGameplayEffectApplied で ActiveHandle を受け取るなどする必要がある。

## WaitForGameplayEffectStackChange
![GA_Document-EventGraph_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/fd5c68ed-4c98-0a65-d2c9-9ceda8b7c0a3.png)
既に発動済みのエフェクトに対して、スタックが変化したときに通知される。

## WaitGameplayEffectBlockedByImmunity
![GA_Document-EventGraph_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/a93a2612-c859-5eb0-ae7b-72f494dd624a.png)
GameplayEffect を適用したとき、Immunity（免疫、レジスト）により発動しなかったときに通知される。

## ApplyRootMotion
RootMotion といえばアニメアセットから参照するルート移動の方を想像します（すると思う）が、実はプログラムから直接呼び出すこともできます。こういった移動処理は挙動も調整しやすいように自前で実装する方も多いと思いますが、**マルチプレイでもきれいに同期をとって動作してくれる**のがポイントです。

### ApplyRootMotionConstantForce
![GA_Document-EventGraph_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/071c8061-8ef8-6f31-70aa-c00703c8bce9.png)
定量（Force）の移動値を与える。IsAdditive が false の場合は Velocity を上書きするような挙動になります。VelocityOnFinish でタスク終了時の Velocity を調整できる。

:::note info
Duration はその期間で力を加え続けるわけではなく、間隔をあけて再度処理が走るような挙動となる（Force が切れたタイミングかな？）。
:::

### ApplyRootMotionJumpForce
![GA_Document-EventGraph_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/8a3c4219-0dd4-0a7e-64bc-835ee716376b.png)
一定時間（Duration）で指定の高さ（Height）と指定方向（Rotation）への距離（Distance）でジャンプする。時間を長くすれば軌道は緩やかに、高さはきっちりぶれずに指定できる、など。調整値を指定できるのは便利（逆計算いらず！）ですが、慣性や減衰は働かないので動きは固くなります。カーブアセットでその辺も調整できるかな？

### ApplyRootMotionMoveToActorForce
![GA_Document-EventGraph_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/1e2eda9d-52bb-4d26-b2d1-a0062324b894.png)
TargetActor に向かって指定時間（Duration）をかけて移動する。ただの線形移動なのでぬるっと動く（TargetLerpSpeed で調整できると思う）。ターゲット位置が移動すると追従します。TargetData で指定する ApplyRootMotionMoveToTargetDataActorForce も存在する。
![0b14af6030dd9496a03e9ad431c9ca7e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/0434f965-589a-571a-03ea-77e3bbd5aab9.png)
縦になが～い！！個人的に愛用している [BlueprintAssist](https://www.unrealengine.com/marketplace/ja/product/blueprint-assist) さんから怒られるんだよね。

### ApplyRootMotionMoveToForce
![GA_Document-EventGraph_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/ef100857-28f5-382e-24b4-dbefb70a6ca8.png)
Location に向かって指定時間（Duration）をかけて移動する。MoveToActorForce の追従なし版。

### ApplyRootMotionRadialForce
![GA_Document-EventGraph_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/a3009998-8481-6626-042d-4b51d2c984db.png)
指定位置（Location/LocationActor）から発生する力（Strength、Radiusにより減衰する）を加える。これも Duration は ConstantForce と似た挙動だったかな？時間は 0.01 など短く指定する。IsPush を false にすると引き寄せとなるのが面白いところ。グラビティホール！！

## MoveToLocation
![GA_Document-EventGraph_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/b44861b8-ef27-3b45-723f-1e56537cac21.png)
Location に向かって指定時間（Duration）をかけて移動する。ApplyRootMotionMoveToForce と挙動はほぼ一緒、と思うじゃない？中身は SetActorLocation でテレポート（bSweep=false）してるから結構危険なんだな。コリジョン内部でスタックしちゃうので気を付けましょう。。ｗ

## RepeatAction
![GA_Document-EventGraph_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/9dc41d60-6b6f-78e0-efee-08cccaefd932.png)
指定間隔（TimeBetweenActions）で指定回数（TotalActionCount）の OnPerformAction が走る。

:::note info
内部は TimerManager で動いており、きっかり TimeBetweenActions ではなく時間"以上"経過で処理が流れてくる。
:::

## WaitDelay
![GA_Document-EventGraph_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/2bd2e19f-66e8-1eb1-a449-0a31e0827e06.png)
中身は Delay と同じ。

## WaitNetSync
![GA_Document-EventGraph_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/6a3afce9-2ffa-0d3d-c75c-98a2c8555b5d.png)
サーバーとクライアントで同期を取る。例えば OnlyServerWait の場合は以下のようになる。

1. サーバーではクライアントからの受信を待機する。
1. クライアントではサーバーに信号を送信し、OnSync で抜ける。
1. サーバーで信号を受信し OnSync で抜ける。

## SpawnActor
![GA_Document-EventGraph_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/8ad3657d-a427-a41d-2475-d174752c62f1.png)
それなりに便利だとは思うのだが使ったことはない。。動的な ExposeOnSpawn のプロパティが渡せるノードとなっているようです。

## WaitMovementModeChange
![GA_Document-EventGraph_32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/5d5fedb5-dada-9cc2-18dc-9e6d529db73d.png)
CharacterMovementComponent.MovementMode の変更が通知される。NewMode が None 以外の場合は通知をフィルターする。

## WaitVelocityChange
![GA_Document-EventGraph_36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/d822b40a-1f23-28f8-0749-afcddda44c56.png)
指定方向への Velocity が指定以上だったとき通知する。
中身の処理的には AvaterActor の Velocity を Tick 監視し、Direction との内積結果が MinimumMagnitude より大きかった時に通知される。

## WaitForOverlap
![GA_Document-EventGraph_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/dbe8328f-af5e-0102-9179-15bef04bc224.png)
自身が何かに衝突した時に HitResult が TargetData として返却される。

:::note warn
Overlap とあるが実際には OnComponentHit を判定してる。。WaitForHit やんけ！
:::

## TargetData
GameplayAbilityTargetActor を用いてターゲット、目標位置などの送信。
[4.11.2 Target Actors](https://github.com/tranek/GASDocumentation/blob/master/README.md#4112-target-actors)にだいたい書いてある。使ったことない。

### VisualizeTargeting
![GA_Document-EventGraph_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/c01687f0-2077-3201-94d3-430fad7f360f.png)

### WaitTargetData / WaitTargetDataUsingActor
![GA_Document-EventGraph_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/4c034f14-65e9-bbca-3086-b243f898ee33.png)