11.Unity进阶Playable总结

  1. 11.Playable系列总结
    1. 11.1 知识点
      1. 系列回顾
        1. 基础链路
        2. 混合
        3. 时间推进
        4. 自定义扩展
        5. 源码
      2. 建图流程速查
      3. Playable 和 AnimatorController 怎么选
      4. 容易踩的坑
      5. 接下来的计划
    2. 11.2 核心要点速览
      1. Playable 系统核心概念
      2. 建图流程
      3. 混合机制
      4. 时间控制模式
      5. 自定义 Playable 节点
      6. Playable vs AnimatorController
      7. 常见错误
      8. 调试工具
      9. 源码分层架构
      10. 核心类型职责速查
    3. 11.3 面试题精选
      1. 基础题
        1. 1. PlayableGraph 的基本组成有哪些?各自职责是什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. AnimationMixerPlayable 和 AnimationLayerMixerPlayable 有什么区别?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 3. PlayableGraph 的惰性求值是什么意思?为什么 Output 必须绑定 SourcePlayable?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 4. DirectorUpdateMode 的四种模式分别适用于什么场景?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 5. 如何实现自定义 Playable 节点?回调时序是怎样的?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 6. Playable 和 AnimatorController 应该如何选型?它们是什么关系?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 7. PlayableGraph 的资源管理有哪些注意事项?忘记销毁会有什么后果?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 8. Playable 系统的源码是如何分层的?Handle 层扮演什么角色?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

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()。一般放在 OnDisableOnDestroy 里。忘了的话句柄和资源一直残留,PlayableGraph Visualizer 里能看到越来越多的”幽灵图”。

把 Playable 当成”更底层的 Animator API”。 Playable 确实比 AnimatorController 更底层,但它不是”在 Animator 下面加了一层”——它是和 Animator 状态机并行的另一套求值体系,有自己的 Graph、Output、连线和时间推进模型。搞混这两者会导致很多概念对不上。

接下来的计划

到这里 Playable 的底层就讲完了——建图、连线、按时间推进、每帧求值,核心就这些东西。但实际项目里直接手写 PlayableGraph 的情况并不多,大家更常用的是上层方案:

  • AnimatorController:状态机驱动,适合常规的角色状态切换(Idle / Move / Attack 之间那些),策划美术能直接在 Animator 窗口里维护。
  • Animancer:Kybernetik 做的第三方插件,底层也是走 Playable,但把建图和混合都封装好了,一行代码就能播动画、做 CrossFade。不想自己管 Graph 又需要用代码精确控制动画的项目,拿来就能用,省不少事。
  • Timeline:官方的多轨道编排工具,过场演出、剧情表现基本都靠它。运行时 PlayableDirectorTimelineAsset 编译成一张 PlayableGraph 交给底层去跑——本质上就是帮你建图。
  • 自己封装:有些团队会在 PlayableGraph 上再包一层,做成项目内部的动画管理模块。灵活度最高,维护成本也最高。

下一个系可能会讲讲 Timeline。从轨道的基本用法讲起,到 PlayableDirector 的 API,再到 Timeline 内部的建图逻辑,最后基于 Timeline 搭一套过场编辑器。这个系列里讲过的 ScriptPlayablePlayableBehaviourMixerPlayable、权重求值,写自定义轨道的时候会反复用到——到时候就知道为什么要先花这么多篇幅把底层理清楚了。


11.2 核心要点速览

Playable 系统核心概念

Playable 是 Unity 提供的一套底层接口,用”节点图”驱动时间轴内容。核心由三部分组成:

组件 职责 典型类型
PlayableGraph 图的容器和调度者,管理节点生命周期、播放控制 -
Playable 图中的数据生产节点,负责处理数据与权重混合 ClipPlayable、MixerPlayable、ScriptPlayable
PlayableOutput 图的出口,把结果输出到场景对象 AnimationPlayableOutput、AudioPlayableOutput

