UE中的AnimNotify
UE中的Montage是驱动游戏逻辑中动画的重要组成部分,而其中AnimNotify和AnimNotifyState是使得动画驱动逻辑层重要的桥梁,本文将简单分析下AnimNotify的工作原理,更多会从UE的源码出发,因此全文的可读性会略差。
UE中AnimNotify逻辑主要分为两部分:收集和触发。其中收集发生在动画Evaluate之前,一般来说在MontageUpdate中,而触发则发生在Evaluate之后,即PostAnimEvaluation中,后续我们会就两个主题分开说明。
收集AnimNotifyEvents:
USkinnedMeshComponent::TickComponent
USkeletalMeshComponent::TickPose
UAnimInstance::UpdateAnimation
UAnimInstance::UpdateMontage
FAnimMontageInstance::Advance
Montage::HandleEvent
触发AnimNotifyEvents:
USkeletalMeshComponent::DispatchParallelTickPose
USkeletalMeshComponent::DispatchParallelEvaluationTasks
USkeletalMeshComponent::CompleteParallelAnimationEvaluation
USkeletalMeshComponent::PostAnimEvaluation
UAnimInstance::DispatchQueuedAnimEvents
UAnimInstance::TriggerAnimNotifies
AnimNotify的Trigger
我们先来看AnimNotify的触发逻辑,相比收集逻辑要更简单一些。
在上一段落我们可以看到当触发AnimNotify时代码的调用堆栈。
触发逻辑的核心在UAnimInstance::TriggerAnimNotifies中实现,此时所有触发的Notify已经添加到了NotifyQueue中,具体如何添加的我们后面再看,我们
先看看在这个函数的触发逻辑如何处理,核心复杂的逻辑在于NotifyState的处理,主要的思路是
- ActiveAnimNotifyState 记录了上一帧Active的AnimNotifyStates
- NewActiveAnimNotifyState记录这一帧Active的AnimNotifyStates,并将其从ActiveAnimNotifyState中移除
- 这样ActiveAnimNotifyState就是这一帧需要执行End的AnimNotifyStates
- 最后再将NewActiveAnimNotifyState覆盖ActiveAnimNotifyState
非常经典的类似于AliveList的做法,简化后的代码如下(不是完整的UE源码):
void UAnimInstance::TriggerAnimNotifies(float DeltaSeconds)
{
// 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)
{
int32 ExistingItemIndex = INDEX_NONE;
if (ActiveAnimNotifyState.Find(*AnimNotifyEvent, ExistingItemIndex))
{
// 依然活跃的AnimNotifyState,则从ActiveAnimNotifyState中移除,防止后续的End逻辑
ActiveAnimNotifyState.RemoveAtSwap(ExistingItemIndex, 1, false);
}
else
{
// 如果是未触发过的AnimStateNotifyEvent,则添加准备触发Begin
NotifyStateBeginEvent.Add(AnimNotifyEvent);
}
NewActiveAnimNotifyState.Add(*AnimNotifyEvent);
continue;
}
// Trigger non 'state' AnimNotifies
TriggerSingleAnimNotify(NotifyQueue.AnimNotifies[Index]);
}
}
// AnimStateNotifyEnd
// 对上一帧活跃但是这一帧无效的AnimNotifyState执行End逻辑
// 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))
{
AnimNotifyEvent.NotifyStateClass->NotifyEnd();
}
}
// AnimStateNotifyBegin
// 对新活跃的AnimNotifyState执行Begin逻辑
for (int32 Index = 0; Index < NotifyStateBeginEvent.Num(); Index++)
{
const FAnimNotifyEvent* AnimNotifyEvent = NotifyStateBeginEvent[Index];
if (ShouldTriggerAnimNotifyState(AnimNotifyEvent->NotifyStateClass))
{
AnimNotifyEvent->NotifyStateClass->NotifyBegin();
}
}
// AnimStateNotifyTick
ActiveAnimNotifyState = MoveTemp(NewActiveAnimNotifyState);
for (int32 Index = 0; Index < ActiveAnimNotifyState.Num(); Index++)
{
const FAnimNotifyEvent& AnimNotifyEvent = ActiveAnimNotifyState[Index];
if (ShouldTriggerAnimNotifyState(AnimNotifyEvent.NotifyStateClass))
{
AnimNotifyEvent.NotifyStateClass->NotifyTick();
}
}
}
这里可以看出来,AnimState的驱动取决于这一次TriggerAnimNotifies时传入的AnimStateEvents,同时保证了NotifyEnd始终再NotifyBegin前执行,
但是这样依然可能存在的隐患是,如果在一帧中,需要NotifyEnd的NotifyEvent和需要NotifyStart的NotifyEvent同时存在,那么只用NotifyStart会触发,
而End在下一帧才触发,导致调用顺序的的错误。 PS:这也是需要在需要的时机设置AnimNotify为BranchPoint的原因
AnimNotifyEvent的收集
AnimNotifyEvent的收集相比之下要复杂些,收集的时机在AnimEvalution之前。我们可以先简单思考下,如何收集AnimNotify,思路其实很简单,就是在每次UpdateAnim时,从上一次更新的时间到这次更新的时候内,获取所有时间重叠的AnimNotifies,AnimNotifyTrigger阶段会处理触发的逻辑,当然BranchPoint会马上特殊处理,我们后续再看。
明白了思路之后,再去看源码其实就是一个复杂化的版本,需要考虑动画的循环等等其他因素。一般来说的最佳实践是在montage中放AnimNotify,所以我们从Montage中的动画通知收集看起。
void FAnimMontageInstance::Advance(float DeltaTime, struct FRootMotionMovementParams* OutRootMotionParams, bool bBlendRootMotion)
{
while (bPlaying && MontageSubStepper.HasTimeRemaining() && (++NumIterations < MaxIterations))
{
EMontageSubStepResult SubStepResult = MontageSubStepper.Advance(Position, &BranchingPointMarker);
......
const bool bHaveMoved = (SubStepResult == EMontageSubStepResult::Moved);
if(bHaveMoved)
{
// Save position before firing events.
if (!bInterrupted)
{
HandleEvents(PreviousSubStepPosition, Position, BranchingPointMarker);
}
}
}
}
HandleEvent负责在AnimMontage的更新时收集时间片的AnimNotify,这里其实还包含了AnimNotify在MontageBlendIn和Out时的处理情况,让我们后面再关注,先关注HandleEvent中的实现。
void FAnimMontageInstance::HandleEvents(float PreviousTrackPos, float CurrentTrackPos, const FBranchingPointMarker* BranchingPointMarker)
{
FAnimNotifyContext NotifyContext(TickRecord);
// We already break up AnimMontage update to handle looping, so we guarantee that PreviousPos and CurrentPos are contiguous.
Montage->GetAnimNotifiesFromDeltaPositions(PreviousTrackPos, CurrentTrackPos, NotifyContext);
// Queue active non-'branching point' notifies.
AnimInstance->NotifyQueue.AddAnimNotifies(NotifyContext.ActiveNotifies, NotifyWeight);
}
GetAnimNotifiesFromDeltaPositions是Montage获取AnimNotifies的核心方法,而Montage的我们知道是一种UAnimComposite结构,包含了多个AnimSequences,所以核心肯定是获取当前Montage时间段内的AnimSequence,这里会同时处理Montage的Loop、PlayBackward的情况,让我们跳过
UAnimComposite::GetAnimNotifiesFromDeltaPositions(自行去源码中检索),最终会调用到AnimSequence::GetAnimNotifiesFromDeltaPositions
void UAnimSequenceBase::GetAnimNotifiesFromDeltaPositions(const float& PreviousPosition, const float& CurrentPosition, FAnimNotifyContext& NotifyContext) const
{
for (int32 NotifyIndex=0; NotifyIndex<Notifies.Num(); NotifyIndex++)
{
const FAnimNotifyEvent& AnimNotifyEvent = Notifies[NotifyIndex];
const float NotifyStartTime = AnimNotifyEvent.GetTriggerTime();
const float NotifyEndTime = AnimNotifyEvent.GetEndTriggerTime();
// Note that if you arrive with zero delta time (CurrentPosition == PreviousPosition), only Notify States will be extracted
if( (NotifyStartTime <= CurrentPosition) && (NotifyEndTime > PreviousPosition) )
{
if (NotifyContext.TickRecord)
{
NotifyContext.ActiveNotifies.Emplace(&AnimNotifyEvent, this, NotifyContext.TickRecord->MirrorDataTable);
NotifyContext.ActiveNotifies.Top().GatherTickRecordData(*NotifyContext.TickRecord);
}
else
{
NotifyContext.ActiveNotifies.Emplace(&AnimNotifyEvent, this, nullptr);
}
const bool bHasFinished = CurrentPosition >= NotifyEndTime;
if (bHasFinished)
{
// KeyPoint!!!!!!!!!!!
NotifyContext.ActiveNotifies.Top().AddContextData<UE::Anim::FAnimNotifyEndDataContext>(true);
}
}
}
}
可以看到这里实现逻辑非常简单,只要Notify的时间有重叠就会被捕获,但是对于NotifyEndTime小于当前时间的,也设置一个标记位,这会使得UAnimNotifyLibrary::NotifyStateReachedEnd返回为true。
但是这也会衍生出我们上一个主题最后提到的问题,如果在如果在一帧中,需要NotifyEnd的NotifyEvent和需要NotifyStart的NotifyEvent同时存在,那么只有NotifyStart会触发,而End在下一帧才触发,导致调用顺序的的错误,因为我们并没有考虑在一次采样的时间片里处理更小的时间片。
回到FAnimMontageInstance::HandleEvents中,我们现在获取到了时间片的AnimNotifies,全部存储在了NotifyContext.ActiveNotifies中,然后我们执行下列代码来真正添加需要的AnimNotifies
AnimInstance->NotifyQueue.AddAnimNotifies(NotifyContext.ActiveNotifies, NotifyWeight);
void FAnimNotifyQueue::AddAnimNotifiesToDest(bool bSrcIsLeader, const TArray<FAnimNotifyEventReference>& NewNotifies, TArray<FAnimNotifyEventReference>& DestArray, const float InstanceWeight)
{
for (const FAnimNotifyEventReference& NotifyRef : NewNotifies)
{
const FAnimNotifyEvent* Notify = NotifyRef.GetNotify();
if( Notify)
{
// only add if it is over TriggerWeightThreshold
if (bNotify->TriggerWeightThreshold <= InstanceWeight )
{
// Only add unique AnimNotifyState instances just once. We can get multiple triggers if looping over an animation.
// It is the same state, so just report it once.
Notify->NotifyStateClass ? DestArray.AddUnique(NotifyRef) : DestArray.Add(NotifyRef);
}
}
}
}
这里会传MontageCurrentWeight,Notify基于weight的过滤会在这里处理,Montage触发的情况下bSrcIsLeader会被设置为true