11.Playable系列总结
11.1 总结
系列回顾
这个系列 10 篇,从建图播动画一直写到读源码,把 Playable 系统从使用到原理过了一遍。
基础链路
最基础的东西其实就一条链路:PlayableGraph.Create() 建图,往里面塞节点(ClipPlayable、MixerPlayable 等),用 Connect 连线,最后挂一个 Output 绑到 Animator 或 AudioSource 上——图就能跑了。后面不管搞多复杂,都是在这条链路上加东西。
有个点值得强调:Graph 是惰性的,节点建了、线连了,Output 没绑 SourcePlayable 的话,评估根本不会拉到那些节点。”图搭好了但没效果”,十有八九就是这个原因。
混合
混合分两种。AnimationMixerPlayable 按权重做加权平均,多个 Clip 的姿态按比例混在一起,权重加起来应该归一,不然动画会飘。AnimationLayerMixerPlayable 是分层覆盖,配合 AvatarMask 可以做”上半身攻击、下半身跑步”——实际项目里这个需求太常见了,也是整个系列里最贴近实际项目的部分。
两种混合方式底层都是权重在控制,容易忽略的一点:权重为 0 的节点虽然不参与输出,但默认还在跑时间,除非你主动 Pause() 或 SetSpeed(0)。
时间推进
四种 DirectorUpdateMode,日常用 GameTime 就够了,但 Manual 模式值得认真理解——不调 Evaluate() 图就不会推进,调一次推一帧,时间完全受你控制。换个角度说,GameTime / UnscaledGameTime / DSPClock 只是 Unity 帮你自动 Evaluate 而已。想通了这个,回放、录制、离线烘焙之类的需求就好理解了。
自定义扩展
ScriptPlayable<T> + PlayableBehaviour 是 Playable 系统的自定义扩展点。ScriptPlayable<T>.Create() 建一个节点,往里面塞一个继承 PlayableBehaviour 的类,评估到这个节点时 PrepareFrame() / ProcessFrame() 就会被回调。系列里做了个动画队列的例子——播完一个 Clip 自动切下一个,逻辑全在 PrepareFrame() 里。
不过 Behaviour 说到底就是个帧回调容器,塞太多状态管理进去反而不好维护,不如在外面用 MonoBehaviour 协调,让 Behaviour 只管”读输入→算结果→写权重”。
源码
最后过了一遍 C# 侧的源码,搞清楚 Handle 层和包装层怎么分工、FrameData 从哪来、PlayableDirector 怎么驱动 Graph。不看源码 API 照样能用,但碰到边界问题(回调时序不对、句柄莫名失效)的时候,知道底下是怎么跑的能省很多排查时间。
建图流程速查
flowchart TD A["PlayableGraph.Create()"] --> B["SetTimeUpdateMode()"] B --> C["创建 Output 并绑定目标
AnimationPlayableOutput / AudioPlayableOutput"] C --> D["创建节点
ClipPlayable / MixerPlayable / ScriptPlayable"] D --> E["Connect 连线 + SetInputWeight 设权重"] E --> F["output.SetSourcePlayable(root)"] F --> G["graph.Play()"] G --> H["OnDisable / OnDestroy 里 graph.Destroy()"]
Playable 和 AnimatorController 怎么选
两者侧重点不一样,不是替代关系。
AnimatorController 是配置驱动的状态机——State、Transition、BlendTree 都在 Animator 窗口里可视化编辑,策划和美术能直接维护,适合走、跑、待机、受击这种稳定的状态切换流程。Playable 是程序驱动的求值图,图的结构可以在运行时动态拼装和拆解,适合动画结构不固定、或者需要精确控制时间推进的场景。
实际项目里两者经常并存。Controller 管宏观状态(Idle / Move / Attack 之间的切换和 Transition),Playable 负责局部叠加或临时接管——比如过场动画临时占据 Animator,或者技能释放时动态插入一段打击动画。AnimatorControllerPlayable 本身就是图里的一个节点,可以直接接到 Mixer 的输入端口上参与混合,两者共存并不冲突。
适合上 Playable 的情况大概这几种:
- 动画结构要在运行时动态变,不是提前连好的状态图
- 多种类型的内容(动画、音频、脚本逻辑)要统一在一张图里调度
- 需要 Manual 模式做确定性求值(回放、录制、离线烘焙)
- 要做分层混合但不想走 AnimatorController 的 Layer
反过来,如果需求就是”A 状态切 B 状态切 C 状态”,用 Controller 更直接,硬拿 Playable 做状态机只会把事情搞复杂。
容易踩的坑
写这个系列的过程中碰过不少问题,有些是自己踩的,有些是看别人踩的,挑几个典型的说一下。
Output 没绑 SourcePlayable。 值得反复强调,因为这个错误的表现是”什么都没发生”——不报错、不崩溃、动画就是不播。新手排查不到的时候很容易怀疑是 Clip 有问题或者 Animator 没配对,其实就是 Output 少调了一行 SetSourcePlayable。
Connect 的时候端口不够。 AnimationMixerPlayable.Create(graph, 3) 第二个参数是输入端口数量,如果后面要连 4 个输入但只开了 3 个口,Connect 会静默失败。日志里有一行 invalid input port index,但混在一堆日志里很容易漏掉。建议 Create 的时候端口数宁可多开不要少给。
图里连出了回路。 PlayableGraph 要求是 DAG(有向无环图)。如果不小心把某个节点的输出又连回了上游,评估行为就未定义了,表现可能是卡死、也可能是动画抖动。图结构简单的时候不太会犯这个错,节点一多、动态拼接的时候要留意。
忘了 Destroy Graph。 PlayableGraph.Create() 出来的图不会自动销毁,必须显式调 graph.Destroy()。一般放在 OnDisable 或 OnDestroy 里。忘了的话句柄和资源一直残留,PlayableGraph Visualizer 里能看到越来越多的”幽灵图”。
把 Playable 当成”更底层的 Animator API”。 Playable 确实比 AnimatorController 更底层,但它不是”在 Animator 下面加了一层”——它是和 Animator 状态机并行的另一套求值体系,有自己的 Graph、Output、连线和时间推进模型。搞混这两者会导致很多概念对不上。
接下来的计划
到这里 Playable 的底层就讲完了——建图、连线、按时间推进、每帧求值,核心就这些东西。但实际项目里直接手写 PlayableGraph 的情况并不多,大家更常用的是上层方案:
- AnimatorController:状态机驱动,适合常规的角色状态切换(Idle / Move / Attack 之间那些),策划美术能直接在 Animator 窗口里维护。
- Animancer:Kybernetik 做的第三方插件,底层也是走 Playable,但把建图和混合都封装好了,一行代码就能播动画、做 CrossFade。不想自己管 Graph 又需要用代码精确控制动画的项目,拿来就能用,省不少事。
- Timeline:官方的多轨道编排工具,过场演出、剧情表现基本都靠它。运行时
PlayableDirector把TimelineAsset编译成一张 PlayableGraph 交给底层去跑——本质上就是帮你建图。 - 自己封装:有些团队会在 PlayableGraph 上再包一层,做成项目内部的动画管理模块。灵活度最高,维护成本也最高。
下一个系可能会讲讲 Timeline。从轨道的基本用法讲起,到 PlayableDirector 的 API,再到 Timeline 内部的建图逻辑,最后基于 Timeline 搭一套过场编辑器。这个系列里讲过的 ScriptPlayable、PlayableBehaviour、MixerPlayable、权重求值,写自定义轨道的时候会反复用到——到时候就知道为什么要先花这么多篇幅把底层理清楚了。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com