节点分类

  • 叶子节点:直接持有可播放内容,如 AnimationClipPlayable
  • Mixer 节点:对多个输入做混合,如 AnimationMixerPlayable
  • 脚本节点:由脚本控制行为,ScriptPlayable<T> + PlayableBehaviour

建图流程

// 1. 创建 Graph
var graph = PlayableGraph.Create("MyGraph");

// 2. 创建 Output 并绑定目标
var output = AnimationPlayableOutput.Create(graph, "AnimOutput", animator);

// 3. 创建节点
var clipPlayable = AnimationClipPlayable.Create(graph, clip);

// 4. 连线 + 设权重
graph.Connect(clipPlayable, 0, mixer, 0);
mixer.SetInputWeight(0, 1f);

// 5. 绑定 Output 的源节点
output.SetSourcePlayable(rootPlayable);

// 6. 播放
graph.Play();

// 7. 销毁(OnDisable/OnDestroy)
graph.Destroy();

关键点

  • Output 必须调用 SetSourcePlayable,否则节点不会被评估
  • 端口数量在 Create 时指定,连接时端口不足会静默失败
  • 图中不允许存在回路(必须是 DAG)
  • 必须显式调用 Destroy() 释放资源

混合机制

AnimationMixerPlayable:加权平均混合

var mixer = AnimationMixerPlayable.Create(graph, 2);
graph.Connect(clipA, 0, mixer, 0);
graph.Connect(clipB, 0, mixer, 1);

// 权重归一化
mixer.SetInputWeight(0, 0.3f);
mixer.SetInputWeight(1, 0.7f);

AnimationLayerMixerPlayable:分层覆盖

var layerMixer = AnimationLayerMixerPlayable.Create(graph, 2);
layerMixer.SetLayerMaskFromAvatarMask(1, upperBodyMask); // 上半身遮罩
layerMixer.SetLayerAdditive(1, false); // 覆盖模式
对比 AnimationMixerPlayable AnimationLayerMixerPlayable
混合方式 加权平均 分层覆盖
适用场景 同一部位的动画过渡 上下身分离、局部覆盖
配合 权重归一化 AvatarMask

注意:权重为 0 的节点默认仍在推进时间,需主动 Pause()SetSpeed(0) 停止。

时间控制模式

DirectorUpdateMode 时间源 典型用途
GameTime Time.time 常规游戏时间,受 timeScale 影响
UnscaledGameTime Time.unscaledTime UI 动画、暂停菜单
DSPClock 音频系统时钟 音频同步播放
Manual 手动调用 Evaluate() 回放、录制、离线烘焙
graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
graph.Evaluate(deltaTime); // 手动推进一帧

自定义 Playable 节点

public class MyBehaviour : PlayableBehaviour
{
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        // 在求值前调用,适合做权重计算
    }
    
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        // 在求值时调用,适合处理输出
    }
}

// 创建节点
var scriptPlayable = ScriptPlayable<MyBehaviour>.Create(graph);
var behaviour = scriptPlayable.GetBehaviour();

回调时序OnGraphStartOnBehaviourPlayPrepareFrameProcessFrameOnBehaviourPauseOnGraphStop

设计建议:PlayableBehaviour 只负责帧逻辑,状态管理交给外部 MonoBehaviour,避免在 Behaviour 中塞太多状态。

Playable vs AnimatorController

维度 Playable AnimatorController
驱动方式 程序驱动,运行时动态构建 配置驱动,可视化编辑
维护者 程序 策划、美术可维护
适用场景 动态结构、精确时间控制、多类型内容统一调度 稳定状态切换(Idle/Move/Attack)
关系 可作为图中的节点 通过 AnimatorControllerPlayable 接入 PlayableGraph

适合用 Playable 的场景

  • 动画结构运行时动态变化
  • 多类型内容(动画、音频、脚本)统一调度
  • 需要 Manual 模式做确定性求值
  • 分层混合但不想走 AnimatorController 的 Layer

常见错误

