LoginSignup
4
1

More than 3 years have passed since last update.

UE4 AnimNotifyをC++で書く場合のメモ

Posted at

概要

UnrealEngine の AnimNotify についてのメモです。
主に AnimNotifyStateをC++で実装する場合の問題点と回避方法について記述しています。

環境

Windows10
Visual Studio 2017
UnrealEngine 4.25

参考

以下を参考にさせて頂きました、ありがとうございます。
UE公式 : アニメーション通知
[UE4] 独自のAnimation Notifyの実装方法

AnimNotify / AnimNotifyState とは

アニメ―ションシーケンスなどに埋め込んで任意のタイミングで何らかの処理を行うための通知システムです。

関連ソース

"Engine\Source\Runtime\Engine\Classes\Animation\AnimNotifies\AnimNotify.h"
"Engine\Source\Runtime\Engine\Classes\Animation\AnimNotifies\AnimNotifyState.h"

BPで作成

ブループリントをAnimNotify か AnimNotifyStateを親として作成します。

AnimNotifyは Received Notifyをオーバーライドして処理を作成します。
AnimNotifyBP_override.jpg

AnimNotifyStateは ReceivedNotifyBegin ReceivedNotifyEnd ReceivedNotifyTickをオーバーライドして処理を作成します。
AnimNotifyStateBP_override.jpg

作成例

Received Notify をオーバーライドして処理を作成する例です。
アクターに対してイベントを送っています。
AnimNotiryBP_sample.jpg

C++で作成

C++にて、AnimNotify か AnimNotifyState を親クラスとして作成します。
BPの場合とはオーバーライドするメソッド名が違うことに注意。

作成例

以下、AnimNotifyStateを継承した自作アニメーション通知ステートのサンプルコードです。メンバCountを追加してありますがそのままだと問題があります

AnimNotifyState_Test.h
#pragma once
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "AnimNotifyState_Test.generated.h"

UCLASS(editinlinenew, Blueprintable, const, hidecategories = Object, collapsecategories, meta = (ShowWorldContextPin, DisplayName = "Test"))
class TEST_API UAnimNotifyState_Test : public UAnimNotifyState
{
    GENERATED_UCLASS_BODY()

public:
    virtual void NotifyBegin(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float TotalDuration);
    virtual void NotifyEnd(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation);
    virtual void NotifyTick(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float FrameDeltaTime);


#if WITH_EDITOR
    virtual bool CanBePlaced(UAnimSequenceBase* Animation) const override;
#endif

    // メンバを持つ(普通に使用するのは問題あり)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UAnimNotifyState_Test")
    float   Count;
};

メンバ Countに対しての処理のみです。最後にアウトプット出力をしています。
普通はBegin/Endのタイミングでイベントを発行するような使い方が多いかと思います。

AnimNotifyState_Test.cpp
#include "./AnimNotifyState_Test.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "Animation/AnimSequenceBase.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimInstance.h"

