动画通知深入分析
前言
本文通过源码对动画通知进行深入分析
Notify
动画通知先从Notify开始看, 这里先看默认的Queue即队列类型的通知, BranchPoint的通知最后再看
先看调用栈, 一般的通知都是到TriggerSingleAnimNotify()
触发, 当然还有蒙太奇通知的特殊处理, 这个后面看
这部分没什么难度和内容, 关键是这个队列数据是怎么维护的?
查找引用后发现, 这个队列数组在每一次PreUpdateAnimation()时会被Reset, 在多线程Proxy中, 会在PostUpdate()中对队列进行Append
这个NotifyQueue成员变量是来自Proxy::UpdateAnimation() 中对动画资源的更新
关注一下关键的判定条件, 通知触发的条件就是
(NotifyStartTime <= CurrentPosition) && (NotifyEndTime > PreviousPosition)
翻译一下通知触发条件就是 :通知的时间位于当前帧时间与前一帧时间中间
总结一下
- AnimInstance执行PreUpdate重置通知队列, 同时也会执行Proxy的PreUpdate方法来重置Proxy内的队列
- Proxy执行Update刷新动画资源, 刷新Proxy的通知队列
- Proxy执行PostUpdate操作把本地的队列数据Append到AnimInstance
- MeshComponent执行PostAnimEvaluation一路执行到TriggerSingleAnimNotify
补充:以上流程全是多线程
另外, 不需要资源直接右键创建的动画通知也是在TriggerSingleAnimNotify中执行, 看下面代码, 通过ProcessEvent执行蓝图事件
NotifyState
NotifyState即动画通知窗口中可以拖动起点和重点的带时长的动画通知, 可以接受Begin/Tick/End事件
NotifyState的调用栈与前者类似, 在TriggerAnimNotifies
中分别处理的Notify和NotifyState的调用
用伪代码描述
1 |
|
因为他是先检测End并触发End, 所以说通知的End一定至少落后Begin一帧, 这在绝大多数情况下是没有问题的, 但是如果是下面这样
第一个通知打印1, 第二个打印2,得到的结果如下图
按照动画通知是配置按道理应该是第一个通知的Begin早于第二个的End, 但是由于前面说的机制, 导致了End会落后Begin一帧,这个在某些时候会导致不必要的问题
当然, 有几个解决办法, 要么尽量把前面通知的End与后面通知的Begin至少间隔一帧执行
或者还可以用BranchPoint
的方式来解决一部分问题, 这个在下面会讲
BranchPoint
BranchPoint类型的动画通知是比较特殊的通知, 他与Queue不同的是不会排队处理, 而是统一到一个地方集中处理.
而且不同与Queue的多线程, BranchPoint的通知是统一在游戏线程同步执行的
官方说比较适合蒙太奇不同section之间有跳转的场合使用
先看一下一个普通的蓝图类Notify调用栈
从Montage的Advance开始看, 这个函数不断的推进蒙太奇动画
上面的图中获取一个BranchingPointMarkerPtr
, 这个指针记录了这个BranchPoint的触发时机和类型
补充: 如果是State通知, 那么 将保存Begin和End两份数据
然后调用 HandleEvents()
在HandleEvents中会先处理队列通知, 把所有通知里的BranchPoint过滤掉
然后是 UpdateActiveStateBranchingPoints()
这里会调用NotifyState的Begin和End事件, 用的方法是记录一个Active的事件数组ActiveStateBranchingPoints
, 通过时间判断触发Begin或者End
最后是调用BranchingPointEventHandler()
这一步先处理一写被标记为转成Queue的BranchPoint通知, 通过宏可以关闭BranchPoint的通知,强制转成Queue, 看下图
然后再处理一遍State类型的Begin和End触发, 最后是才是处理Notify的触发
那么问题来了, 为什么UpdateActiveStateBranchingPoints()和BranchingPointEventHandler()都有对State通知的触发
首先机制不一样, 前者是通过当前时间来触发, 后者是通过序列化到资源的BranchingPointMarkerPtr数据来判定触发与否,
这是因为蒙太奇动画经常会跳转section导致动画的position会瞬切, 这当中涉及到的问题很多, 前后这两者加起来保证了正常的触发
最后还有一个函数Termnate()会确保所有State通知如果在动画结束了还没有被End, 来保证触发通知的End事件
补充: Sequence的BranchPoint也是会当作Queue通过TriggerSingleAnimNotify()等形式触发
补充: 引擎自带的蒙太奇通知都是BranchPoint执行的
回到上面2个State通知的情况, 如果前者使用的是BranchPoint, 那么看一下结果
结果就是前面的通知的End可以在正确的时机触发而不用等到下一帧了
但是BranchPoint有个问题, 在同一帧中的不能有太多的BranchPoint, 否则可能会导致部分通知丢失, 这个在添加通知的时候就会有警告
这是由BranchPoint的触发逻辑决定的, 因为在触发逻辑中, BranchPoint会去查找当前时间点第一个找到的BranchPoint通知, 同一时间的其他会被丢弃掉
总结
- 关于Queue与BranchPoint
Queue与BranchPoint 主要区别在于Queue方法是异步的,而BranchPoint 是同步的。
Queue方法:当动画的 Evalutate 阶段结束时触发通知。不适合改变部分或跳跃蒙太奇位置。
BranchPoint 方法:到达分支点时,在正确的时间触发通知,精度较高。适合更改部分或跳转到蒙太奇位置。
如果一个特定的动画需要跳转到一个非常精确的点,最好使用BranchPoint 方法以获得更高的精度。
如果由于Queue方法的异步特性而在另一个帧中触发了事件,或者如果出现挂起,请使用BranchPoint方法。
BranchPoint方法以高开销为代价达到高精度,有好处也有缺点。如果允许若干帧的精度差距, 不会跳帧, 那么使用队列是足够了
补充: 在大幅度的跳帧的情况下, 两者都可能不能触发
- Notify和NotifyState
Notify是一次性事件, 如果跳帧了可能会被跳过
NotifyState是持续性的一个状态, 有Begin/End/Tick事件, 除非整个被跳过, 否则有头必有尾
另外,两者的蓝图Notify事件都是const的, 如果需要进行非const操作还是建议到cpp中去执行(老版本UE4中的Notify的蓝图事件不是const)