错误 表现 解决
Output 未绑定 SourcePlayable 静默失败,无效果 调用 output.SetSourcePlayable(root)
端口数量不足 Connect 静默失败 Create 时端口数宁多勿少
图中存在回路 卡死或动画抖动 确保图是 DAG
忘记销毁 Graph 资源泄漏,幽灵图 OnDisable/OnDestroy 中调用 graph.Destroy()
混淆 Playable 与 Animator 概念对不上 Playable 是独立的求值体系,不是 Animator 的底层 API

调试工具

  • PlayableGraph Visualizer:官方工具,从 Output 出发组织图结构
  • PlayableGraph Monitor:社区插件,从叶子节点往 Output 方向构图,更适合排查结构

源码分层架构

Playable 系统从 C# 侧看分为六层:

层级 核心类型 职责
C++ 黑盒层 引擎内部 评估、遍历、数据流、Job 等核心逻辑
Handle 层 PlayableHandle、PlayableOutputHandle 真正控制时间、状态、端口、权重
包装层 Playable、PlayableOutput、ScriptPlayable<T> C# API 入口,转发操作到 Handle
数据层 FrameData 帧上下文,携带 deltaTime、weight、evaluationType
驱动层 PlayableGraph、PlayableDirector 图的创建、连接、评估、销毁
资源层 PlayableAsset、PlayableBinding 描述”播什么”和”有哪些输出”

调用链上层 API → Handle → bindings(C++ FreeFunction)→ 引擎内部评估

Handle 层的关键地位

  • Playable / PlayableOutput 只是轻量包装
  • 真正的状态、时间、端口、权重都落在句柄上
  • 看到 extern / FreeFunction 就进入了 C++ 黑盒

核心类型职责速查

接口

  • IPlayable:节点统一入口,暴露 GetHandle()
  • IPlayableOutput:输出统一入口,暴露 GetHandle()
  • IPlayableBehaviour:生命周期回调接口(OnGraphStart、PrepareFrame、ProcessFrame 等)
  • IPlayableAsset:资源接口,规定 CreatePlayable、outputs、duration

结构体

  • Playable:节点包装,持有 PlayableHandle
  • PlayableHandle:底层句柄,控制时间、状态、端口、权重
  • PlayableGraph:图容器,负责创建、连接、评估、销毁
  • PlayableOutput:输出包装,持有 PlayableOutputHandle
  • ScriptPlayable<T>:把 PlayableBehaviour 包装成可播放节点
  • FrameData:帧上下文,由引擎生成并传入回调

  • PlayableBehaviour:行为基类,提供生命周期回调默认实现
  • PlayableAsset:资源基类,生成图节点
  • PlayableDirector:驱动组件,管理 PlayableAsset 与 PlayableGraph

枚举

  • PlayState:Paused / Playing
  • DirectorUpdateMode:GameTime / UnscaledGameTime / DSPClock / Manual
  • DirectorWrapMode:Hold / Loop / None
  • FrameData.EvaluationType:Evaluate / Playback

11.3 面试题精选

基础题

1. PlayableGraph 的基本组成有哪些?各自职责是什么?

题目

请简述 PlayableGraph 的三个核心组成部分及其职责。

深入解析

PlayableGraph 是 Unity 提供的一套底层接口,用于驱动时间轴内容。它由三个核心部分组成:

  1. PlayableGraph:图的容器和调度者

    • 管理图中的所有 Playable 节点和 PlayableOutput
    • 控制节点之间的连接关系
    • 负责生命周期管理(创建、播放、暂停、销毁)
  2. Playable:图中的数据生产节点

    • 叶子节点:直接持有可播放内容(如 AnimationClipPlayable)
    • Mixer 节点:对多个输入做混合(如 AnimationMixerPlayable)
    • 脚本节点:由脚本控制行为(ScriptPlayable)
  3. PlayableOutput:图的出口

    • 把 Playable 传递的数据交给具体 target 执行
    • AnimationPlayableOutput 绑定 Animator
    • AudioPlayableOutput 绑定 AudioSource
