7.Playable自定义Playable
7.1 知识点
PlayableGraph、IPlayable、Playable、ScriptPlayable、PlayableBehaviour 之间的关系
PlayableGraph
图的容器,管理所有节点和输出。用PlayableGraph.Create()创建,负责图的生命周期。PlayableOutput
图的输出节点,由PlayableGraph创建并绑定到具体目标(如Animator、AudioSource),用来把计算结果写回场景。IPlayable
播放节点的统一接口。Playable与各种具体 Playable 都是基于它来对外工作。Playable
一个轻量struct,内部是PlayableHandle的封装,实现IPlayable。常被当作通用节点类型使用。ScriptPlayable
实现IPlayable的泛型struct,用来包装PlayableBehaviour。它与Playable之间有显式/隐式转换,因此可以当作普通节点连接到图中。PlayableBehaviour
业务逻辑基类,包含PrepareFrame()、OnPlayableCreate()等回调方法。
关系总结:
PlayableGraph管理所有节点与输出PlayableOutput由PlayableGraph创建并受其管理,图的“出口”通过它把结果输出到目标Playable/ScriptPlayable<T>都实现IPlayableScriptPlayable<T>内部持有PlayableBehaviour- 业务逻辑通过
ScriptPlayable<T>.GetBehaviour()取得 PrepareFrame()等回调里会传入owner(包装它的Playable节点)
classDiagram
%% 图容器 / 输出
PlayableGraph "1" o-- "many" Playable : 管理/包含节点
PlayableGraph "1" o-- "many" PlayableOutput : 管理/包含输出
PlayableOutput --> Playable : SetSourcePlayable() 绑定源节点
%% 接口与实现
IPlayable <|.. Playable
IPlayable <|.. ScriptPlayable~T~
IPlayable <|.. AnimationClipPlayable
IPlayable <|.. AnimationMixerPlayable
IPlayable <|.. AudioClipPlayable
%% 行为逻辑(节点包装 Behaviour)
ScriptPlayable~T~ --> PlayableBehaviour : 包装/持有
PlayableBehaviour <|-- UserDefinedBehaviour
%% 关键接口(节选,方便对照)
class PlayableGraph {
+Create() 创建图
+Play() 播放
+Stop() 停止
+Destroy() 销毁/释放
+Connect() 连接节点
+Evaluate() 手动评估
}
class Playable {
<> 实现 IPlayable
+GetGraph() 获取所属 Graph
}
class ScriptPlayable~T~ {
<>
+GetBehaviour() 获取 Behaviour 实例
}
class PlayableBehaviour {
+OnPlayableCreate() 创建回调
+PrepareFrame() 帧前准备
+ProcessFrame() 帧处理
+OnPlayableDestroy() 销毁回调
}
class PlayableOutput {
<> 实现 IPlayableOutput
+SetSourcePlayable(p : Playable) 指定输出来源
}
class UserDefinedBehaviour {
<<继承 PlayableBehaviour>>
}
自定义Playable实现目标
本例的目标是做一个“动画队列”:传入 AnimationClip[],按顺序播放,单个 clip 播完自动切到下一个,并循环回到第一个。
思路和步骤
实现思路:
- 在
PlayableBehaviour里控制切换逻辑,用ScriptPlayable<T>变成节点,再接入图并输出。
核心步骤:
- 实现自定义 PlayableBehaviour(继承
PlayableBehaviour,实现Initialize()与PrepareFrame()) - 在 MonoBehaviour 中使用(创建
ScriptPlayable节点,获取PlayableBehaviour实例初始化并连接输出)
代码实现
步骤1:实现自定义 PlayableBehaviour
1.1 继承 PlayableBehaviour 并定义字段
public class PlayQueueClipPlayableBehaviour : PlayableBehaviour
{
private int currentClipIndex = -1;
private float timeToNextClip;
private AnimationMixerPlayable animationMixerPlayable;
}
继承 PlayableBehaviour,定义需要的字段:currentClipIndex 记录当前播放索引,timeToNextClip 记录到下一个剪辑的剩余时间,animationMixerPlayable 是内部混合器。
1.2 实现 Initialize 方法
public void Initialize(AnimationClip[] animationClipArray, Playable ownerScriptPlayable, PlayableGraph playableGraph)
{
// 设置 ownerScriptPlayable 节点的输入数量为 1
ownerScriptPlayable.SetInputCount(1);
// 创建混合器,输入数量等于动画剪辑数量
animationMixerPlayable = AnimationMixerPlayable.Create(playableGraph, animationClipArray.Length);
// 将混合器连接到 ownerScriptPlayable 节点的输入端口
playableGraph.Connect(animationMixerPlayable, 0, ownerScriptPlayable, 0);
// 设置 ownerScriptPlayable 节点的输入权重为 1
ownerScriptPlayable.SetInputWeight(0, 1f);
// 为每个动画剪辑创建可播放项并连接到混合器
for (int clipIndex = 0; clipIndex < animationMixerPlayable.GetInputCount(); clipIndex++)
{
playableGraph.Connect(AnimationClipPlayable.Create(playableGraph, animationClipArray[clipIndex]), 0, animationMixerPlayable, clipIndex);
animationMixerPlayable.SetInputWeight(clipIndex, 0.0f);
}
// 初始化第一个剪辑的状态
if (animationMixerPlayable.GetInputCount() > 0)
{
currentClipIndex = 0;
AnimationClipPlayable firstClip = (AnimationClipPlayable)animationMixerPlayable.GetInput(0);
firstClip.SetTime(0f);
timeToNextClip = firstClip.GetAnimationClip().length;
animationMixerPlayable.SetInputWeight(0, 1.0f);
}
}
这个 Initialize() 主要做四件事:给 ScriptPlayable 开输入端口、创建 mixer、把每个 clip 接到 mixer、初始化第一个要播的 clip。
这里需要传入 ownerScriptPlayable:PlayableBehaviour 本身拿不到外层节点,但初始化阶段要连线/设端口,所以必须由外部传入。
1.3 重载 PrepareFrame 方法
public override void PrepareFrame(Playable owner, FrameData info)
{
// 如果没有输入,直接返回
if (animationMixerPlayable.GetInputCount() == 0)
return;
// 减少距离下一个剪辑的时间
timeToNextClip -= (float)info.deltaTime;
// 如果当前剪辑播放完毕,切换到下一个剪辑
if (timeToNextClip <= 0.0f)
{
// 移动到下一个剪辑索引
currentClipIndex++;
// 如果已经播放完所有剪辑,循环回到第一个
if (currentClipIndex >= animationMixerPlayable.GetInputCount())
currentClipIndex = 0;
// 获取当前剪辑的可播放项
AnimationClipPlayable currentClip = (AnimationClipPlayable)animationMixerPlayable.GetInput(currentClipIndex);
// 重置时间,使下一个剪辑从开头开始播放
currentClip.SetTime(0f);
// 设置距离下一个剪辑的时间为当前剪辑的长度
timeToNextClip = currentClip.GetAnimationClip().length;
}
// 调整所有输入权重:当前播放的剪辑权重为 1.0,其他为 0.0
for (int clipIndex = 0; clipIndex < animationMixerPlayable.GetInputCount(); clipIndex++)
{
if (clipIndex == currentClipIndex)
animationMixerPlayable.SetInputWeight(clipIndex, 1.0f);
else
animationMixerPlayable.SetInputWeight(clipIndex, 0.0f);
}
}
PrepareFrame() 每帧都会被 Unity 调用,这里是业务逻辑入口。这个例子里:当前 clip 播完就切到下一个,并把 mixer 的权重切到当前那一路。
步骤2:在 MonoBehaviour 中使用自定义 PlayableBehaviour
2.1 创建图并设置更新模式
// 创建 Playable 图并设置更新模式为 GameTime
playableGraph = PlayableGraph.Create();
playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
先创建 PlayableGraph,并设置时间更新模式为游戏时间。
2.2 创建 ScriptPlayable 节点
// 使用 ScriptPlayable 创建自定义的 PlayQueueClipPlayableBehaviour
ScriptPlayable<PlayQueueClipPlayableBehaviour> playQueueScriptPlayable =
ScriptPlayable<PlayQueueClipPlayableBehaviour>.Create(playableGraph);
用 ScriptPlayable<T>.Create() 创建节点,T 填自定义 PlayableBehaviour 类型。ScriptPlayable<T> 可隐式转成 Playable,后续连线可以当普通节点用。
2.3 获取 PlayableBehaviour 实例并初始化
// 获取 PlayableBehaviour 实例
PlayQueueClipPlayableBehaviour playQueueClipPlayableBehaviour = playQueueScriptPlayable.GetBehaviour();
// 初始化动画队列
playQueueClipPlayableBehaviour.Initialize(animationClipArray, playQueueScriptPlayable, playableGraph);
用 GetBehaviour() 拿到 PlayableBehaviour 实例,再调用初始化方法。注意初始化时要把 ScriptPlayable 节点本身传进去,因为 PlayableBehaviour 自己拿不到“外壳节点”。
2.4 创建动画输出并连接到 ScriptPlayable 节点
// 创建动画输出,连接到 Animator 组件
AnimationPlayableOutput animationPlayableOutput =
AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());
// 设置输出源为自定义的 PlayQueueClipPlayableBehaviour
animationPlayableOutput.SetSourcePlayable(playQueueScriptPlayable);
创建动画输出节点,绑定到 Animator。然后把 ScriptPlayable 节点设为输出源。这里传的是节点,不是节点内部的 Behaviour 对象。
2.5 播放图
// 播放该图
playableGraph.Play();
开始播放图,之后每帧 Unity 会自动更新这张图,PlayableBehaviour 的 PrepareFrame() 也会被调用。
2.6 清理资源
void OnDisable()
{
// 销毁该图创建的所有可播放项和输出
playableGraph.Destroy();
}
组件禁用时调用 Destroy() 释放资源(节点与输出),避免内存泄漏。建议放在 OnDisable(),组件禁用时即可释放。
效果展示



