【UE4】AnimNotify 相关源码分析

本文内容

   本文记录了 UE4 中动画通知触发的流程,以及触发 AnimNotifyState Begin/End 的源码分析。总结如下:

1. AnimNotify

   在 Tick 内检测到这一帧内有 AnimNotify 进入,就直接触发,实际配置在这一帧内的位置不重要(比如 0.016 ~ 0.033)

2. AnimNotifyState
  • 在 Tick 内检测有哪些上一帧还在 Active,这一帧以及没有了的 AnimNotifyState,记录并在这帧内触发所有的 NotifyEnd;
  • 在 Tick 内检测这一帧有哪些新的 AnimNotifyState,记录并在这帧内触发所有新的的 NotifyBegin;
  • 在 Tick 内检测这一帧有在 Active,并且还在的 AnimNotifyState,这帧内触发所有的 NotifyTick;

  上边三条是按顺序执行的,即一帧内,所有 NotifyEnd 先于所有 NotifyBegin , 先于所有 NotifyTick。
  但是 End 事件一定是检测到这个 AnimNotifyState 这一帧内没有了,才会触发,所有一定会玩一帧,但是 Begin 一定不会晚一帧,所有在动画条上,一帧内的 NotifyEnd 一定比这帧内的 NotifyBegin 晚一帧触发

一、问题描述

  UE4 中的动画通知分为两种:

  1. AnimNotify
  2. AnimNotifyState

  第一种是只有 Begin 的触发一次的 Notify,第二种是有 Begin 和 End 以及 Tick 的有持续时间(在动画条上有长度的 Notify)。本文讨论内容只关于 AnimNotifyState。

  对于 AnimNotifyState,UE4 官方文档中说明:

  • 可以保证从 Notify Begin Event 开始
  • 可以保证从 Notify EndEvent 结束
  • 可以保证 Notify Tick 是在 Notify Begin 和 Notify End event 之间的
  • 不能保证 Anim Notifies (normal or state) 的顺序,如果把两个 AnimNotifyState 首尾相连,并不能保证前一个 Notify 的 End 在后一个的 Begin 之前。只应该在这里进行和其他 Notify 之间没有关联的单独操作。原文:

The order between different Anim Notifies (normal or state) is not guaranteed.
If you put two Anim Notify States next to each other, the first one is not guaranteed to end before the next one starts.
Only use this for individual actions which do not rely on other Notifies.

  即两个 AnimNotifyState,A 和 B,并不能保证执行顺序是:

A.Begin --> A.End --> B.Begin --> B.End

  而有可能是:

A.Begin --> B.Begin --> A.End --> B.End

  且由于 AnimNotifyState 过程中跳出,会自动调用这个 Notify 的 End,所以出现上述问题有三种可能方式

  1. 连续两段 AnimNotifyState,前一段的 End 和后一段的 Begin 离得很近
  2. 在一个动画条内,JumpSection 从一个 AnimNotifyState 内跳到一个 Section 开头,而这个 Section 开头就有一个 B Notify;
  3. 在第一个 Montage 被 Stop 的时候,正处于 A Notify 之内,而第二段要播的 Montage 第0帧就有一个顶头的 B Notify;

  上述三种方式都有可能出现 B.Begin --> A.End 的情况!

二、快速复现

  修改任意一个Montage,以及一个 AnimNotifyState 修改为如下方式,注意:第一段的 End 和第二
段的 Begin 都不是首尾相连的,但是 End 和 Begin 在同一帧内!!

在这里插入图片描述
  然后在进游戏开枪(或者在编辑器里,loop改成一次,点动画的play,但是编辑器里更难复现),就会出现 Begin -> Begin -> End -> End,也就是第二段的 End 先于第一段的 Begin!
  离远一点就不会有这个问题!!

三、源码分析

1. 引擎源码

  UE4 的 AnimNotify(AnimNotifyState),只要是 Queued(默认),都是在 Tick 里调用,放到 Queue 中并处理的,具体流程如下(每一帧):

void USkinnedMeshComponent::TickComponent(float DeltaTime, blabla)
{
    
    
	// ...
	// Tick Pose first
	if (ShouldTickPose())
	{
    
    
		TickPose(DeltaTime, false);
	}

	// If we have been recently rendered, and bForceRefPose has been on for at least a frame, or the LOD changed, update bone matrices.
	if( ShouldUpdateTransform(bLODHasChanged) )
	{
    
    
		// ...
		RefreshBoneTransforms(ThisTickFunction);
	}
	// ...
}

  RefreshBoneTransforms --> 一些调用链。。 --> FinalizeBoneTransform