答题示例

PlayableGraph 由三部分组成:Graph 是容器和调度者,管理节点生命周期和播放控制;Playable 是数据生产节点,负责处理动画、音频等内容;PlayableOutput 是出口,把结果输出到 Animator 或 AudioSource 等场景对象。

参考文章
  • 1.概述.md

2. AnimationMixerPlayable 和 AnimationLayerMixerPlayable 有什么区别?

题目

请对比 AnimationMixerPlayable 和 AnimationLayerMixerPlayable 的混合方式及适用场景。

深入解析

AnimationMixerPlayable

  • 混合方式:加权平均
  • 多个 Clip 的姿态按权重比例混在一起
  • 权重应该归一化(加起来等于 1),否则动画会”飘”
  • 适用场景:同一部位的动画过渡,如走→跑的渐变

AnimationLayerMixerPlayable

  • 混合方式:分层覆盖
  • 配合 AvatarMask 可以指定每层影响哪些骨骼
  • 可以做”上半身攻击、下半身跑步”的效果
  • 适用场景:上下身分离、局部动画覆盖

关键区别

  • Mixer 是”融合”,LayerMixer 是”叠加/覆盖”
  • Mixer 的权重归一化很重要,LayerMixer 的 LayerMask 是核心
答题示例

AnimationMixerPlayable 是加权平均混合,多个动画姿态按权重比例融合,适合同一部位的过渡;AnimationLayerMixerPlayable 是分层覆盖,配合 AvatarMask 可以做上下身分离,适合局部动画叠加。实际项目中 LayerMixer 更常用,因为”上半身攻击、下半身跑步”这种需求很常见。

参考文章
  • 3.创建动画混合树.md
  • 9.PlayableGraph的分层混合.md

进阶题

3. PlayableGraph 的惰性求值是什么意思?为什么 Output 必须绑定 SourcePlayable?

题目

“图搭好了但没效果”是 Playable 新手常遇到的问题,请解释其背后的原因。

深入解析

PlayableGraph 采用惰性求值机制:

  • Graph 不会主动评估所有节点
  • 评估从 Output 出发,沿着连接关系”拉取”数据
  • 只有 Output 绑定的 SourcePlayable 及其上游节点才会被评估

常见错误

var graph = PlayableGraph.Create();
var output = AnimationPlayableOutput.Create(graph, "Output", animator);
var clipPlayable = AnimationClipPlayable.Create(graph, clip);
// 忘了这一行!
// output.SetSourcePlayable(clipPlayable);
graph.Play(); // 什么都不会发生

为什么这样设计

  • 性能优化:只评估真正需要的节点
  • 灵活性:可以在一张图里放多个分支,按需切换输出源
答题示例

PlayableGraph 是惰性求值的,评估从 Output 出发向上游拉取数据。如果 Output 没有调用 SetSourcePlayable 绑定源节点,Graph 就不知道从哪里开始评估,整个图都不会执行。这是新手最常踩的坑——代码不报错,但动画就是不播。

参考文章
  • 2.播放单个动画剪辑.md

4. DirectorUpdateMode 的四种模式分别适用于什么场景?

题目

请解释 DirectorUpdateMode 的四种模式及其适用场景。

深入解析
模式 时间源 特点 适用场景
GameTime Time.time 受 timeScale 影响 常规游戏动画
UnscaledGameTime Time.unscaledTime 不受 timeScale 影响 UI 动画、暂停菜单
DSPClock 音频系统时钟 精确音频同步 音频播放、节奏游戏
Manual 手动调用 Evaluate() 完全可控 回放、录制、离线烘焙

Manual 模式的特殊价值

  • 不调 Evaluate() 图就不会推进
  • 调一次推一帧,时间完全受控
  • 适合需要确定性求值的场景
graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
// 在需要的时候手动推进
graph.Evaluate(deltaTime);
答题示例

