[UE] 足跡、足音エフェクトを再生する

![27aa85faf671f413f33eb941609df438.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/289b9b90-4065-9c5c-fa99-0cd333cb44c2.gif)
3度のアップデート（作り直し）を経て、いくつか知見を得たのでここに記します。

# footsteps とは
足音、足跡、その他、足元からトレースした地面に対して再生するエフェクトを指します。

## エフェクト素材について
[マーケットプレイス](https://www.unrealengine.com/marketplace/ja/store)にはたくさんの素材があるので探してみるのもよいでしょう。

- [Niagara Footstep VFX](https://www.unrealengine.com/marketplace/ja/product/niagara-footstep-vfx) - 無料のNiagaraアセットパック。サウンドは含まれてないです。
- [Footsteps Sounds with Blueprint Setup] (https://www.unrealengine.com/marketplace/ja/product/footsteps-sounds-with-blueprint-setup) - 過去に無料月アセットだったこともあり所持してました。

# AnimNotify の実装

1. アニメ再生中で足が接地した（＝エフェクトを再生したい）時、通知を発行します。
1. その時、実際に接地しているか、足元素材が何かを知るために地面方向にトレースします。
1. トレースに成功したら HitResult（位置、法線、Etc...）からエフェクトを再生します。

## ソケットの設定
![db18ae6d4839bc5166312d3b1fb70418.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/6803118e-36dd-c890-d204-714109b16c45.png)
トレースを開始する位置を設定しましょう。画像では左右、足ソケットのタイプを定義しています。
![1904e9b573408e164e738ee258138a01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/69eb9526-c4b9-0134-1468-0d19174341e3.png)
ソケット名はスケルトン依存なので、スケルトン専用の AnimNotify 子クラスを用意したりします。

### GameplayTag の活用
上記、足のソケットなど、その種類には列挙型で定義したいところですが、

- C++ での UEnum はプログラマでしか追加できない。ビルドする必要がある。
- Blueprint の列挙型は C++ で参照できない。

といったそれぞれで欠点があるため、GameplayTag を使っています。
エディタから追加できる、チェックボックス入力のサポート、定義の階層化などの利点があります。
幅広い用途として使えてしまうため扱いが難しい、リダイレクタが弱く変更しづらい（＝命名むずい）のが欠点ですかね。

## エフェクトの再生分け
エフェクトは歩きによる足跡だけではありません。
走り、ジャンプ、着地、などその時の動作。地面の表面材質によっても変わります。
この技の時は違う煙エフェクトを出したいって？よかろう。追加してください。
![f363e222c7077ea5b7ef1324a119fde8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/06ecc81a-4790-63bb-d8c5-6f192d8db70f.png)
アクションも GameplayTag で定義します。

## データテーブル化する
![7829b96a66972a3e0a036df050afc302.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/e06a46f6-b7bc-b59f-8b82-ace7c0244fde.png)
アクション＆サーフェイスによるデータを登録します。
命名規則は [Action].[Surface] とし、指定のサーフェイスが登録されていない場合は階層を削って、再帰検索で再生するようにしてます。

### サーフェイス名の取得
もちろん先にレベル側で物理マテリアルの設定が必要です。
参考：[UE4 PhysicalMaterialの設定と取得についてのメモ](https://qiita.com/unknown_ds/items/b709f146b7e7b76c2fda)

```C++
FName GetSurfaceName( TEnumAsByte<EPhysicalSurface> SurfaceType )
{
	if ( SurfaceType == SurfaceType_Default )
	{
		return TEXT( "Default" );
	}
	else if ( const FPhysicalSurfaceName* FoundSurface = UPhysicsSettings::Get()->PhysicalSurfaces.FindByPredicate(
				  [&]( const FPhysicalSurfaceName& SurfaceName ) { return SurfaceName.Type == SurfaceType; } ) )
	{
		return FoundSurface->Name;
	}
	return NAME_None;
}
```
UPhysicsSettings から取得できます。
※1 PhysicalSurfaces には Default は含まれていない。
※2 bReturnPhysicalMaterial をオプションに含めないとサーフェイスは取得できない。

## EffectAssetSubsystem を作った
VFX あるところに SFX あり。エフェクト素材は必ず紐づきがあります。
それぞれ別々で再生するより、一つのまとめた再生機構を作ることをおススメします。

```C++:
USTRUCT( BlueprintType )
struct FEffectAssets
{
	GENERATED_BODY()

	UPROPERTY( EditDefaultsOnly, BlueprintReadWrite )
	TArray<class UParticleSystem*> ParticleSystems;

	UPROPERTY( EditDefaultsOnly, BlueprintReadWrite )
	TArray<class UNiagaraSystem*> NiagaraSystems;

	UPROPERTY( EditDefaultsOnly, BlueprintReadWrite )
	TArray<class USoundBase*> Sounds;
};
```
上記の構造体に PlayEffectAtLocation / PlayEffectAttached（From DataTable） を呼ぶ仕組みです。

### なぜ BlueprintFunctionLibrary ではなく Subsystem がよいのか
Blueprint からのアクセスが Subsystem モジュールにまとまります。静的関数にむき出しだとグローバル領域が汚れるのを嫌う。
汎用な機能になるので、ライブラリ化したときに子クラスで動作を上書き（プロジェクト毎でサウンドミドルウェアを導入したりとか）できるのも良い点です。
親クラスは UEngineSubsystem か UWorldSubsystem がよいかなと思います。WorldSubsystem の場合は、プレビュー画面で再生できるように DoesSupportWorldType で EWorldType::EditorPreview を追加するのをお忘れなく。

## LineTrace の非同期化
トレース（レイキャスト）に関してたびたび負荷が問題となりますが、Footstep の場合は同期的に結果を得る必要がないため、非同期処理にします。
こういう負荷系の話をするときは、具体的に Nmsec 負荷が下がったよ！とかあればよいのですが、※ データはありません。

<details><summary>AsyncLineTrace の実装</summary><div>

```C++
void UAnimN_Footstep::TraceFoot( USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, FName SocketName )
{
	if ( AActor* OwnerActor = MeshComp ? MeshComp->GetOwner() : nullptr )
	{
		// 足元下方向への距離はアセット依存なのでプロパティ化
		FVector LocalTraceOffset = GetTraceOffset();

		FVector TraceBegin = MeshComp->GetSocketLocation( SocketName ) +
							 FVector::UpVector * 5.f;	 // 地面へのめり込みを考慮して開始地点は少し浮かす
		FVector TraceEnd = TraceBegin + LocalTraceOffset;

		FCollisionQueryParams TraceParams( NAME_None, false, OwnerActor );
		TraceParams.bReturnPhysicalMaterial = true;	   // サーフェイス取得フラグを設定する

		UWorld* World = OwnerActor->GetWorld();
		if ( World->IsGameWorld() )
		{
			FTraceDelegate TraceFootDelegate;
			TraceFootDelegate.BindUObject( this, &ThisClass::TraceFootDone, MeshComp, Animation );
			World->AsyncLineTraceByChannel( EAsyncTraceType::Single, TraceBegin, TraceEnd, ECC_Visibility, TraceParams,
				FCollisionResponseParams::DefaultResponseParam, &TraceFootDelegate );
		}
#if WITH_EDITOR
		else
		{
			FHitResult HitResult;
			if ( World->LineTraceSingleByChannel( HitResult, TraceBegin, TraceEnd, ECC_Visibility, TraceParams ) )
			{
				FVector HitLocation = HitResult.Location;
				EPhysicalSurface HitSurfaceType = SurfaceTypeInEditor;
				TriggerEffect( OwnerActor, Animation, HitLocation, HitSurfaceType );
			}
			else
			{
				DrawDebugLine( World, TraceBegin, TraceEnd, FColor::Red, false, 1.f );
			}
		}
#endif
	}
}

void UAnimN_Footstep::TraceFootDone(
	const FTraceHandle& TraceHandle, FTraceDatum& TraceDatum, USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation )
{
	if ( TraceDatum.OutHits.Num() == 0 )
	{
		return;
	}

	if ( AActor* OwnerActor = MeshComp ? MeshComp->GetOwner() : nullptr )
	{
		const FHitResult& HitResult = TraceDatum.OutHits[0];

		FVector HitLocation = HitResult.Location;
		EPhysicalSurface HitSurfaceType = HitResult.PhysMaterial.IsValid() ? HitResult.PhysMaterial->SurfaceType.GetValue()
																		   : EPhysicalSurface::SurfaceType_Default;
		TriggerEffect( OwnerActor, Animation, HitLocation, HitSurfaceType );
	}
}
```
</div></details>
![4cab94084e51a0ddb0659251701dfe00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/48372893-e0ec-c405-797d-6464fb03be51.png)
Footstep に限らず、サーフェイスをトレースしてエフェクトを出すみたいなことは他でもやるので、UBlueprintAsyncActionBase 継承のノードを作ったりもしました。便利。

## TriggerWeightThreshold
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Footstepsメモ②<a href="https://twitter.com/hashtag/UE4Study?src=hash&amp;ref_src=twsrc%5Etfw">#UE4Study</a><br><br>WalkのStart-Loopのように、AnimBPによるステートマシーンのブレンドなどにおいては、AnimNotify詳細のEvent.TriggerWeightThresholdを設定する。<br>アニメのInstanceWeightがこの値以下だったとき再生されなくなる。ALSV4では0.3で設定されている。 <a href="https://t.co/gjjPjGMWeV">pic.twitter.com/gjjPjGMWeV</a></p>&mdash; koorinonaka (@koorinonaka) <a href="https://twitter.com/koorinonaka/status/1406557438745055233?ref_src=twsrc%5Etfw">June 20, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## GetNotifyName, GetEditorColor
Footstep に限った話ではないのですが、上記関数はカスタマイズしておくと情報出せて便利です。
![863aeb6187c5baf576db9533fb57c440.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/1b50d392-4edc-60e8-5bb8-4c4035d4be13.png)
通知名やプロパティを含めるだとか、必須の情報が欠けていたら分かりやすく赤（＝エラー）とする、とかね。
なんか鳴らないんですよね、調べてもらっていいですか？を防止しましょう。