闲鱼技术01 发表于 2023-2-14 08:05

[RS] Timeline踩坑(3):多轨道播放不同步

记录环境

Unity 2020.3.40f1Timeline 1.4.8
<hr>前置设定

项目的TML框架

采用DirectorUpdateMode.Manual的方式,自己管理TML的更新。由TMLCtrl进行管理每个TMLCtrl自己处理分帧、追帧:在ManualUpdate()中取Time.deltaTime,以1 / tmlAsset.editorSettings.fps为时间步,多次执行director.Evaluate()ManualUpdate()注册到统一管理的Game.Update中被调用
自定义轨道&用法


[*]播放角色动画的轨道OverrideAnimator:
创建新的PlayableGraph,借助AnimationClipPlayable、AnimationPlayableOutput、AnimationMixerPlayable等控制动画播放。只在ProcessFrame()里,将轨道的时间设置给ClipPlayable、MixerPlayable。其他内容都由PlayableGraph自动处理,比如何时将动画数据写入到骨骼上。
[*]在单位节点创建特效的轨道:EffectOnNode:
这条轨道,在项目中经常被用于特定环境下,播放“附加物”使用。比如:跟随身宠物互动;跟特殊物件交互。交互实现方式是,GameOjbect激活时自动播放Animator里的动画。
<hr>案例1:执行时长差异

问题描述


两轨道动画播放不同步

TML中OverrideAnimator、EffectOnNode(Animator)同时使用,两个轨道的动画不匹配。EffectOnNode(Animator)播放的动画 超前于 OverrideAnimator的动画。并且这个差距在播放开始产生,过程中不会改变。
上图是用TML自带轨道做的,但问题跟我们自定义的一致。红色Cube用OverrideAnimator轨道控制,白色Cube用EffectOnNode控制,它们播放的动画片段完全一样。可以看到,TML进入两个轨道的Clip后,白Cube跑的比红Cube快(动画超前)。
原因说明


产生问题有以下几个要点
两个物体的动画,一个是用TML轨道的时间控制;一个是走Animator自己的更新一个Unity的帧(Time.deltaTime)会包含多个TML的帧(FPS=30)EffectOnNode轨道不是顶头播放的(没有从第0帧开始执行)。更准确的说,这个轨道进入的TML帧不是当前Unity帧的第0帧(结合下图理解)。

出现动画不一致时,帧状态示意

结合上图,出错过程如下:
假定当前:1个Unity帧,包含5个TML的帧
[*]在TML的第3帧(帧尾),两个轨道同时进入片段(执行EnterClip、ProcessFrame等)
1)OverrideAnimator轨道,用TML当前帧的时间播放单位的动画,取到0.033s
2)EffectOnNode轨道,把GameObject创建出来,Animator同帧自动播放,取到的时间是整个Unity帧(Time.deltaTime),也就是 5 * 0.033s = 0.165s

这个描述可能不准确,因为Animator应该是在Unity帧的最后才同步结果给主线程,不是在第3个TML帧立刻执行完。但这个不影响对当前问题的分析。

[*]后面两个TML帧中(4、5)
1)OverrideAnimator轨道继续,分别取当前TML帧的时候,走2 * 0.033s = 0.066s
2)EffectOnNode轨道无事可做,Animator因为这一个Unity帧已经执行完了,也不再执行最终,在开始播放的这一整个Unity帧中,Animator比TML多执行0.066s,导致两边不同步
解决方案


方案1:改资源
从上面分析可以确定,只要TML轨道、Animator取到的时间都是一整个Unity帧,就不会有问题。因此让两条轨道都顶头开始,可以规避这个问题。

方案2:设置固定帧率
从根本上讲,只要保证Unity帧跟TML帧始终保持1:1的时长,那么就不会有任何问题。恰好Unity给我们提供了相关的接口:
// 我们TML都是采用30FPSTime.captureDeltaTime = 0.03333f;
这个方案应该只用于编辑器预览,因为我们会把一些重度表演的TML录成视频在游戏中播放,所以这个方案还有价值。
特别注意一点,上面的接口有个等价的帧率设置,在处理当前问题的时候不能用:
Time.captureFramerate = 30;
理论上两个接口是等价的,但由于浮点数精度问题,在当前背景下我们项目中只能用captureDeltaTime。30FSP换算成时间是0.03333334,最后1位的4超出一点,经过误差累积也会带来不一致的问题。

方案3:改TML的用法
对于这个问题,我们没办法完全通过改代码的方式解决各种环境下的问题。只能改整体做法:统一所有播动画的方式,即由TML触发播放的动画,全部使用OverrideAnimator轨道控制播放进度。
<hr>案例2:结果生效时机差异

问题描述


正确效果


错误效果

还是OverrideAnimator、EffectOnNode两条轨道配合使用出的问题,但不是动画不同步,而是EffectOnNode创建的GameObject(沙袋)位置不对。
沙袋的创建点,是在角色的某个骨骼处。Idle动作这个骨骼在地面位置(y=0);Born动作,会把骨骼点移动到空中,实现沙袋被吊在半空的效果。
原因说明


这个问题的关键点在于,动画对骨骼的影响何时生效!!
Animator、PlayableGrouph自身的运行都是在子线程(Job)中,它们的结果会在一帧结束前应用到主线程。

轨道帧数示例