void USkeletalMeshComponent::FinalizeBoneTransform() 
{
    
    
	Super::FinalizeBoneTransform();

	// After pose has been finalized, dispatch AnimNotifyEvents in case they want to use up to date pose.
	// (For example attaching particle systems to up to date sockets).

	/
	// Notify / Event Handling!
	// This can do anything to our component (including destroy it) 
	// Any code added after this point needs to take that into account
	/
	ConditionallyDispatchQueuedAnimEvents();
	// ...
}

  即在 ConditionallyDispatchQueuedAnimEvents 里处理 Notify / Event Handling!。

void USkeletalMeshComponent::ConditionallyDispatchQueuedAnimEvents()
{
    
    
	if (bNeedsQueuedAnimEventsDispatched)
	{
    
    
		bNeedsQueuedAnimEventsDispatched = false;

		for (UAnimInstance* LinkedInstance : LinkedInstances)
		{
    
    
			LinkedInstance->DispatchQueuedAnimEvents();
		}

		if (AnimScriptInstance)
		{
    
    
			AnimScriptInstance->DispatchQueuedAnimEvents();
		}

		if (PostProcessAnimInstance)
		{
    
    
			PostProcessAnimInstance->DispatchQueuedAnimEvents();
		}
	}
}
void UAnimInstance::DispatchQueuedAnimEvents()
{
    
    
	// now trigger Notifies
	TriggerAnimNotifies(GetProxyOnGameThread<FAnimInstanceProxy>().GetDeltaSeconds());

	// Trigger Montage end events after notifies. In case Montage ending ends abilities or other states, we make sure notifies are processed before montage events.
	TriggerQueuedMontageEvents();

	// ...
}

  在 TriggerAnimNotifies 里触发 Notifies。