GameTime 跟随游戏时间,适合常规动画;UnscaledGameTime 忽略暂停,适合 UI;DSPClock 跟音频时钟,适合音游;Manual 模式最灵活,调用 Evaluate 才推进一帧,适合回放录制这类需要精确控制时间的场景。理解 Manual 模式后,其他三种模式本质上就是 Unity 帮你自动 Evaluate 而已。

参考文章
  • 6.控制PlayableGraph树的播放状态和时序.md
  • 8.PlayableGraph的驱动更新模式.md

5. 如何实现自定义 Playable 节点?回调时序是怎样的?

题目

请说明 ScriptPlayable 和 PlayableBehaviour 的配合方式,以及回调的执行顺序。

深入解析

创建自定义节点

public class MyBehaviour : PlayableBehaviour
{
    public float duration;
    
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        // 求值前调用,适合做权重计算
    }
    
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        // 求值时调用,playerData 是 Output 绑定的目标
    }
}

// 使用
var scriptPlayable = ScriptPlayable<MyBehaviour>.Create(graph);
var behaviour = scriptPlayable.GetBehaviour();
behaviour.duration = 2f;

回调时序

  1. OnPlayableCreate:节点创建时
  2. OnGraphStart:图开始播放时
  3. OnBehaviourPlay:节点开始播放时
  4. PrepareFrame:每帧求值前
  5. ProcessFrame:每帧求值时
  6. OnBehaviourPause:节点暂停时
  7. OnGraphStop:图停止时
  8. OnPlayableDestroy:节点销毁时

设计建议:PlayableBehaviour 只负责帧逻辑,状态管理交给外部 MonoBehaviour。

答题示例

用 ScriptPlayable<T>.Create() 创建节点,GetBehaviour() 拿到 Behaviour 实例写逻辑。回调顺序是 OnGraphStart → OnBehaviourPlay → PrepareFrame → ProcessFrame → OnBehaviourPause → OnGraphStop。PrepareFrame 在求值前调用适合算权重,ProcessFrame 在求值时调用适合处理输出。建议 Behaviour 只管帧逻辑,状态放外面管理。

参考文章
  • 7.自定义Playable.md

深度题

6. Playable 和 AnimatorController 应该如何选型?它们是什么关系?

题目

在项目中应该如何选择 Playable 和 AnimatorController?它们是替代关系吗?

深入解析

它们不是替代关系,而是互补关系

维度 Playable AnimatorController
驱动方式 程序驱动,运行时动态构建 配置驱动,可视化编辑
维护者 程序 策划、美术可维护
灵活度 高,可动态拼装 低,结构固定
学习成本

AnimatorController 的优势

  • State、Transition、BlendTree 可视化编辑
  • 策划和美术能直接维护
  • 适合稳定的状态切换流程(Idle/Move/Attack)

Playable 的优势

  • 运行时动态构建和拆解图结构
  • 精确控制时间推进(Manual 模式)
  • 多类型内容统一调度(动画+音频+脚本)
  • 分层混合更灵活

实际项目中的共存模式

  • Controller 管宏观状态切换
  • Playable 负责局部叠加或临时接管
  • AnimatorControllerPlayable 可以作为 PlayableGraph 中的一个节点

适合用 Playable 的场景

  1. 动画结构运行时动态变化
  2. 多类型内容统一调度
  3. 需要 Manual 模式做确定性求值
  4. 分层混合但不想走 AnimatorController 的 Layer
答题示例

两者不是替代关系。AnimatorController 是配置驱动的状态机,策划美术能在编辑器里维护,适合稳定的状态切换;Playable 是程序驱动的求值图,灵活度高,适合动态结构和精确时间控制。实际项目里经常共存:Controller 管宏观状态,Playable 做局部叠加或临时接管。AnimatorControllerPlayable 本身就能作为图中的一个节点参与混合。

参考文章
  • 1.概述.md
  • 4.混合AnimationClip和AnimatorController.md

7. PlayableGraph 的资源管理有哪些注意事项?忘记销毁会有什么后果?

