尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用Playables绕开状态机:一次彻底的动画重构

用Playables绕开状态机:一次彻底的动画重构 上一篇结尾我埋了个钩子,说想彻底摆脱状态机开销可以看看Playables,结果评论区好几个人催更。行,这篇就把这块摊开讲。先打个预防针:这篇比前面几篇硬,代码也多。如果你项目里就几个角色,状态机跑得好好的,那你看个乐呵就行,真没必要上Playables。这东西是给那种角色成百上千、状态机评估已经压不下去了的场景准备的。杀鸡不用牛刀,别看完就急吼吼去重构。先想明白一件事:状态机的开销到底省在哪前面几篇反复说,Animator状态机每帧要干一堆活:判断当前状态、读参数、挨个查过渡条件、算过渡进度。这些活儿是Mecanim为了好用和可视化必须背的包袱。问题是,很多时候你根本用不上这套。想想那些远处的杂兵。它们可能就在Idle、Walk、Run三个动作之间切,逻辑简单得不能再简单。可你用Animator,它照样每帧给你走一整套状态机评估,该查的过渡一个不落。这就是典型的杀鸡用牛刀——你付了一大笔通用性的钱,但实际只用了最基础的功能。Playables的思路是:别用状态机了,我直接告诉你播哪个clip、怎么混,不需要你在那儿自己判断。没有状态框,没有过渡线,没有条件检查。你要什么,代码里写死;不要的,一分钱不花。Playables到底是个啥Playables API是Unity底层的动画接口。你可以把它理解成一张动画数据的流动图。图里有几种基本零件:一种是源节点,比如一个AnimationClipPlayable,它就代表一个动画片段,负责产出这个clip的骨骼数据。一种是混合节点,比如AnimationMixerPlayable,它有好几个输入口,每个口接一个源,然后按你给的权重把它们混起来。最后是输出节点,把混合完的结果接到角色的Animator上,让骨骼真正动起来。数据从源节点流出,经过混合节点揉一揉,最后从输出节点吐给角色。整个过程你全程掌控,没有任何引擎替你做判断的环节。最简单的例子:纯代码播一个clip先从最小的开始,就播一个动画,感受下没有状态机是什么样:usingUnityEngine;usingUnityEngine.Playables;usingUnityEngine.Animations;publicclassPlayOneClip:MonoBehaviour{publicAnimationClipclip;PlayableGraphgraph;voidStart(){// 建一张图graphPlayableGraph.Create(PlayOneClip);// 建个输出口,接到本物体的Animator上varoutputAnimationPlayableOutput.Create(graph,output,GetComponentAnimator());// 把clip包成一个源节点varclipPlayableAnimationClipPlayable.Create(graph,clip);// 源接到输出output.SetSourcePlayable(clipPlayable);// 开跑graph.Play();}voidOnDestroy(){// 图是手动管理的,不销毁会泄漏if(graph.IsValid())graph.Destroy();}}就这么点代码,一个动画就播起来了。注意这里面没有任何状态机。没有过渡条件要检查,没有Layer要评估。引擎干的活儿,就是老老实实把这个clip的数据吐给角色,仅此而已。对比一下,同样播一个Idle,Animator后台还在不停查要不要切走,而这里啥都没查。这就是省下来的开销。还有个细节,OnDestroy里必须销毁graph。Playables是手动管理的,你建的图得自己负责收尾,忘了就内存泄漏。这点跟Animator不一样,Animator这些Unity都替你管了,用Playables就得自己上心。进阶:两个动画之间自己控制混合光播一个没意思,真正有用的是混合。比如从走路平滑过渡到跑步,这在Animator里是拉根过渡线的事,在Playables里得自己动手。思路是用一个混合节点,接两个clip,然后自己控制两边的权重:usingUnityEngine;usingUnityEngine.Playables;usingUnityEngine.Animations;publicclassBlendTwoClips:MonoBehaviour{publicAnimationClipwalkClip;publicAnimationCliprunClip;[Range(0f,1f)]publicfloatblend0f;// 0全是走,1全是跑PlayableGraphgraph;AnimationMixerPlayablemixer;voidStart(){graphPlayableGraph.Create(BlendTwoClips);varoutputAnimationPlayableOutput.Create(graph,output,GetComponentAnimator());// 建个混合节点,2个输入口mixerAnimationMixerPlayable.Create(graph,2);output.SetSourcePlayable(mixer);// 两个clip各接一个口varwalkAnimationClipPlayable.Create(graph,walkClip);varrunAnimationClipPlayable.Create(graph,runClip);graph.Connect(walk,0,mixer,0);// 接到0号口graph.Connect(run,0,mixer,1);// 接到1号口graph.Play();}voidUpdate(){// 每帧根据blend值,分配两个输入口的权重mixer.SetInputWeight(0,1f-blend);// 走的权重mixer.SetInputWeight(1,blend);// 跑的权重}voidOnDestroy(){if(graph.IsValid())graph.Destroy();}}看那个SetInputWeight,这就是混合的核心。blend是0的时候,走占满、跑为零;blend慢慢到1,走淡出、跑淡入。你要做走到跑的平滑过渡,只要让blend这个值随速度平滑变化就行。关键在于,这个权重完全是你说了算的。Animator里过渡曲线怎么走是引擎按你连线时的设置来的,而这里,你想怎么混就怎么混,每一帧的权重都攥在自己手里。想做非线性的过渡?想根据别的条件动态调?随便你。把切换动作也自己接管真实项目里角色不止两个动作,得能在多个动作间切换。你可以把混合节点的输入口开多一点,把所有可能用到的clip都接上,然后靠权重来选择当前播哪个。我封装一个简单的控制器,让它能在若干个动作之间平滑切换:usingUnityEngine;usingUnityEngine.Playables;usingUnityEngine.Animations;publicclassSimpleAnimController:MonoBehaviour{publicAnimationClip[]clips;// 所有要用的动作,比如 Idle/Walk/RunPlayableGraphgraph;AnimationMixerPlayablemixer;intcurrentIndex0;inttargetIndex0;floattransitionSpeed5f;voidStart(){graphPlayableGraph.Create(SimpleAnimController);varoutputAnimationPlayableOutput.Create(graph,output,GetComponentAnimator());// 混合节点,口数等于clip数量mixerAnimationMixerPlayable.Create(graph,clips.Length);output.SetSourcePlayable(mixer);// 把每个clip都接上for(inti0;iclips.Length;i){varclipPlayableAnimationClipPlayable.Create(graph,clips[i]);graph.Connect(clipPlayable,0,mixer,i);}// 一开始只播第一个mixer.SetInputWeight(0,1f);graph.Play();}// 外部调这个来切动作publicvoidPlayClip(intindex){targetIndexindex;}voidUpdate(){// 每帧把目标动作的权重往1推,其他往0推for(inti0;iclips.Length;i){floatcurrentmixer.GetInputWeight(i);floatdesired(itargetIndex)?1f:0f;floatnextMathf.MoveTowards(current,desired,transitionSpeed*Time.deltaTime);mixer.SetInputWeight(i,next);}}voidOnDestroy(){if(graph.IsValid())graph.Destroy();}}用的时候,PlayClip(0)播Idle,PlayClip(1)播Walk,切换的时候会自动平滑过渡。你看,到这儿其实已经把Animator状态机的核心功能——切换加过渡——自己实现了一遍。但区别在于,整个过程没有任何过渡条件检查,没有Layer评估,没有Any State全帧扫描。你就是明明白白地在调权重,引擎不替你做任何多余的判断。这些省下来的判断,乘以你成百上千的角色数量,就是Playables的价值所在。真正的杀手锏:一张图管一堆角色上面的例子还是一个角色一张图,其实Playables更狠的用法,是配合Job System和Animator.Update手动驱动,做批量处理。思路大概是这样:你不再让每个角色的Animator自动更新,而是搞一个中央调度,统一遍历所有角色,手动推进它们的动画图。这样你能精确控制哪些角色这帧更新、哪些跳过,还能把骨骼计算丢到Worker线程上并行。这套东西展开就太长了,而且涉及Animation C# Job System,属于进阶中的进阶。这里先给你个概念,让你知道天花板在哪:Playables加上Job,是Unity传统动画方案能榨出的性能上限。再往上,就得上DOTS那套全新的架构了。泼盆冷水:Playables不是银弹讲了这么多好处,得说说代价,不然你容易上头。开发成本陡增。Animator里拖拖拽拽连连线就能干的事,Playables全得写代码。混合逻辑、切换逻辑、过渡曲线,原来引擎替你做的,现在都得自己实现。代码量不是小数目。可视化没了。状态机那个界面很直观,策划美术都能看懂、能调。换成Playables,一切都在代码里,别人想改个过渡时间都得找你。团队协作上是个损失。调试更麻烦。状态机运行时你能在界面上看到当前在哪个状态、过渡到哪儿了。Playables这些都得自己打log或者写调试工具。容易泄漏。前面反复提的,graph得手动销毁。图一复杂,管理起来一不小心就漏。所以我的建议是:别全盘替换,挑着用。玩家近处那几个主要角色,该用Animator用Animator,好调好改;真正拖性能的是那一大群远处杂兵,把它们换成Playables的简化版本,针对性地把状态机开销砍掉。这种混着用的方案,收益和成本最平衡。什么时候该考虑Playables给你几个判断信号:Profiler里Animators.Update高得离谱,而且你已经把前面几篇讲的——不动的关掉、Layer砍了、过渡精简了、参数哈希了——全做完了,还是压不下去。你的场景里有大量逻辑简单的重复角色,它们用不上状态机那些花哨功能,却在付全套的评估开销。你对动画的控制有特殊需求,状态机那套框架反而束手束脚。如果这几条你都不沾边,那老老实实用Animator,把前面几篇的优化做扎实,足够了。真的,别为了用而用。收个尾Playables摆脱状态机开销的本质,一句话:把引擎替你做的判断,全收回到自己手里,不需要的就不做。状态机贵,贵在它的通用和自动——它得应付各种情况,所以每帧查东查西。Playables则是我知道我要什么,直接产出结果,省掉了所有猜测和检查。这种确定性,换来的就是性能。但确定性的另一面是繁琐。你把引擎的活儿接过来了,引擎的便利也就一起还回去了。值不值,取决于你的项目规模和性能压力。到这儿,从动画为什么费CPU到状态机评估再到用Playables绕开它,这条线基本讲完了。再往下的Job System和DOTS,是另一个更大的话题,真有人想看我再另起炉灶。这系列就先到这儿。
返回列表