8
10

More than 3 years have passed since last update.

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

Posted at

27aa85faf671f413f33eb941609df438.gif
3度のアップデート(作り直し)を経て、いくつか知見を得たのでここに記します。

footsteps とは

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

エフェクト素材について

マーケットプレイスにはたくさんの素材があるので探してみるのもよいでしょう。

AnimNotify の実装

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

ソケットの設定

db18ae6d4839bc5166312d3b1fb70418.png
トレースを開始する位置を設定しましょう。画像では左右、足ソケットのタイプを定義しています。
1904e9b573408e164e738ee258138a01.png
ソケット名はスケルトン依存なので、スケルトン専用の AnimNotify 子クラスを用意したりします。

GameplayTag の活用

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

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

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

エフェクトの再生分け

エフェクトは歩きによる足跡だけではありません。
走り、ジャンプ、着地、などその時の動作。地面の表面材質によっても変わります。
この技の時は違う煙エフェクトを出したいって?よかろう。追加してください。
f363e222c7077ea5b7ef1324a119fde8.png
アクションも GameplayTag で定義します。

データテーブル化する

7829b96a66972a3e0a036df050afc302.png
アクション&サーフェイスによるデータを登録します。
命名規則は [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 );
    }
}


4cab94084e51a0ddb0659251701dfe00.png
Footstep に限らず、サーフェイスをトレースしてエフェクトを出すみたいなことは他でもやるので、UBlueprintAsyncActionBase 継承のノードを作ったりもしました。便利。

TriggerWeightThreshold

GetNotifyName, GetEditorColor

Footstep に限った話ではないのですが、上記関数はカスタマイズしておくと情報出せて便利です。
863aeb6187c5baf576db9533fb57c440.png
通知名やプロパティを含めるだとか、必須の情報が欠けていたら分かりやすく赤(=エラー)とする、とかね。
なんか鳴らないんですよね、調べてもらっていいですか?を防止しましょう。

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10