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、权重求值,写自定义轨道的时候会反复用到——到时候就知道为什么要先花这么多篇幅把底层理清楚了。
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();
回调时序:OnGraphStart → OnBehaviourPlay → PrepareFrame → ProcessFrame → OnBehaviourPause → OnGraphStop
设计建议: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:节点包装,持有 PlayableHandlePlayableHandle:底层句柄,控制时间、状态、端口、权重PlayableGraph:图容器,负责创建、连接、评估、销毁PlayableOutput:输出包装,持有 PlayableOutputHandleScriptPlayable<T>:把 PlayableBehaviour 包装成可播放节点FrameData:帧上下文,由引擎生成并传入回调
类:
PlayableBehaviour:行为基类,提供生命周期回调默认实现PlayableAsset:资源基类,生成图节点PlayableDirector:驱动组件,管理 PlayableAsset 与 PlayableGraph
枚举:
PlayState:Paused / PlayingDirectorUpdateMode:GameTime / UnscaledGameTime / DSPClock / ManualDirectorWrapMode:Hold / Loop / NoneFrameData.EvaluationType:Evaluate / Playback
11.3 面试题精选
基础题
1. PlayableGraph 的基本组成有哪些?各自职责是什么?
题目
请简述 PlayableGraph 的三个核心组成部分及其职责。
深入解析
PlayableGraph 是 Unity 提供的一套底层接口,用于驱动时间轴内容。它由三个核心部分组成:
PlayableGraph:图的容器和调度者
- 管理图中的所有 Playable 节点和 PlayableOutput
- 控制节点之间的连接关系
- 负责生命周期管理(创建、播放、暂停、销毁)
Playable:图中的数据生产节点
- 叶子节点:直接持有可播放内容(如 AnimationClipPlayable)
- Mixer 节点:对多个输入做混合(如 AnimationMixerPlayable)
- 脚本节点:由脚本控制行为(ScriptPlayable)
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;
回调时序:
OnPlayableCreate:节点创建时OnGraphStart:图开始播放时OnBehaviourPlay:节点开始播放时PrepareFrame:每帧求值前ProcessFrame:每帧求值时OnBehaviourPause:节点暂停时OnGraphStop:图停止时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 的场景:
- 动画结构运行时动态变化
- 多类型内容统一调度
- 需要 Manual 模式做确定性求值
- 分层混合但不想走 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();
}
忘记销毁的后果:
- 句柄和资源残留
- PlayableGraph Visualizer 中出现”幽灵图”
- 内存泄漏
- 多次进入场景后资源累积
常见陷阱:
- 只在 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