可以看到 idle / run / attack 会循环播放:播完一个就切到下一个。
7.2 知识点代码
PlayQueueSample.cs
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
/// <summary>
/// 使用自定义 PlayableBehaviour 的示例组件
/// 该组件演示了如何使用 ScriptPlayable 创建和使用自定义的 PlayableBehaviour
/// </summary>
[RequireComponent(typeof(Animator))]
public class PlayQueueSample : MonoBehaviour
{
/// <summary>
/// 要按顺序播放的动画剪辑数组
/// </summary>
public AnimationClip[] animationClipArray;
/// <summary>
/// Playable 图,用于管理动画播放流程
/// </summary>
PlayableGraph playableGraph;
/// <summary>
/// 初始化并创建动画队列
/// </summary>
void Start()
{
// 创建 Playable 图并设置更新模式为 GameTime
playableGraph = PlayableGraph.Create();
playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
// 使用 ScriptPlayable 创建自定义的 PlayQueueClipPlayableBehaviour
// playQueueScriptPlayable 是 PlayableGraph 中的节点(ScriptPlayable<PlayQueueClipPlayableBehaviour> 类型)
// ScriptPlayable<T> 是 Playable 的一个具体实现,它包装了 PlayableBehaviour
// ScriptPlayable<T> 可以隐式转换为 Playable,所以可以当作 Playable 使用
// 它包装了 PlayQueueClipPlayableBehaviour 行为,是图中的一个可连接节点
ScriptPlayable<PlayQueueClipPlayableBehaviour> playQueueScriptPlayable = ScriptPlayable<PlayQueueClipPlayableBehaviour>.Create(playableGraph);
// 获取 PlayableBehaviour 实例
// playQueueClipPlayableBehaviour 是实际的业务逻辑对象(PlayableBehaviour 类型)
// 它包含 Initialize、PrepareFrame 等方法,负责动画队列的具体实现
// 注意:PlayableBehaviour 无法直接获取包装它的 ScriptPlayable
// 虽然在 PrepareFrame 等方法中会传入 owner 参数(就是包装它的 Playable),
// 但 Initialize 是在外部调用的,此时还没有进入 Playable 的生命周期,所以需要显式传入
PlayQueueClipPlayableBehaviour playQueueClipPlayableBehaviour = playQueueScriptPlayable.GetBehaviour();
// 初始化动画队列
// ownerScriptPlayable 参数传入 playQueueScriptPlayable,表示"拥有此行为的 ScriptPlayable 节点"
// 参数类型是 Playable,但传入的是 ScriptPlayable<PlayQueueClipPlayableBehaviour>
// 这是因为 ScriptPlayable<T> 可以隐式转换为 Playable(通过隐式转换操作符)
// 在 Initialize 中需要操作 Playable 节点(如设置输入数量、连接其他节点),所以需要传入节点本身
playQueueClipPlayableBehaviour.Initialize(animationClipArray, playQueueScriptPlayable, playableGraph);
// 创建动画输出,连接到 Animator 组件
AnimationPlayableOutput animationPlayableOutput =
AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());
// 设置输出源为自定义的 PlayQueueClipPlayableBehaviour
// 注意:这里传入的是 playQueueScriptPlayable(Playable 节点),而不是 playQueueClipPlayableBehaviour(Behaviour 行为)
// 因为输出需要连接到图中的一个节点,而不是节点内部的行为
// SetSourcePlayable 接受 Playable 类型参数,ScriptPlayable<T> 可以隐式转换
animationPlayableOutput.SetSourcePlayable(playQueueScriptPlayable);
// 播放该图
playableGraph.Play();
}
/// <summary>
/// 组件禁用时清理资源,销毁 Playable 图及其所有可播放项和输出
/// </summary>
void OnDisable()
{
// 销毁该图创建的所有可播放项和输出
playableGraph.Destroy();
}
}
PlayQueueClipPlayableBehaviour.cs
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
/// <summary>
/// 自定义 PlayableBehaviour,用于按顺序播放多个动画剪辑
/// 该组件演示了如何创建自定义的可播放项,通过重载 PrepareFrame() 方法控制动画播放顺序
/// 一次只播放一个动画剪辑,当前剪辑播放完毕后自动切换到下一个
/// </summary>
public class PlayQueueClipPlayableBehaviour : PlayableBehaviour
{
/// <summary>
/// 当前播放的动画剪辑索引
/// </summary>
private int currentClipIndex = -1;
/// <summary>
/// 距离下一个剪辑的时间
/// </summary>
private float timeToNextClip;
/// <summary>
/// 动画混合器,用于混合多个动画剪辑
/// </summary>
private AnimationMixerPlayable animationMixerPlayable;
/// <summary>
/// 初始化动画队列
/// </summary>
/// <param name="animationClipArray">要播放的动画剪辑数组</param>
/// <param name="ownerScriptPlayable">拥有此行为的 ScriptPlayable 节点(即 ScriptPlayable<PlayQueueClipPlayableBehaviour>)</param>
/// <param name="playableGraph">PlayableGraph</param>
///
/// 【Playable 和 ScriptPlayable 的关系说明】
/// - Playable 是一个结构体,是所有 Playable 类型的基类/接口
/// - ScriptPlayable<T> 是 Playable 的一个具体实现,它包装了一个 PlayableBehaviour
/// - ScriptPlayable<T> 可以隐式转换为 Playable(通过隐式转换操作符)
/// - 所以参数类型是 Playable,但实际传入的是 ScriptPlayable<PlayQueueClipPlayableBehaviour>
///
/// 【为什么需要显式传入 ownerScriptPlayable】
/// - PlayableBehaviour 无法直接获取包装它的 ScriptPlayable
/// - 虽然在 PrepareFrame、OnPlayableCreate 等方法中会传入 owner 参数(就是包装它的 Playable),
/// 但 Initialize 是在外部调用的,此时还没有进入 Playable 的生命周期
/// - 因此需要在外部获取 ScriptPlayable 后,显式传入给 Initialize 方法
///
/// 【ownerScriptPlayable 和当前 PlayableBehaviour 实例的关系】
/// - ownerScriptPlayable 是 PlayableGraph 中的节点(容器),类型是 ScriptPlayable<PlayQueueClipPlayableBehaviour>
/// - 当前对象(this)是这个节点包含的行为(业务逻辑),类型是 PlayQueueClipPlayableBehaviour
/// - 通过 ownerScriptPlayable 可以操作节点在图中的连接和配置(如设置输入数量、连接其他节点)
/// - 在 PrepareFrame 等方法中,Unity 会自动传入 owner 参数,就是同一个 ownerScriptPlayable</param>
public void Initialize(AnimationClip[] animationClipArray, Playable ownerScriptPlayable, PlayableGraph playableGraph)
{
// 设置 ownerScriptPlayable 节点的输入数量为 1
// ownerScriptPlayable 是 PlayableGraph 中的节点,需要设置它有多少个输入端口
ownerScriptPlayable.SetInputCount(1);
// 创建混合器,输入数量等于动画剪辑数量
animationMixerPlayable = AnimationMixerPlayable.Create(playableGraph, animationClipArray.Length);
// 将混合器连接到 ownerScriptPlayable 节点的输入端口
// Connect 参数说明:
// 参数1:源 Playable(animationMixerPlayable - 混合器)
// 参数2:源输出端口(0 - 混合器只有一个输出端口)
// 参数3:目标 Playable(ownerScriptPlayable - 拥有此行为的 ScriptPlayable 节点)
// 参数4:目标输入端口(0 - ownerScriptPlayable 的第一个输入端口)
// 这样混合器的输出会连接到 ownerScriptPlayable 节点,ownerScriptPlayable 再输出到图的外部
playableGraph.Connect(animationMixerPlayable, 0, ownerScriptPlayable, 0);
// 设置 ownerScriptPlayable 节点的输入权重为 1
// 表示混合器的输出完全传递给 ownerScriptPlayable 节点
ownerScriptPlayable.SetInputWeight(0, 1f);
// 为每个动画剪辑创建可播放项并连接到混合器
for (int clipIndex = 0; clipIndex < animationMixerPlayable.GetInputCount(); clipIndex++)
{
// 创建动画剪辑的可播放项并连接到混合器
playableGraph.Connect(AnimationClipPlayable.Create(playableGraph, animationClipArray[clipIndex]), 0, animationMixerPlayable, clipIndex);
// 设置初始权重为 0.0(PrepareFrame 会调整第一个剪辑的权重为 1.0)
animationMixerPlayable.SetInputWeight(clipIndex, 0.0f);
}
// 初始化第一个剪辑的状态
if (animationMixerPlayable.GetInputCount() > 0)
{
currentClipIndex = 0;
AnimationClipPlayable firstClip = (AnimationClipPlayable)animationMixerPlayable.GetInput(0);
firstClip.SetTime(0f);
timeToNextClip = firstClip.GetAnimationClip().length;
animationMixerPlayable.SetInputWeight(0, 1.0f);
}
}
/// <summary>
/// 重载 PrepareFrame 方法,每帧调用以更新播放状态
/// 控制动画剪辑的切换和权重调整
/// </summary>
/// <param name="owner">拥有此行为的 Playable 节点
/// 注意:这个 owner 参数就是 Initialize 时传入的 ownerScriptPlayable(同一个对象)
/// Unity 会在每帧自动调用此方法,并传入包装此 PlayableBehaviour 的 Playable 节点
/// 如果需要在 PrepareFrame 中操作节点,可以使用这个 owner 参数,而不需要保存 Initialize 时的 ownerScriptPlayable</param>
/// <param name="info">帧数据,包含时间信息</param>
public override void PrepareFrame(Playable owner, FrameData info)
{
// 如果没有输入,直接返回
if (animationMixerPlayable.GetInputCount() == 0)
return;
// 减少距离下一个剪辑的时间
timeToNextClip -= (float)info.deltaTime;
// 如果当前剪辑播放完毕,切换到下一个剪辑
if (timeToNextClip <= 0.0f)
{
// 移动到下一个剪辑索引
currentClipIndex++;
// 如果已经播放完所有剪辑,循环回到第一个
if (currentClipIndex >= animationMixerPlayable.GetInputCount())
currentClipIndex = 0;
// 获取当前剪辑的可播放项
AnimationClipPlayable currentClip = (AnimationClipPlayable)animationMixerPlayable.GetInput(currentClipIndex);
// 重置时间,使下一个剪辑从开头开始播放
currentClip.SetTime(0f);
// 设置距离下一个剪辑的时间为当前剪辑的长度
timeToNextClip = currentClip.GetAnimationClip().length;
}
// 调整所有输入权重:当前播放的剪辑权重为 1.0,其他为 0.0
for (int clipIndex = 0; clipIndex < animationMixerPlayable.GetInputCount(); clipIndex++)
{
if (clipIndex == currentClipIndex)
animationMixerPlayable.SetInputWeight(clipIndex, 1.0f);
else
animationMixerPlayable.SetInputWeight(clipIndex, 0.0f);
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com