シーケンサーのフレームレートロックを使用するとAnimNotifyStateのBegin後すぐにEndが呼ばれることがある

お世話になっております。

現在プロジェクトでは、シーケンサーのカメラカットごとにアニメーションシーケンスでキャラクターの位置を変更している場合があり、

その位置の切り替わりが見えないようにするため、シーケンサーのフレームロック(EMovieSceneEvaluationType::FrameLocked)

を使用していたのですが、AnimNotifyStateのBegin後すぐにEndが呼ばれる場合があり、AnimNotifyStateが意図しない挙動となることがありました。

調査したところ、FAnimMontageInstance::SetSequencerMontagePosition内のシーケンサーの再生位置を設定している箇所で

前回の再生位置と今回の再生位置が一致する場合があり、FMontageSubStepper::Advance内で動いていないと判断されるため、

AnimNotifyStateのEndが呼ばれるようになっておりました。

どうやら、シーケンサーのフレームレートロックを使用していると、前回の再生位置と今回の再生位置との差が1未満かつ

桁が繰り上がらない場合、FMovieScenePlaybackPosition::PlayTo内の少数切り捨て処理により、再生位置が一致しておりました。

以上を踏まえて、2点質問させていただきたいことがあります。

1.シーケンサーのフレームロックを使用する場合、AnimNotifyStateを使用は非推奨なんでしょうか?

2.非推奨の場合、シーケンサーのカメラカットごとにアニメーションシーケンスでキャラクターの位置を変更したい場合は

どういった方法をとればよいでしょうか?

現状、想定している対応方法としては以下の方法を考えておりますが、他によい方法があれば教えていただきたいです。

①シーケンサーのフレームロックを外す

②再生するアニメーションシーケンスの補間をStepにする

(アニメーションがカクカクになるため、キーは1フレームごとに打つつもりでいます)

[Attachment Removed]

再現手順
①添付したSampleProjectを起動

②PL_SampleLevelを開く

③PIEでレベルをプレイ

④1キーを押し、シーケンサーを再生する

デバッグ表示にて、Notify Begin後すぐにNotify Endが表示されることを確認

(発生頻度が低いため、何度か手順④を繰り返してください)

[Attachment Removed]

大変お待たせしてしまい恐縮です。

​再現プロジェクトによって問題を再現することが出来ました。

ご調査いただいた通りカーソル位置が変化しないTickが発生するとアニメーションの更新が停止したと判定されてEndイベントが発火してしまいます。

対策としてはシーケンサー​のメニューのフレームレートオプション > Advanced Option > Desired Tick Intervalを30fpsに設定で改善がみられました。

こちらを一度お試しいただけますでしょうか。

​> 2.非推奨の場合、シーケンサーのカメラカットごとにアニメーションシーケンスでキャラクターの位置を変更したい場合は

どういった方法をとればよいでしょうか?

上記対策で解決が得られない場合の調査方向のために確認させていただきたいのですが、状況としては同じキャラクターがカットの切り替わりで移動する場合に中間のフレームが表示されることが望ましくないためという認識でよろしいでしょうか?​

[Attachment Removed]

提案が機能しなかった旨承知しました。大変お手数をおかけしました。

修正方法としては必ずシーケンサーが1tickごとに進行するか、もし進行しなかった場合にはAnimNotifyStateのEndイベントが発火しないようにするという方法が思いつきます。

誤差によってフレームが進まない動作は表示の滑らかさにも影響を与えるため(0~2フレームのカーソル移動が起こりえる)以下の様な修正が検討できます。