UAnimNotifyState_Test::UAnimNotifyState_Test(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{

    Count = 0.0f;
}

void UAnimNotifyState_Test::NotifyBegin(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float _TotalDuration)
{
    Super::NotifyBegin(MeshComp, Animation, _TotalDuration);

    // 初期化
    Count = 0.0f;
}

void UAnimNotifyState_Test::NotifyEnd(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation)
{
    Super::NotifyEnd(MeshComp, Animation);


    // 結果表示
    UE_LOG(LogTemp, Log, TEXT("Id:%d, Cound:%f"), MeshComp->GetOwner()->GetUniqueID(), Count);
}

void UAnimNotifyState_Test::NotifyTick(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float FrameDeltaTime)
{
    Super::NotifyTick(MeshComp, Animation, FrameDeltaTime);

    // カウント
    Count += FrameDeltaTime;
}

#if WITH_EDITOR
bool UAnimNotifyState_Test::CanBePlaced(UAnimSequenceBase* Animation) const
{
    if( Animation ){
        if( Animation->IsA(UAnimSequence::StaticClass())
            || Animation->IsA(UAnimMontage::StaticClass())
        ){
            return(true);
        }
    }

    return(false);
}
#endif // WITH_EDITOR

CanBePlaced について

アニメ―ションシーケンスのみやモンタージュのみといったように通知を配置できる条件を設定できます。

ファイル名で配置条件を指定する例
bool UAnimNotify_Test::CanBePlaced(UAnimSequenceBase* Animation) const
{
    if( Animation->GetName().Equals(TEXT("TestAnimSeq")) ){
        return(true);
    }
    return(false);
}

通知の設定

任意のアニメ―ションシーケンスアセットを開いて通知から右クリック 通知ステートを追加 で先ほど作成した通知を追加します。
詳細をみると追加したメンバ Count が確認できます。

setting_Notify.jpg

これで通知範囲にアニメ―ションが入った場合、NotifyBegin が呼ばれ、通知範囲からアニメ―ションが出た場合、NotifyEnd が呼ばれ、通知範囲内で NotifyTick が呼ばれるようになります。

実行結果

任意のアクターがこのアニメーションを再生するとアウトプットログは以下の様になります。処理が呼ばれることを確認できます。

アウトプットログ
LogTemp: Id:165160, Cound:0.783333

UROなどのアニメ―ション最適化処理をしているとカメラとの距離などにより数値が変わる場合があります。

上記のサンプルコードの問題点について

上とは別なアクターがこのアニメーションを再生するとアウトプットログは以下の様になりました。

アウトプットログ
LogTemp: Id:165102, Cound:0.783333

ユニークIDが違うので別アクターから呼び出されたことがわかります。Tickで回しているカウンタの数値は一緒です。
これを2体のアクターがほぼ同時に呼ぶと以下のような結果になりました。

アウトプットログ
LogTemp: Id:165160, Cound:1.449999
LogTemp: Id:165102, Cound:1.499999

数値がおかしくなりました。
これは UAnimNotifyStateUCLASS(const) で定義されたクラスでインスタンス毎ではなく同じオブジェクトを再利用しているためです。
この回避方法としては、

1.TMapなどの配列で外部から変数を退避&書き戻しを行う
2.エンジンに実装されている UAnimNotifyState_TimedParticleEffect クラスのような処理を行う(パーティクル名とソケット名などで再検索をしています)

のような方法が考えられます。
この const クラスは AnimNotifyState だけではなく、AnimNotify でも同様ですが、こちらはワンショットなため発覚しにくいと思います。こちらも可能ならメンバ変数を外部へ退避できる仕組みを作成するのが好ましいと思います。

回避コード例

Owner になるクラスに

オーナーアクタークラス
TMap<int32, UAnimNotifyState*> AnimNotifyStateMap;

のような TMap を作成します。
次に以下の様に変数を退避/書き戻しを行います。Copyはメンバのコピーメソッドです。

AnimNotifyState_Test.cpp

void UAnimNotifyState_Test::NotifyBegin(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float _TotalDuration)
{
    Super::NotifyBegin(MeshComp, Animation, _TotalDuration);

    Count = 0.0f;

    auto _Owner = MeshComp->GetOwner();
    if(::IsValid(_Owner)){
    auto _Object = NewObject<UAnimNotifyState_Test>();
        Copy(_Object, this);
        // TMAPへ追加
        _Owner->AnimNotifyStateMap.Add(Animation->GetUniqueID(), _Object);
    }

}

void UAnimNotifyState_Test::NotifyEnd(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation)
{
    Super::NotifyEnd(MeshComp, Animation);

    auto _Owner = MeshComp->GetOwner();

    // 変数を書き戻し
    if(::IsValid(_Owner)){
        UAnimNotifyState* _Object = *_Owner->AnimNotifyStateMap.Find(Animation->GetUniqueID());
        if( ::IsValid(_Object)){
            Copy(this, Cast<UAnimNotifyState_Test>(_Object));
        }
    }

    if(::IsValid(_Owner)){
        // TMAPから削除
        _Owner->AnimNotifyStateMap.Remove(Animation->GetUniqueID());
    }

    // 結果表示
    UE_LOG(LogTemp, Log, TEXT("Id:%d, Cound:%f"), MeshComp->GetOwner()->GetUniqueID(), Count);
}

void UAnimNotifyState_Test::NotifyTick(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float FrameDeltaTime)
{
    Super::NotifyTick(MeshComp, Animation, FrameDeltaTime);

    auto _Owner = MeshComp->GetOwner();

    // 変数を書き戻し
    if(::IsValid(_Owner)){
        UAnimNotifyState* _Object = *_Owner->AnimNotifyStateMap.Find(Animation->GetUniqueID());
        if( ::IsValid(_Object)){
            Copy(this, Cast<UAnimNotifyState_Test>(_Object));
        }
    }

    Count += FrameDeltaTime;

    // 退避
    if(::IsValid(_Owner)){
        UAnimNotifyState* _Object = *_Owner->AnimNotifyStateMap.Find(Animation->GetUniqueID());
        if( ::IsValid(_Object)){
            Copy(Cast<UAnimNotifyState_Test>(_Object), this);
        }   
    }
}

これで回避は一応できます。
ですが、これにも問題があります。1つのアニメ―ションシーケンスに複数通知を置くと検索で違うものを取得する場合があります。(検索Keyがアニメ―ションのユニークIDなため)
使用方法に合わせて回避コードを組む必要があります。

まとめ

もっと良い回避方法があればどなたがご教授下さい。
というかアニメ―ション通知の仕様がインスタンス毎に作成されるようになってほしいです。

4
1
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
4
1