前言 一些对于AnimMontage的相关研究和分析。
基本架构 可以将动画蒙太奇相关设计的几个类大体上如此表示
大体上可以总结出如下四点:
AnimInstance 是和SkeletalMeshComponent 一一绑定的,用来控制对应SkeletalMeshComponent 的动画情况
AnimMontage 是编辑器中蓝图资产的基类,用来编辑相关动画的
AnimMontageInstance 是实际播放蒙太奇时由AnimInstance 创建的实例,它会绑定到对应的资产AnimMontage ,从而获得在里面编辑出的数据
AnimNotify 和AnimNotifyState 都是对应AnimMontage ,同一个Montage 中会多个Notify 是不同的实例,但是播放时不同MontageInstance 的实例是同一个,因此不能在Notify 中储存和Actor 相关的状态
动画蒙太奇 播放蒙太奇 每次在尝试播放蒙太奇时,都会调用到UAnimInstance::Montage_PlayInternal()
这个函数作为最终的入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 float UAnimInstance::Montage_PlayInternal (UAnimMontage* MontageToPlay, const FMontageBlendSettings& BlendInSettings, float InPlayRate , EMontagePlayReturnType ReturnValueType , float InTimeToStartMontageAt , bool bStopAllMontages ) { if (MontageToPlay && (MontageToPlay->GetPlayLength () > 0.f ) && MontageToPlay->HasValidSlotSetup ()) { if (CurrentSkeleton && MontageToPlay->GetSkeleton ()) { const FName NewMontageGroupName = MontageToPlay->GetGroupName (); if (bStopAllMontages) { StopAllMontagesByGroupName (NewMontageGroupName, BlendInSettings); } if (MontageToPlay->bEnableRootMotionTranslation || MontageToPlay->bEnableRootMotionRotation) { FAnimMontageInstance* ActiveRootMotionMontageInstance = GetRootMotionMontageInstance (); if (ActiveRootMotionMontageInstance) { ActiveRootMotionMontageInstance->Stop (BlendInSettings); } } FAnimMontageInstance* NewInstance = new FAnimMontageInstance (this ); const float MontageLength = MontageToPlay->GetPlayLength (); NewInstance->Initialize (MontageToPlay); NewInstance->Play (InPlayRate, BlendInSettings); NewInstance->SetPosition (FMath::Clamp (InTimeToStartMontageAt, 0.f , MontageLength)); MontageInstances.Add (NewInstance); ActiveMontagesMap.Add (MontageToPlay, NewInstance); if (MontageToPlay->HasRootMotion ()) { RootMotionMontageInstance = NewInstance; } OnMontageStarted.Broadcast (MontageToPlay); return (ReturnValueType == EMontagePlayReturnType::MontageLength) ? MontageLength : (MontageLength / (InPlayRate*MontageToPlay->RateScale)); } } return 0.f ; }
蒙太奇更新 蒙太奇的每次更新都是由AnimInstance推动所有MontageInstances 更新。
混合权重更新 首先会更新MontageInstance 上的FAlphaBlend ,这个结构用于实现该MontageInstance 的混合信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void FAnimMontageInstance::UpdateWeight (float DeltaTime) { PreviousWeight = Blend.GetBlendedValue (); Blend.Update (DeltaTime); if (Blend.GetBlendTimeRemaining () < 0.0001f ) { ActiveBlendProfile = nullptr ; } NotifyWeight = FMath::Max (PreviousWeight, Blend.GetBlendedValue ()); }
在Blend 更新后,MontageInstance 会保存一个混合权重到角色身上。
这个权重用在蒙太奇实例将动画通知事件(AnimNotify /AnimNotifyState )添加到AnimInstance 时,此时会做一个检测,如果权重过小就会被抛弃。
蒙太奇步进 由AnimInstance 驱动的的步进,主要分为四个阶段,从Gameplay角度来分析的话,主要逻辑在Advanced 阶段
MontageSync_PreUpdate:准备蒙太奇的同步组状态,确保在推进动画之前,一些必要的状态或数据是一致的。
Advance:更新蒙太奇的播放状态,处理动画事件和根运动的应用。
check: 验证可用性,避免上一步中因为动画事件导致蒙太奇或Actor的销毁导致的异常。
MontageSync_PostUpdate:确保在蒙太奇更新后,状态保持一致,处理完必要的后续逻辑。
MontageSubStepper步进 这是蒙太奇步进的辅助类,目的是为了将这一段时间的更新进行准确地触发。
因为蒙太奇存在如片段section 跳转和动画通知animNotfiy 的触发影响到蒙太奇播放的速率、片段等问题,一旦步进过大导致中间可能触发的事件后没能正常触发后,就会导致播放结果错误。
举个例子,某次更新经过的时间中会触发两个跳转到A和B。但是实际上,应该先执行跳转到A,然后在A后面重新播放剩下的时间。如果这个中途中还有跳转到C,那就还得再跳转到C。而时间中的B跳转就因为已经跳转走了所以不会触发。
所以会尝试进行多次的精准更新。每次更新中一旦出现跳转后,就要根据跳转的情况重新尝试更新后续时间。
目前最大更新限制在10次,所以步长太多跳转多次的情况下仍然存在有BUG的可能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 EMontageSubStepResult FMontageSubStepper::Advance (float & InOut_P_Original, const FBranchingPointMarker** OutBranchingPointMarkerPtr) { if (MontageInstance->ForcedNextToPosition.IsSet ()) { const float NewPosition = MontageInstance->ForcedNextToPosition.GetValue (); if (MontageInstance->ForcedNextFromPosition.IsSet ()) { InOut_P_Original = MontageInstance->ForcedNextFromPosition.GetValue (); } DeltaMove = NewPosition - InOut_P_Original; PlayRate = DeltaMove / TimeRemaining; bPlayingForward = (DeltaMove >= 0.f ); TimeStretchMarkerIndex = INDEX_NONE; } else { PlayRate = MontageInstance->PlayRate * Montage->RateScale; const bool bAttemptTimeStretchCurve = Montage->TimeStretchCurve.IsValid () && !FMath::IsNearlyEqual (PlayRate, 1.f ); if (bAttemptTimeStretchCurve) { ConditionallyUpdateTimeStretchCurveCachedData (); } if (!bAttemptTimeStretchCurve || !bHasValidTimeStretchCurveData) { bPlayingForward = (PlayRate > 0.f ); DeltaMove = TimeRemaining * PlayRate; TimeStretchMarkerIndex = INDEX_NONE; } else { float P_Target = FindMontagePosition_Target (InOut_P_Original); P_Target += bPlayingForward ? TimeRemaining : -TimeRemaining; P_Target = TimeStretchCurveInstance.Clamp_P_Target (P_Target); const float NewP_Original = FindMontagePosition_Original (P_Target); DeltaMove = NewP_Original - InOut_P_Original; PlayRate = DeltaMove / TimeRemaining; } } if (OutBranchingPointMarkerPtr) { *OutBranchingPointMarkerPtr = Montage->FindFirstBranchingPointMarker (InOut_P_Original, InOut_P_Original + DeltaMove); if (*OutBranchingPointMarkerPtr) { DeltaMove = (*OutBranchingPointMarkerPtr)->TriggerTime - InOut_P_Original; } } { const float OldDeltaMove = DeltaMove; if (bPlayingForward) { const float MaxSectionMove = CurrentSectionLength - PositionInSection; if (DeltaMove >= MaxSectionMove) { DeltaMove = MaxSectionMove; bReachedEndOfSection = true ; } } else { const float MinSectionMove = - PositionInSection; if (DeltaMove <= MinSectionMove) { DeltaMove = MinSectionMove; bReachedEndOfSection = true ; } } if (OutBranchingPointMarkerPtr && *OutBranchingPointMarkerPtr && (OldDeltaMove != DeltaMove)) { *OutBranchingPointMarkerPtr = nullptr ; } } if (FMath::Abs (DeltaMove) > 0.f ) { InOut_P_Original += DeltaMove; const float TimeStep = DeltaMove / PlayRate; TimeRemaining = FMath::Max (TimeRemaining - TimeStep, 0.f ); return EMontageSubStepResult::Moved; } else { return EMontageSubStepResult::NotMoved; } }
蒙太奇Advanced 蒙太奇的Advanced 主要依靠 MontageSubStepper 来将一次步进拆分成多次以实现更细致地逐步推进的。这里主要关注Advance事如何触发的动画通知。
每次MontageSubStepper 步进时都会尝试处理这段时间内的根运动情况、动画通知并自动判断是否结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 while (bPlaying && MontageSubStepper.HasTimeRemaining () && (++NumIterations < MaxIterations)){ const float PreviousSubStepPosition = Position; const FBranchingPointMarker* BranchingPointMarker = nullptr ; EMontageSubStepResult SubStepResult = MontageSubStepper.Advance (Position, &BranchingPointMarker); const bool bPlayingForward = MontageSubStepper.GetbPlayingForward (); const float SubStepDeltaMove = MontageSubStepper.GetDeltaMove (); DeltaTimeRecord.Delta += SubStepDeltaMove; const bool bPlayingForward = MontageSubStepper.GetbPlayingForward (); if (!IsStopped () && bEnableAutoBlendOut) { const int32 CurrentSectionIndex = MontageSubStepper.GetCurrentSectionIndex (); const int32 NextSectionIndex = bPlayingForward ? NextSections[CurrentSectionIndex] : PrevSections[CurrentSectionIndex]; if (NextSectionIndex == INDEX_NONE) { } } const bool bHaveMoved = (SubStepResult == EMontageSubStepResult::Moved); if (bHaveMoved) { if (bExtractRootMotion && AnimInstance.IsValid () && !IsRootMotionDisabled ()) { const FTransform RootMotion = Montage->ExtractRootMotionFromTrackRange (PreviousSubStepPosition, Position); if (bBlendRootMotion) { const float Weight = Blend.GetBlendedValue (); AnimInstance.Get ()->QueueRootMotionBlend (RootMotion, Montage->SlotAnimTracks[0 ].SlotName, Weight); } else { OutRootMotionParams->Accumulate (RootMotion); } } } const float PositionBeforeFiringEvents = Position; if (bHaveMoved) { if (!bInterrupted) { TWeakObjectPtr<UAnimInstance> AnimInstanceLocal = AnimInstance; HandleEvents (PreviousSubStepPosition, Position, BranchingPointMarker); if (AnimInstanceLocal.IsValid () && AnimInstanceLocal->MontageInstances.Num () == 0 ) { return ; } } } if (MontageCVars::bEndSectionRequiresTimeRemaining == false || MontageSubStepper.HasTimeRemaining ()) { if (MontageSubStepper.HasReachedEndOfSection () && !BranchingPointMarker && (PositionBeforeFiringEvents == Position)) { / const int32 CurrentSectionIndex = MontageSubStepper.GetCurrentSectionIndex (); const int32 RecentNextSectionIndex = bPlayingForward ? NextSections[CurrentSectionIndex] : PrevSections[CurrentSectionIndex]; if (RecentNextSectionIndex != INDEX_NONE) { float LatestNextSectionStartTime, LatestNextSectionEndTime; Montage->GetSectionStartAndEndTime (RecentNextSectionIndex, LatestNextSectionStartTime, LatestNextSectionEndTime); const float EndOffset = UE_KINDA_SMALL_NUMBER / 2.f ; Position = bPlayingForward ? LatestNextSectionStartTime : (LatestNextSectionEndTime - EndOffset); SubStepResult = EMontageSubStepResult::Moved; } else { break ; } } } if (SubStepResult == EMontageSubStepResult::NotMoved) { break ; } }
蒙太奇更新时处理动画通知事件 动画通知事件会分成两种类型触发时间类型:
队列 :这些动画通知会在蒙太奇更新时触发插入队列中,这一轮动画更新完成后再运行逻辑。
分支点 :会在蒙太奇更新时直接触发对应逻辑。所以可用来做蒙太奇跳转播放的逻辑,在蒙太奇播放中可以正确的执行蒙太奇跳转。
但是这个分支点的命名方式真的很怪,其实它只是标注个动画通知能够瞬时触发,触发时可能会导致蒙太奇播放跳转。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 void FAnimMontageInstance::HandleEvents (float PreviousTrackPos, float CurrentTrackPos, const FBranchingPointMarker* BranchingPointMarker) { if (bInterrupted) { return ; } FAnimNotifyContext NotifyContext (TickRecord) ; { Montage->GetAnimNotifiesFromDeltaPositions (PreviousTrackPos, CurrentTrackPos, NotifyContext); Montage->FilterOutNotifyBranchingPoints (NotifyContext.ActiveNotifies); AnimInstance->NotifyQueue.AddAnimNotifies (NotifyContext.ActiveNotifies, NotifyWeight); } { TMap<FName, TArray<FAnimNotifyEventReference>> NotifyMap; for (auto SlotTrack = Montage->SlotAnimTracks.CreateIterator (); SlotTrack; ++SlotTrack) { TArray<FAnimNotifyEventReference>& CurrentSlotNotifies = NotifyMap.FindOrAdd (SlotTrack->SlotName); NotifyContext.ActiveNotifies.Reset (); SlotTrack->AnimTrack.GetAnimNotifiesFromTrackPositions (PreviousTrackPos, CurrentTrackPos, NotifyContext); Swap (CurrentSlotNotifies, NotifyContext.ActiveNotifies); } AnimInstance->NotifyQueue.AddAnimNotifies (NotifyMap, NotifyWeight); } if (!UpdateActiveStateBranchingPoints (CurrentTrackPos)) { return ; } if (BranchingPointMarker) { BranchingPointEventHandler (BranchingPointMarker); } }
结束蒙太奇 蒙太奇的结束存在两个重要的概念节点,BlendOut 和Ended ,BlendOut 代表着蒙太奇实例开始混出,Ended 则代表着蒙太奇实例的彻底销毁。
区分这两种的缘故是因为,为了更好的衔接动画表现。在从一个蒙太奇切换到另一个动画时,通过两个动画的权重配合避免动作的直接转换过于突兀。这个阶段对于蒙太奇来说叫做BlendOut混出 ,在这个阶段下,逻辑上该蒙太奇已经结束了,但是对应的功能表现上还仍需要使用,因此区分了两种情况。
BlendOut 触发混出调用的即是 FAnimMontageInstance::Stop(const FMontageBlendSettings& InBlendOutSettings, bool bInterrupt)
这个函数。
再上一层则往往在AnimInstance 中的对应功能了。比较常见的情况是播放新蒙太奇时,自动打断了同组的所有蒙太奇而触发的蒙太奇混出。
在混出开始时会将自身在动画实例的活跃情况清空,并且在AnimInstance 上加入Blendingout 的通知事件。
1 2 3 AnimInstance->OnMontageInstanceStopped (*this ); AnimInstance->QueueMontageBlendingOutEvent (FQueuedMontageBlendingOutEvent (Montage, bInterrupted, OnMontageBlendingOutStarted));
蒙太奇提供一个对于状态的检测,判断依据是蒙太奇的Blend 的目标值已经被置为0。即蒙太奇一旦被通知混出后,逻辑上已经是停止状态了。
1 bool IsStopped () const { return Blend.GetDesiredValue () == 0.f ; }
Ended 结束即是在蒙太奇更新的步进Advanced 阶段中。如果已经停止状态且混出完成后,直接Terminate 并发送Endded 的事件。
蒙太奇的获取动画Slot 其实就是播放时,获取到每个蒙太奇,并根据slot查询获得对应的动画资源,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 for (const FMontageEvaluationState& EvalState : GetMontageEvaluationData ()) { const UAnimMontage* const Montage = EvalState.Montage.Get (); if (Montage->IsValidSlot (SlotNodeName)) { FAnimTrack const * const AnimTrack = Montage->GetAnimationData (SlotNodeName); FAnimExtractContext ExtractionContext (static_cast <double >(EvalState.MontagePosition), Montage->HasRootMotion() && RootMotionMode != ERootMotionMode::NoRootMotionExtraction, EvalState.DeltaTimeRecord) ; FAnimationPoseData NewAnimationPoseData (NewPose) ; AnimTrack->GetAnimationPose (NewAnimationPoseData, ExtractionContext);
动画通知 动画通知有如下两个分类方式,共四种组合
AnimNotify / AnimNotifyState
AnimNotify :固定在特定帧上的一次触发,多用于粒子、音效等瞬时触发,只有Notify事件
AnimNotifyState :具备begin、end以及Tick事件,用来执行某段固定时间内的逻辑,如打击判定等
Queue/ BranchPoint
Queue :在每一次蒙太奇Advance 时存入队列,直到本次动画更新完成后统一触发逻辑,大部分动画通知事件都是这一类
BranchPoint :在蒙太奇Advance 时直接触发,每次MontageSubStepper::Advance
时只触发一个,往往用于蒙太奇中要调整播放的情况下,性能高
AnimNotifyState AnimNofityState 是UE中,为动画或蒙太奇播放过程中触发特定逻辑的一种机制。
相较于AnimNotify 只会在固定时间轴上触发的离散机制,AnimNotifyState 提供了完整的开始、结束、Tick功能,因此在游戏开发中,更容易用AnimNotifyState 来作为动画相关逻辑机制的载体。
AnimNotifyState实例绑定于动画 在开发中可以发现,在一个动画中,添加多个相同的ANS 时,会产生不同的实例。
但是对于一个动画,由不同的Actor进行播放时,会发现实例的地址都是相同的,即是多个不同的Actor会触发同样的实例。
这就导致了使用ANS 时,不应该在内部存储状态。
这里个人采取的做法时将相关数据直接绑定到通过MeshComponent 获得的Actor 上。ANS 只进行逻辑调整,不存储信息。
Queue的触发逻辑 UAnimInstance::TriggerAnimNotifies 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 USkeletalMeshComponent* SkelMeshComp = GetSkelMeshComponent (); TArray<FAnimNotifyEvent> NewActiveAnimNotifyState; NewActiveAnimNotifyState.Reserve (NotifyQueue.AnimNotifies.Num ()); TArray<FAnimNotifyEventReference> NewActiveAnimNotifyEventReference; NewActiveAnimNotifyEventReference.Reserve (NotifyQueue.AnimNotifies.Num ()); TArray<const FAnimNotifyEvent *> NotifyStateBeginEvent; TArray<const FAnimNotifyEventReference *> NotifyStateBeginEventReference; for (int32 Index=0 ; Index<NotifyQueue.AnimNotifies.Num (); Index++){ if (const FAnimNotifyEvent* AnimNotifyEvent = NotifyQueue.AnimNotifies[Index].GetNotify ()) { if (AnimNotifyEvent->NotifyStateClass) { int32 ExistingItemIndex = INDEX_NONE; if (ActiveAnimNotifyState.Find (*AnimNotifyEvent, ExistingItemIndex)) { check (ActiveAnimNotifyState.Num () == ActiveAnimNotifyEventReference.Num ()); ActiveAnimNotifyState.RemoveAtSwap (ExistingItemIndex, 1 , false ); ActiveAnimNotifyEventReference.RemoveAtSwap (ExistingItemIndex, 1 , false ); } else { NotifyStateBeginEvent.Add (AnimNotifyEvent); NotifyStateBeginEventReference.Add (&NotifyQueue.AnimNotifies[Index]); } NewActiveAnimNotifyState.Add (*AnimNotifyEvent); FAnimNotifyEventReference& EventRef = NewActiveAnimNotifyEventReference.Add_GetRef (NotifyQueue.AnimNotifies[Index]); EventRef.SetNotify (&NewActiveAnimNotifyState.Top ()); continue ; } TriggerSingleAnimNotify (NotifyQueue.AnimNotifies[Index]); } }
遍历目前的所有ANE ,对于AN 类型直接触发事件
对于ANS 类型,根据现有的已激活ANS ,分成新激活和已激活的两类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for (int32 Index = 0 ; Index < ActiveAnimNotifyState.Num (); ++Index) { const FAnimNotifyEvent& AnimNotifyEvent = ActiveAnimNotifyState[Index]; const FAnimNotifyEventReference& EventReference = ActiveAnimNotifyEventReference[Index]; if (AnimNotifyEvent.NotifyStateClass && ShouldTriggerAnimNotifyState (AnimNotifyEvent.NotifyStateClass)) { { AnimNotifyEvent.NotifyStateClass->NotifyEnd (SkelMeshComp, Cast <UAnimSequenceBase>(AnimNotifyEvent.NotifyStateClass->GetOuter ()), EventReference); } } if (ActiveAnimNotifyState.IsValidIndex (Index) == false ) { return ; } }
对于所有已经不在的ANS 调用结束事件
1 2 3 4 5 6 7 8 9 10 11 12 for (int32 Index = 0 ; Index < NotifyStateBeginEvent.Num (); Index++) { const FAnimNotifyEvent* AnimNotifyEvent = NotifyStateBeginEvent[Index]; const FAnimNotifyEventReference * AnimNotifyEventReference = NotifyStateBeginEventReference[Index]; if (ShouldTriggerAnimNotifyState (AnimNotifyEvent->NotifyStateClass)) { { AnimNotifyEvent->NotifyStateClass->NotifyBegin (SkelMeshComp, Cast <UAnimSequenceBase>(AnimNotifyEvent->NotifyStateClass->GetOuter ()), AnimNotifyEvent->GetDuration (), *AnimNotifyEventReference); } } }
将新出现的的ANS 调用开始事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ActiveAnimNotifyState = MoveTemp (NewActiveAnimNotifyState); ActiveAnimNotifyEventReference = MoveTemp (NewActiveAnimNotifyEventReference); for (int32 Index = 0 ; Index < ActiveAnimNotifyState.Num (); Index++){ const FAnimNotifyEvent& AnimNotifyEvent = ActiveAnimNotifyState[Index]; const FAnimNotifyEventReference& EventReference = ActiveAnimNotifyEventReference[Index]; if (ShouldTriggerAnimNotifyState (AnimNotifyEvent.NotifyStateClass)) { { AnimNotifyEvent.NotifyStateClass->NotifyTick (SkelMeshComp, Cast <UAnimSequenceBase>(AnimNotifyEvent.NotifyStateClass->GetOuter ()), DeltaSeconds, EventReference); } } }
对现有的所有ANS 触发TICK事件,并且将激活情况保存
总结
AN触发
ANS结束事件
ANS开始事件
ANS帧事件
根据部分资料上判断,ANS和AN、同类事件的触发顺序不保证一致。在后续涉及到网络同步时需要谨慎处理
AnimNotify 相关触发流程在AnimInstance 上,AnimIntance 是一个对应于Actor 的动画控制系统。因此不必太关心动画提前结束、打断等是否会导致结束
GAS联动 GAS和蒙太奇的本身的联动就是AbilityTask_playMontageAndWait 这个AT
该AT 允许GA 中播放蒙太奇并处理BlendOut 和Complete 的回调。并根据实际情况拆分出Interrupt 情况下的回调
在GAS 使用时,会调用ASC上的接口,以便在ASC 中也维护住对于蒙太奇接口的使用情况LocalAnimMontageInfo
作为动作游戏常用的节点,PlayMontageAndWait
是一个非常常用的AT。它提供了以下四种情况根据蒙太奇播放情况的回调
OnCompleted:蒙太奇动画正常播放结束
OnBlendOut: 蒙太奇动画被混出
OnInterrupted:蒙太奇动画被打断
OnCancelled:技能被取消,主要在播放蒙太奇失败的情况和为外部取消时调用的接口
在某种程度上,我们的逻辑可以认为1和2为一类、为蒙太奇成功播放完成。3和4为一类、即蒙太奇播放过程中失败了的情况。
AnimMontageInstace本身结束会存在两种情况,完整结束end和被其他动画打断混合blendOut,这里是将blendOut根据是否interrupt来额外区分出Interrupted事件。
同时通过EndTask时的委托解绑来避免重复触发事件。
参数分析
bStopWhenAbilityEnds:在GA 结束销毁AT 时,同步停止蒙太奇。注意在GA 被取消时会直接停止蒙太奇。
bAllowInterruptAfterBlendOut:在正常情况下,蒙太奇被BlendOut后,该AT会不再处理该蒙太奇的interrupt事件,该参数允许在blendOut后处理interrupt事件。
ASC会保存一个通过技能使用的蒙太奇信息在 LocalAnimMontageInfo 中。在该AT使用的蒙太奇blendOut就会清空掉对应蒙太奇
参考材料 UE4/UE5 动画蒙太奇Animation Montage 源码解析
[UE4/UE5 动画通知AnimNotify AnimNotifyState源码解析](UE4/UE5 动画通知AnimNotify AnimNotifyState源码解析)
【UE5】对GAS的一点小改进——任意Mesh任意数量蒙太奇播放
UE5 一文读懂动画蒙太奇