概要
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
をオーバーライドして処理を作成します。
AnimNotifyStateは ReceivedNotifyBegin
ReceivedNotifyEnd
ReceivedNotifyTick
をオーバーライドして処理を作成します。
作成例
Received Notify
をオーバーライドして処理を作成する例です。
アクターに対してイベントを送っています。
C++で作成
C++にて、AnimNotify か AnimNotifyState を親クラスとして作成します。
BPの場合とはオーバーライドするメソッド名が違うことに注意。
作成例
以下、AnimNotifyStateを継承した自作アニメーション通知ステートのサンプルコードです。メンバCount
を追加してありますがそのままだと問題があります。
#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のタイミングでイベントを発行するような使い方が多いかと思います。
#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
が確認できます。
これで通知範囲にアニメ―ションが入った場合、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
数値がおかしくなりました。
これは UAnimNotifyState
は UCLASS(const)
で定義されたクラスでインスタンス毎ではなく同じオブジェクトを再利用しているためです。
この回避方法としては、
1.TMapなどの配列で外部から変数を退避&書き戻しを行う
2.エンジンに実装されている UAnimNotifyState_TimedParticleEffect
クラスのような処理を行う(パーティクル名とソケット名などで再検索をしています)
のような方法が考えられます。
この const クラスは AnimNotifyState だけではなく、AnimNotify でも同様ですが、こちらはワンショットなため発覚しにくいと思います。こちらも可能ならメンバ変数を外部へ退避できる仕組みを作成するのが好ましいと思います。
回避コード例
Owner
になるクラスに
TMap<int32, UAnimNotifyState*> AnimNotifyStateMap;
のような TMap を作成します。
次に以下の様に変数を退避/書き戻しを行います。Copy
はメンバのコピーメソッドです。
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なため)
使用方法に合わせて回避コードを組む必要があります。
まとめ
もっと良い回避方法があればどなたがご教授下さい。
というかアニメ―ション通知の仕様がインスタンス毎に作成されるようになってほしいです。