【UE4】动画通知

   本文总结 UE4 中动画通知 AnimNotify(动画通知状态 AnimNotifyState)的源码分析以及问题总结。

一、动画通知简介

   动画通知是 UE4 中主要配置在动画(Animation)或者动画蒙太奇(AnimMontage)上的通知,通过动画的运行驱动通知的触发。通知分为两种:

  1. AnimNotify(动画通知) – 没有时长;只有 Notify 方法;详见 AnimNotify.h
  2. AnimNotifyState(动画通知状态) – 有时长(TotalDuration);有 NotifyBegin, NotifyTick, NotifyEnd 方法;详见 AnimNotifyState.h

  对于 AnimNotifyState官方描述中说到:

  • 可以保证从 Notify Begin Event 开始
  • 可以保证从 Notify End Event 结束
  • 可以保证 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

另外,值得注意的是:

  1. 动画通知默认在 DedicateServer、主控端、模拟段 都会执行。如果不想在 DS 上执行,可以在选项中将下边这个选项取消勾选。
    在这里插入图片描述

  2. 对于同一个 Montage 或者 Animation 上的通知,指针是同一个,所以在通知中如果存储了数据,两个同样的角色,一起释放同一个 Montage 的时候,会出现两个通知修改同一片数据的情况;

  3. 同一个 Montage 上多个同类型的通知,指针是不一样的,GetName() 出来的是 xxx_C_1、xxx_C_2 这样的

二、动画通知的触发

  动画通知的上述方法的触发,主要是从 UAnimInstance::TriggerAnimNotifies 中触发的(还有其他的后边会说一部分)。

2.1 TriggerAnimNotifies 源码分析

  在 Tick 中,AnimInstance 中的 NotifyQueue 里的 AnimNotifiesFAnimNotifyQueue 类型),会添加这一帧在的动画通知(AnimNotify、AnimNotifyState),然后在 TriggerAnimNotifies 执行这一帧的所有动画通知,执行顺序为:

  1. 所有 AnimNotifyNotify
  2. 所有上一还在,但是这一帧已经不在的 AnimNotifyStateNotifyEnd
  3. 上一帧没在,这一帧新加的 AnimNotifyStateNotifyBegin
  4. 这一帧在的所有的 AnimNotifyStateNotifyTick(即 Begin 的同一帧就会 Tick)

  对于 AnimNotify 来说,只有 Notify,所以比较简单,不进行讨论。接下来对 AnimNotifyState 进行重点分析:
   AnimNotifyState 的触发核心是四个数组:

  1. ActiveAnimNotifyState: 记录上一帧在 Active 的 AnimNotifyState(简称 Act)
  2. NotifyQueue.AnimNotifies: 记录这一帧 Montage 上所有 Notify(包括 Notify 和 NotifyState)
  3. NewActiveAnimNotifyState(局部): 通过 1 和 2 计算出有哪些是这一帧新的 NotifyState(简称 NewAct)
  4. NotifyStateBeginEvent(局部):只用来触发新的 NotifyState 的 NotifyBegin
2.2 TriggerAnimNotifies 完整流程分析
1. 创建 NewActiveAnimNotifyState
// Array that will replace the 'ActiveAnimNotifyState' at the end of this function. 
TArray<FAnimNotifyEvent> NewActiveAnimNotifyState; 
NewActiveAnimNotifyState.Reserve(NotifyQueue.AnimNotifies.Num());
2. 最关键一步

  把 ActiveAnimNotifyState 中这一帧还在的删除掉,剩下的就是上一帧在,这一帧不在的,用于后边触发 NotifyEnd
  把上一帧不在,这一帧在的,放到 NotifyStateBeginEvent 里,用于后边触发 NotifyBegin
  触发所以的 AnimNotify

// 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);
    }
}
3. 触发 Act 里的所有 NotifyEnd
// Send end notification to AnimNotifyState not active anymore. 
for (int32 Index = 0; Index < ActiveAnimNotifyState.Num(); ++Index) 
{
    
     
    const FAnimNotifyEvent& AnimNotifyEvent = ActiveAnimNotifyState[Index]; 
    if (AnimNotifyEvent.NotifyStateClass && ShouldTriggerAnimNotifyState(AnimNotifyEvent.NotifyStateClass)) 
    {
    
     
        TRACE_ANIM_NOTIFY(this, AnimNotifyEvent, End); 
        AnimNotifyEvent.NotifyStateClass->NotifyEnd(
        	SkelMeshComp, 
        	Cast<UAnimSequenceBase>(AnimNotifyEvent.NotifyStateClass->GetOuter())); 
    } 
    // blabla ActiveAnimNotifyState 是否已经失效检查
}
4. 触发 NotifyStateBeginEvent 里的所有 NotifyBegin
// Call 'NotifyBegin' event on freshly added AnimNotifyState. 
for (const FAnimNotifyEvent* AnimNotifyEvent : NotifyStateBeginEvent) 
{
    
     
    if (ShouldTriggerAnimNotifyState(AnimNotifyEvent->NotifyStateClass)) 
    {
    
     
        TRACE_ANIM_NOTIFY(this, *AnimNotifyEvent, Begin); 
        AnimNotifyEvent->NotifyStateClass->NotifyBegin(
        	SkelMeshComp, 
        	Cast<UAnimSequenceBase>(AnimNotifyEvent->NotifyStateClass->GetOuter()), 
        	AnimNotifyEvent->GetDuration()); 
    } 
}
5. MoveTemp
// Switch our arrays. 
ActiveAnimNotifyState = MoveTemp(NewActiveAnimNotifyState);

  把 NewAct 给 Act,因为 Act 是成员变量,NewAct 是局部变量。

6. 触发 Act 里所有的 NotifyTick
// Tick currently active AnimNotifyState 
for(const FAnimNotifyEvent& AnimNotifyEvent : ActiveAnimNotifyState) 
{
    
     
    if (ShouldTriggerAnimNotifyState(AnimNotifyEvent.NotifyStateClass)) 
    {
    
     
        TRACE_ANIM_NOTIFY(this, AnimNotifyEvent, Tick); 
        AnimNotifyEvent.NotifyStateClass->NotifyTick(
        	SkelMeshComp, 
        	Cast<UAnimSequenceBase>(AnimNotifyEvent.NotifyStateClass->GetOuter()), 
        	DeltaSeconds); 
    }
}

另外,值得注意的是:

  1. 由于判断是否触发 NotifyEnd 的判断方法是通过 A 通知上一帧还在,这一帧不在来判断,所以在看起来通知的最后一帧时,是不会触发 NotifyEnd 的,即首尾相连的两个通知,一定是后边的先 Begin,下一帧前边的才 End,甚至不用首尾相连,而是首尾在同一帧内(如下),同样是后边的先 Begin,下一帧前边的才 End。
    在这里插入图片描述

三、动画通知的问题总结

  详见

猜你喜欢

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