3度のアップデート(作り直し)を経て、いくつか知見を得たのでここに記します。
footsteps とは
足音、足跡、その他、足元からトレースした地面に対して再生するエフェクトを指します。
エフェクト素材について
マーケットプレイスにはたくさんの素材があるので探してみるのもよいでしょう。
- Niagara Footstep VFX - 無料のNiagaraアセットパック。サウンドは含まれてないです。
- [Footsteps Sounds with Blueprint Setup] (https://www.unrealengine.com/marketplace/ja/product/footsteps-sounds-with-blueprint-setup) - 過去に無料月アセットだったこともあり所持してました。
AnimNotify の実装
- アニメ再生中で足が接地した(=エフェクトを再生したい)時、通知を発行します。
- その時、実際に接地しているか、足元素材が何かを知るために地面方向にトレースします。
- トレースに成功したら HitResult(位置、法線、Etc...)からエフェクトを再生します。
ソケットの設定
トレースを開始する位置を設定しましょう。画像では左右、足ソケットのタイプを定義しています。
ソケット名はスケルトン依存なので、スケルトン専用の AnimNotify 子クラスを用意したりします。
GameplayTag の活用
上記、足のソケットなど、その種類には列挙型で定義したいところですが、
- C++ での UEnum はプログラマでしか追加できない。ビルドする必要がある。
- Blueprint の列挙型は C++ で参照できない。
といったそれぞれで欠点があるため、GameplayTag を使っています。
エディタから追加できる、チェックボックス入力のサポート、定義の階層化などの利点があります。
幅広い用途として使えてしまうため扱いが難しい、リダイレクタが弱く変更しづらい(=命名むずい)のが欠点ですかね。
エフェクトの再生分け
エフェクトは歩きによる足跡だけではありません。
走り、ジャンプ、着地、などその時の動作。地面の表面材質によっても変わります。
この技の時は違う煙エフェクトを出したいって?よかろう。追加してください。
アクションも GameplayTag で定義します。
データテーブル化する
アクション&サーフェイスによるデータを登録します。
命名規則は [Action].[Surface] とし、指定のサーフェイスが登録されていない場合は階層を削って、再帰検索で再生するようにしてます。
サーフェイス名の取得
もちろん先にレベル側で物理マテリアルの設定が必要です。
参考:UE4 PhysicalMaterialの設定と取得についてのメモ
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 あり。エフェクト素材は必ず紐づきがあります。
それぞれ別々で再生するより、一つのまとめた再生機構を作ることをおススメします。
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 負荷が下がったよ!とかあればよいのですが、※ データはありません。
AsyncLineTrace の実装
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 );
}
}
TriggerWeightThreshold
Footstepsメモ②#UE4Study
— koorinonaka (@koorinonaka) June 20, 2021
WalkのStart-Loopのように、AnimBPによるステートマシーンのブレンドなどにおいては、AnimNotify詳細のEvent.TriggerWeightThresholdを設定する。
アニメのInstanceWeightがこの値以下だったとき再生されなくなる。ALSV4では0.3で設定されている。 pic.twitter.com/gjjPjGMWeV
GetNotifyName, GetEditorColor
Footstep に限った話ではないのですが、上記関数はカスタマイズしておくと情報出せて便利です。
通知名やプロパティを含めるだとか、必須の情報が欠けていたら分かりやすく赤(=エラー)とする、とかね。
なんか鳴らないんですよね、調べてもらっていいですか?を防止しましょう。