FMovieSceneEvaluationRange FMovieScenePlaybackPosition::PlayTo(FFrameTime InputPosition, EPlayDirection PreferredDirection)
{
...
	// Floor to the current frame number if running frame-locked
	if (EvaluationType == EMovieSceneEvaluationType::FrameLocked)
	{
		UE_LOG(LogMovieScene, VeryVerbose, TEXT("Cursor positon in PlayTo %f %d"), InputPosition.AsDecimal(), InputPosition.FloorToFrame().Value );
		if (InputPosition.GetSubFrame() > 0.999f) //わずかに足りない場合をケアする
		{
			InputPosition = InputPosition.CeilToFrame();
		}
		else
		{
			InputPosition = InputPosition.FloorToFrame();
		}
	}

通常時はこれでカバーできますが、もしカーソルの停止やスローモーションなどの操作を行った場合には、当然カーソルが進まないフレームが生まれEndイベントが発火する可能性はのこります。

シーケンサーで利用されるアニメーションはAnimNotifyStateを使わずにシーケンサーのイベントで代替するか、発火を抑える仕組みを検討する必要があります。

このような操作の可能性はありますか?

[Attachment Removed]

システムはMaxFPS設定によって33.33msに合わせようとしますが、同期タイミングなどにより値は上下し、

最終的な計測値を使って更新を行うためこのような誤差が生まれます。

(UEngine::UpdateTimeAndHandleMaxTickRateをご覧ください)

同関数中に見られるUseFixedTimeStepを設定し固定デルタタイムを使うことも​検討できると思いますが、

サンプル中の0.999fといったマジックナンバーを0.9f程度に調整する手もございます。​

[Attachment Removed]

お世話になっております。

ご返答頂きありがとうございます。

諸々承知いたしました。

挙動としても問題なかったため、サンプル中のマジックナンバーを

0.9fに変更することで対応しようと思います。

ご対応いただきありがとうございました。

[Attachment Removed]

お世話なっております。

プロジェクト側の対応方針として、ご提案いただいたエンジン改造だと影響範囲も広く、リスクも大きいため、

エンジン側のAnimNotifyStateの実装を見直すことで対応できないかを検討することになりました。

度々、ご対応いただいているところ申し訳ないのですが、

AnimNotifyState側で改善を行う場合についてのサンプルコードをご提示いただけないでしょうか?

また、こちらの不具合について、バグトラックの追加は行われるのかどうかもお伺いしたいです。

[Attachment Removed]

AnimInstance側を修正するとなるとNotMovedの場合でもNotifyStateをキューに積むようにすることで改善できそうです。

以下のコードをお試しいただけますでしょうか。

void FAnimMontageInstance::Advance(float DeltaTime, struct FRootMotionMovementParams* OutRootMotionParams, bool bBlendRootMotion)
{
...
				// Delegate has to be called last in this loop
				// so that if this changes position, the new position will be applied in the next loop
				// first need to have event handler to handle it
				// Save off position before triggering events, in case they cause a jump to another position
				const float PositionBeforeFiringEvents = Position;
 
				const bool bHaveNotMoved = (SubStepResult == EMontageSubStepResult::NotMoved);	//追加
				if( bHaveMoved || bHaveNotMoved )	//条件を追加
				{
					// Save position before firing events.
					if (!bInterrupted)
					{
						// Must grab a reference on the stack in case "this" is deleted during iteration
						TWeakObjectPtr<UAnimInstance> AnimInstanceLocal = AnimInstance;
 
						HandleEvents(PreviousSubStepPosition, Position, BranchingPointMarker);
 
						// Break out if we no longer have active montage instances. This may happen when we call UninitializeAnimation from a notify
						if (AnimInstanceLocal.IsValid() && AnimInstanceLocal->MontageInstances.Num() == 0)
						{
							return;
						}
					}
				}

> また、こちらの不具合について、バグトラックの追加は行われるのかどうかもお伺いしたいです。

はい。再現プロジェクトも頂いているので登録を行おうと思います。

[Attachment Removed]

バグを登録させていただきました。 UE-369316 Frame-locked sequencer intermittently triggers notify state end event

[Attachment Removed]

お世話になっております。

バグトラックの追加及び、サンプルコードのご提供ありがとうございます。

適用したところ、不具合改善しておりました。

こちらのサンプルコードについて、質問させていただきたいのですが、

サンプルコード確認したところ、EMontageSubStepResult::NotMoved の場合にも

AnimNotifyのキューを積むよう変更しておりますが、こちらの変更についてリスクなどございますでしょうか?

また、これまでEMontageSubStepResult::NotMoved の場合、キューを積まないようにしていた理由などあるんでしょうか?

[Attachment Removed]

> こちらの変更についてリスクなどございますでしょうか?

NotMovedを返す条件をみるとFAnimMontageInstance::bPlayingがfalseの時や再生速度を0にするような操作を行った場合の挙動が変るかもしれません。

大変恐縮ですが実際にQAを通したものではないので、こちらは実際のタイトルで動作確認を行っていただけますでしょうか。

> これまでEMontageSubStepResult::NotMoved の場合、キューを積まないようにしていた理由などあるんでしょうか

UE4時代に組み込まれた判定となっており、歴史的経緯といったお答えになってしまいます。

シーケンサー側の制御の変化が変ったためにこのような挙動が表面化した可能性もございます。

[Attachment Removed]

お世話になっております。

エンジン改造によるリスクも承知いたしました。

リスクも含めて、プロジェクト側で対応するかどうか検討させていただきます。

ご対応いただきありがとうございました。

[Attachment Removed]

お世話になっております。

ご返答頂きありがとうございます。

質問いただいていた件、返答いたします。

>対策としてはシーケンサーのメニューのフレームレートオプション > Advanced Option > Desired Tick Intervalを30fpsに設定で改善がみられました。こちらを一度お試しいただけますでしょうか。

共有いただいた方法を試しましたが、不具合は発生しており、改善がみれませんでした。

>状況としては同じキャラクターがカットの切り替わりで移動する場合に中間のフレームが表示されることが望ましくないためという認識でよろしいでしょうか?

そちらの認識で間違いないです。

[Attachment Removed]

お世話になっております。

サンプルコードありがとうございます。

こちらを適用しましたが、改善できない場合がありました。

状況としては、UMovieSceneSequencePlayer::Update の引数から渡される DeltaTime が0.033325で、

前回の FFrameTime が Frame=11 / Subframe=0.999148 場合、

今回の FFrameTime が Frame=12 / Subframe=0.998903 と計算され、

提案いただいたソースコードにより、

前回の FFrameTime が Frame=12 / Subframe=0、

今回の FFrameTime が Frame=12 / Subframe=0 となるため、

前回と今回が一致する状況は残っておりました。

UMovieSceneSequencePlayer::Update の引数から渡される DeltaTimeが30fps以上の値が入ってくることが

問題と考えているのですが、こちらについて原因わかりますでしょうか?

[Attachment Removed]