void UAnimInstance::TriggerAnimNotifies(float DeltaSeconds)
{
    
    
	SCOPE_CYCLE_COUNTER(STAT_AnimTriggerAnimNotifies);
	USkeletalMeshComponent* SkelMeshComp = GetSkelMeshComponent();

	// Array that will replace the 'ActiveAnimNotifyState' at the end of this function.
	TArray<FAnimNotifyEvent> NewActiveAnimNotifyState;
	NewActiveAnimNotifyState.Reserve(NotifyQueue.AnimNotifies.Num());

	// AnimNotifyState freshly added that need their 'NotifyBegin' event called.
	TArray<const FAnimNotifyEvent *> NotifyStateBeginEvent;

	for (int32 Index=0; Index<NotifyQueue.AnimNotifies.Num(); Index++)
	{
    
    
		if(const FAnimNotifyEvent* AnimNotifyEvent = NotifyQueue.AnimNotifies[Index].GetNotify())
		{
    
    
			// AnimNotifyState
			if (AnimNotifyEvent->NotifyStateClass)
			{
    
    
				if (!ActiveAnimNotifyState.RemoveSingleSwap(*AnimNotifyEvent, false))
				{
    
    
					// Queue up calls to 'NotifyBegin', so they happen after 'NotifyEnd'.
					NotifyStateBeginEvent.Add(AnimNotifyEvent);
				}
				NewActiveAnimNotifyState.Add(*AnimNotifyEvent);
				continue;
			}

			// Trigger non 'state' AnimNotifies
			TriggerSingleAnimNotify(AnimNotifyEvent);
		}
	}

	// Send end notification to AnimNotifyState not active anymore.
	for (int32 Index = 0; Index < ActiveAnimNotifyState.Num(); ++Index)
	{
    
    
		// 触发 NotifyState 的 NotifyEnd !!!!!
	}

	// Call 'NotifyBegin' event on freshly added AnimNotifyState.
	for (const FAnimNotifyEvent* AnimNotifyEvent : NotifyStateBeginEvent)
	{
    
    
		// 触发 NotifyState 的 NotifyBegin !!!!!
	}

	// Switch our arrays.
	ActiveAnimNotifyState = MoveTemp(NewActiveAnimNotifyState);

	// Tick currently active AnimNotifyState
	for(const FAnimNotifyEvent& AnimNotifyEvent : ActiveAnimNotifyState)
	{
    
    
		// 触发 NotifyState 的 NotifyTick !!!!!
	}
}

  即,源码保证了,在同一帧内的所有 AnimNotifyState,所有要触发的 End,在所有要触发的 Begin 前边
  但是只要这一帧内,有 Begin,就要触发;这一帧内有 End,一定是下一帧检测到没有这个 Notify 了,才触发 End(即延后一帧 End。这就导致End 可能比 Begin 后一帧才触发。

2. 错误情况

在这里插入图片描述
  这里 2 和 3 之间其实是两帧(因为这个显示是按照 30 帧/s 算的,“2” 这里是 0.134s,“3” 这里是 0.167s),其中相差了 0.033 秒,即 30帧/s 下的一帧时间。如果游戏运行是 60帧/s ,那么,上图两个notify,前边的 End 和后边的 Begin 都在 2 到 3 这两帧内的前一帧中(下面称为 第五帧,称后一帧为 第六帧)。
  这样就会出现在 第五帧里,检测到新的 Projectile_1 出现,但是没有 Active,所以直接 Begin;注意这里没有检测到正在 Active 的 Projectile_0 消失(虽然这一帧内有 End),所以不 End。在第六帧里,检测到 Projectile_0 消失,但是在 Active,所以 End。

NotifyQueue.AnimNotifies.Num() = 0

NotifyQueue.AnimNotifies.Num() = 0

检测到有新的来了,直接 Begin
NotifyQueue.AnimNotifies.Num() = 1
ActiveAnimNotifyState:
AnimNotifyStateSpawnProjectile_0
===== NotifyBegin – NotifyName = AnimNotifyStateSpawnProjectile_0

NotifyQueue.AnimNotifies.Num() = 1
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_0
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_0

… 同上

… 同上

检测到有新的来了,直接 Begin;虽然这一帧有 End,但是并不触发,因为 Notify 还在
NotifyQueue.AnimNotifies.Num() = 2
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_0
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_0
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_1
===== NotifyBegin – NotifyName = AnimNotifyStateSpawnProjectile_1

检测 Notify 不在了,但是在 Avtive 队列中,触发 End
NotifyQueue.AnimNotifies.Num() = 1
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_0
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_1
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_1
===== NotifyEnd – NotifyName = AnimNotifyStateSpawnProjectile_0

NotifyQueue.AnimNotifies.Num() = 1
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_1
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_1

… 同上

… 同上

… 同上

… 同上

检测 Notify 不在了,但是在 Avtive 队列中,触发 End
NotifyQueue.AnimNotifies.Num() = 0
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_1
===== NotifyEnd – NotifyName = AnimNotifyStateSpawnProjectile_1

NotifyQueue.AnimNotifies.Num() = 0

NotifyQueue.AnimNotifies.Num() = 0

  其中 NotifyQueue.AnimNotifies 是检测到的 AnimNotify(包括 AnimNotify 和AnimNotifyState);ActiveAnimNotifyState 是在 Active 的 AnimNotifyState。错误情况就会出现 ActiveAnimNotifyState 有两个的情况

3. 正确情况

在这里插入图片描述
  即在 1.5 到 2 这一帧里,没有逻辑,在 2 ~ 2.5 这一帧里,检测到 Projectile_0 没有了,但是在 Active,所以 End;且 Projectile_1 出现了,但是没有 Active,所以 Begin。一帧内的 End 一定先于 Begin,所以没有问题。

NotifyQueue.AnimNotifies.Num() = 0

NotifyQueue.AnimNotifies.Num() = 0

检测到有新的来了,直接 Begin
NotifyQueue.AnimNotifies.Num() = 1
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_0
===== NotifyBegin – AnimNotifyStateSpawnProjectile_0

… 同上

… 同上

… 同上

检测 Notify 不在了,但是在 Avtive 队列中,触发 End;检测到有新的来了,直接 Begin
NotifyQueue.AnimNotifies.Num() = 1
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_0
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_1
===== NotifyEnd – AnimNotifyStateSpawnProjectile_0
===== NotifyBegin – AnimNotifyStateSpawnProjectile_1

NotifyQueue.AnimNotifies.Num() = 1
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_1
NotifyState->NotifyName = AnimNotifyStateSpawnProjectile_1

… 同上

… 同上

… 同上

… 同上

… 同上

检测 Notify 不在了,但是在 Avtive 队列中,触发 End
NotifyQueue.AnimNotifies.Num() = 0
ActiveAnimNotifyState: AnimNotifyStateSpawnProjectile_1
===== NotifyEnd – AnimNotifyStateSpawnProjectile_1

NotifyQueue.AnimNotifies.Num() = 0

NotifyQueue.AnimNotifies.Num() = 0

相关文档

  1. 官方文档:Animation Notifications (Notifies)

猜你喜欢

转载自blog.csdn.net/Bob__yuan/article/details/109816377