结合上图,出错过程如下:
假定当前:1个Unity帧,包含3个TML的帧
[*]在TML的第1帧(帧头),两个轨道同时进入片段
1)OverrideAnimator轨道,用TML当前帧的时间设置动画里的时间。这里虽然设置的动画的时间,但是骨骼的坐标并未被修改。因为这个Unity帧还未结束。
2)EffectOnNode轨道,在骨骼点处把GameObject创建出来。这里取到的是Idle的坐标,也就是地面位置,出错!!。
[*]TML的第2、3帧,两个轨道各自运行,对结果已经没有影响
在第3帧,OverrideAnimator轨道第3次设置完动画时间后,在帧尾应该会把坐标应用到骨骼点上,在这里取才是正确的值。
解决方案

方案1:改资源


将沙袋的出生点,放到一个不会随动画动的固定骨骼。然后沙袋自身高度提高到合适位置。
方案:改代码


每个TML帧强制更新动画的PlayableGrouph,使动画结果立刻应用到骨骼上
clipPlayable.SetTime(curTime);mixerPlayable.SetTime(tmlCtrl.Time);// 强制在主线程,用动画信息更新骨骼playableGraph.Evaluate();
这个方案也存在一些问题:
每执行一次Evaluate,都会多产生一次动画模拟的消耗。因为PlayableGraph自己Job中会执行模拟,主线程中强制执行的Evaluate不会影响Job里的执行。不能直接停止PlayableGraph自己的执行,完全由Evaluate代替。也是实践下来发现的一个坑:用PlayableGraph实现覆盖Animator播动画的时候,Animator自己的动画模拟也没有停止,它会在Unity帧尾将结果同步到主线程。如果在TML的ProcessFrame事件中执行Evaluate,那么在帧尾它的结果会被Animator冲掉,最终看上去好像TML没生效!!!
<hr>案例3:启动时机差异

问题描述


正确效果


错误效果

还是OverrideAnimator、EffectOnNode两条轨道配合使用出的问题,也是两种动画播放没有匹配上;Animator(绷带)播放超前。从现象上看跟案例1一毛一样,但是这里的两条轨道都是顶头开始的,因此可以断定与案例1不是同一个问题。
这个问题,只在模拟模式下,并且是进入养成系统的第一个展示单位才会出错。
AB包模式正常;在养成中第一次切换出来也正常。
经过Log大法的调试,最终确定在TML播放的第1个Unity帧,EffectOnNode创建的Animator就开始执行了;第2个Unity帧开始,OverrideAnimator的轨道才生效。最终导致Animator超前了1个Unity帧。
原因说明


这个问题是框架设计+业务使用两方问题共同导致的。

问题流程示例

同样结合上图:

[*]TML框架在执行Play()时,会将自身的ManualUpdate()注册到Game.Update;同时立刻执行一次director.Evaluate(),由于没有设置时间,相当于执行了一次第0帧的演算。
1)OverrideAnimator轨道进入了,但由于没有给TML设置时间,这个轨道没有产生“进度”
2)EffectOnNode轨道也进入了,并在进入时立刻创建绷带的GameObject,然后绷带自己的Animator开始播放(Animator播放了1个Unity帧)
[*]第2个Unity帧,Game.Update执行,驱动TML走了ManualUpdat()。这里会设置TML的时间,然后执行director.Evaluate()。
1)OverrideAnimator轨道根据TML的时间,给动画设置时间,并产生实际效果(动画第1帧的状态)
2)EffectOnNode创建的Animator,已经走到了动画第2帧的状态

到这好像不同步的问题已经说清楚了,但其实并没有!!案例3跟前面两个有一个很大的差别:它是跨越两个Unity帧才出的问题。如果是这样,应该一直都是错误状态才对,为什么在特定操作下才会出问题呢?

这个就要看,项目中其他几个框架的执行顺序了:
Unity Input Events
[*]Unity Update
1)Res.Update:资源加载模块
2)Game.Update:游戏逻辑更新(驱动TML)
3)Scene.Update:切场景

还有一个设定:
AB包模式下,资源是异步加载返回模拟模式下,资源同步加载返回

接下来说明一种错误的情况,与两种正确的情况分别是什么流程。
错误流程:模拟模式下,切场景进入养成系统,显示的第一个单位

切场景完成在Scene.Update里回调业务模块业务模块通过Res模块加载单位及TML并播放,由于是模拟模式,加载立刻结束,没有执行Res.Update播放TML时,执行一次Evaluate让绷带的Animator播放;然后将ManualUpdate注册到Game.Update中等待执行从上面的执行顺序可以知道,Scene在Game后面,所以ManualUpdate要在下个Unity帧才会执行。
正确流程1:模拟模式下,在养成系统里切换单位

切换走的是按钮点击事件,执行时机是Unity Input Events同样是模拟模式,加载立刻完成没有Res.Update; 然后执行Evaluate,注册ManualUpdate到Game.Update中等待执行
[*]差异来了!Game在Input之后执行,所以在同一Unity帧,执行了ManualUpdate。
那么两条轨道的动画,同时开始,执行了相同的时长,结果自然正确
正确流程2:AB模式下,切场景进入养成系统,显示的第一个单位

切场景结束,在Scene.Update里回调业务业务在AB包模式下,走Res模块加载资源。资源加载完成,在Res.Update又回调业务。业务播放TML,执行Evaluate、注册ManualUpdate同样的道理,Game在Res之后执行,所有同一帧内执行了ManualUpdate
解决方案:改资源


不用EffectOnNode的方式创建绷带,而是一开始就放到TML的Prefab里,也用OverrideAnimator播放动画。(由于这里涉及到多个方面的问题,暂不考虑功能上的解决方案)
页: [1]
查看完整版本: [RS] Timeline踩坑(3):多轨道播放不同步