题目

PlayableGraph 的生命周期管理需要注意什么?如何避免资源泄漏?

深入解析

关键原则PlayableGraph.Create() 创建的图不会自动销毁,必须显式调用 graph.Destroy()

正确的生命周期管理

private PlayableGraph graph;

private void Awake()
{
    graph = PlayableGraph.Create();
    // ... 建图
}

private void OnDisable()
{
    // 方案一:暂停
    graph.Stop();
}

private void OnDestroy()
{
    // 方案二:销毁(必须)
    graph.Destroy();
}

忘记销毁的后果

  1. 句柄和资源残留
  2. PlayableGraph Visualizer 中出现”幽灵图”
  3. 内存泄漏
  4. 多次进入场景后资源累积

常见陷阱

  • 只在 OnDisable 里 Stop,忘了 OnDestroy 里 Destroy
  • 动态创建的图没有对应的销毁逻辑
  • 对象池复用时图没有正确重置

最佳实践

  • 建图和销毁成对出现
  • 使用 using 或 try-finally 确保销毁
  • 在编辑器下用 Visualizer 定期检查是否有残留图
答题示例

PlayableGraph.Create() 出来的图不会自动销毁,必须显式调用 Destroy()。一般放在 OnDestroy 里,和 Awake/Start 里的 Create 成对。忘记销毁会导致句柄残留、内存泄漏,PlayableGraph Visualizer 里能看到越来越多的幽灵图。动态创建图的时候尤其要注意,建图和销毁必须成对。

参考文章
  • 2.播放单个动画剪辑.md
  • 6.控制PlayableGraph树的播放状态和时序.md

8. Playable 系统的源码是如何分层的?Handle 层扮演什么角色?

题目

请简述 Playable 系统的分层架构,以及 Handle 层在其中的关键作用。

深入解析

Playable 系统从 C# 侧看分为六层:

层级 核心类型 职责
C++ 黑盒层 引擎内部 评估、遍历、数据流、Job 等核心逻辑
Handle 层 PlayableHandle、PlayableOutputHandle 真正控制时间、状态、端口、权重
包装层 Playable、PlayableOutput、ScriptPlayable<T> C# API 入口,转发操作到 Handle
数据层 FrameData 帧上下文,携带 deltaTime、weight、evaluationType
驱动层 PlayableGraph、PlayableDirector 图的创建、连接、评估、销毁
资源层 PlayableAsset、PlayableBinding 描述”播什么”和”有哪些输出”

Handle 层的关键地位

  • Playable / PlayableOutput 只是轻量包装,真正的状态、时间、端口、权重都落在句柄上
  • 调用链:上层 API → Handle → bindings(C++ FreeFunction)→ 引擎内部评估
  • 看到 extern / FreeFunction 就进入了 C++ 黑盒

PlayableHandle 的核心方法

  • 有效性与类型:IsValid() / GetPlayableType()
  • 播放控制:Play() / Pause() / GetPlayState() / SetSpeed()
  • 时间与时序:GetTime() / SetTime() / GetDuration()
  • 端口与连接:GetInputCount() / SetInputCount() / GetInputWeight()
  • 图关系:GetGraph() / GetInputHandle()

FrameData 的作用

  • 由引擎在评估阶段生成
  • 携带 deltaTime、weight、evaluationType、effectivePlayState
  • 传入 PrepareFrame / ProcessFrame 回调
答题示例

Playable 系统分六层:C++ 黑盒层做核心评估,Handle 层是 C# 能触达的最底层,包装层是 API 入口,数据层是帧上下文,驱动层管图的创建和销毁,资源层描述播放内容。Handle 层是关键——Playable 和 PlayableOutput 只是壳,真正的时间、状态、端口、权重都落在 PlayableHandle 和 PlayableOutputHandle 上。调用链是 API → Handle → C++ FreeFunction → 引擎内部。

参考文章
  • 10.Playable源码浅析.md


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