概述

Playables API 推出已经一年有余(2017–07–04 New in Unity 2017.1)。即使你没时间其他的新功能, 也应该看看这个 Playable API .做过大型游戏的同学无论你是做过 2D或3D 只要使用过 Animaiton Controller,或多或少体会过被 蜘蛛网(复杂状态机过渡) 支配的恐惧。当下有了 Playable API 可供使用,我们能轻易的向 Legacy animation API 的使用习惯靠拢 — 高效及易于定制。

一个简单的状态机,当然这不是我们想要的。

在我看来 Playable API 的目的就是为了替换掉Legacy动画系统,并且兼容Timeline(本篇不介绍timeline 感兴趣的可以自己去看看)。总的一个词概括就是 【dynamically】,如同使用组件一般的灵活。

目前我在测试中使用了 UNITY2018.1+ 编辑器。如果不使用该可视化插件您在 UNITY5.x 版本就能使用Playable API。 使用5.x版本的Playable API 时请注意后续的代码API变更,某些函数名或调用方式可能已经更改,如果从未使用过 建议从 UNITY2017+ 开始入手。

准备工作(可跳过)

调试工具 graph-visualizer

我们先Clone UNITY TECHNOLOGIES 提供的 Playable 可视化工具,便于后续的理解与调试。

1
git clone https://github.com/Unity-Technologies/graph-visualizer

我在这里直接克隆到测试工程中,目前此工具支持的UNITY版本如下:

Unity version Release
2018.1+ v2.2 (master)
2017.1+ v1.1

如何使用

Clone成功后 打开 Window/graph-visualizer ,在创建PlayGraph的地方调用 GraphVisualizerClient.Show(graph) 即可,在运行时可看到动态创建的树形结构

您将会得到这样的画面 右侧为示范图例,可手动关闭。

Playable基础结构


这两个结构体是我们最需要关注的部分,我们可以结合上文的 PlayableGraph 例图来总结一下规律。 AnimationOutput 是根节点,Playable 是可以通过 后为XXXXXMixerPlayable 来进行组合 或 单独使用,并且至少有一个才能构成最简单的 PlayableGraph。

<font color=#3976C3>小技巧:编辑器中输入MixerPlayable 可以看到所有可用的Mixer(结构体),通过这些类 我们可以轻易的将一个个不同种类的节点连接在一起从而形成新的树形结构。</font>

快速上手案例 动画的播放

创建一个最基础的 PlayableGraph

我们使用五行代码就创建了一个最基础的 PlayableGraph。

1
2
3
4
5
6
7
playableGraph = PlayableGraph.Create("test graph");
var playableOutput = AnimationPlayableOutput.Create(playableGraph, "test Animation",_animator = GetComponent<Animator>());

var clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);
playableOutput.SetSourcePlayable(clipPlayable);

playableGraph.Play();

⚠️ 您同样可以使用一行代码就能调用此动画,这里需要注意的是Animator不能为空,否则编辑器会直接crash而不报异常。

1
AnimationPlayableUtilities.PlayClip(GetComponent<Animator>(), clip, out playableGraph);

创建最基础的两动画融合

这里我们新增了几行代码,值得注意的是截图所示mixer节点中有两个重要的属性 inputcount 与 outputcount ,顾名思义。我们既可以从 PlayableGraph 的级别指定两个AnimationClip连接到mixer上,可以使用mixer 直接去进行链接点设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
playableGraph = PlayableGraph.Create("test graph");
var playableOutput = AnimationPlayableOutput.Create(playableGraph, "test Animation",_animator = GetComponent<Animator>());

mixerPlayable = AnimationMixerPlayable.Create(playableGraph, 2);
playableOutput.SetSourcePlayable(mixerPlayable);

var clipPlayable1 = AnimationClipPlayable.Create(playableGraph, clip1);
var clipPlayable2 = AnimationClipPlayable.Create(playableGraph, clip2);

//playableGraph.Connect(clipPlayable1,0,mixerPlayable,0);
//playableGraph.Connect(clipPlayable2, 0, mixerPlayable, 1);
//上面的两行与下面的两行代码是等价的
mixerPlayable.ConnectInput(0,clipPlayable1,0);
mixerPlayable.ConnectInput(1,clipPlayable2,0);

weight = Mathf.Clamp01(weight);
mixerPlayable.SetInputWeight(0, 1.0f-weight);//分别设置两个数据端口的权重
mixerPlayable.SetInputWeight(1, weight);

playableGraph.Play();
GraphVisualizerClient.Show(playableGraph);

混合使用 AnimationClip 与 AnimatorController

修改上文【创建最基础的两动画融合】所提供的代码块 即可。AnimatorController 可以看作是一颗子树,它可以轻易的使用mixerPlayable与其他的clip进行融合,这一切的便利归功于UNITY重写的通用动画调用层。

在视频中您可以观察最左侧在融合权重的变化下两颗树的融合情况:

1
var clipPlayable2 = AnimatorControllerPlayable.Create(playableGraph, controller);

增加 PlayableGraph 的输出口(类型)

看到这里我们应该慢慢的有一些概念了

记忆技巧:右输入口(数量) 左输出口(数量)。 这两个东西贯穿整体的设计中。即使最基础的 playable 组件都有这两个属性。这两个属性也是我们设计一棵树的常规操作。PlayableGraph 就是这颗树,额外需要理解的是: 它允许(限制了) 我们需要用不同 类型的主分支 然后才扩展出其他分支。

⚠️ 所有子节点初始化的时候都没有设置输出输入端口数量 您需要手动指派。否则会报错:Connecting invalid input

<font color=#3976C3>小技巧:编辑器中输入playableoutput 可以看到所有可用的类型(结构体),通过这些类 我们可以轻易的给PlayableGraph添加不同类型的子分支。</font>

目前我们已经用过了AnimationPlayableOutput 下面演示一些 AudioPlayableOutput 的案例。

1
2
3
4
5
playableGraph = PlayableGraph.Create();
var audioOutput = AudioPlayableOutput.Create(playableGraph, "Audio", GetComponent<AudioSource>());
var audioClipPlayable = AudioClipPlayable.Create(playableGraph, audioClip, true);
audioOutput.SetSourcePlayable(audioClipPlayable);
playableGraph.Play();

借此代码抛砖引玉,另外几种类型(主分支)也是同样的使用方法

控制PlayableGraph(树)中的状态切换

因为 XXXPlayable 都是继承自同接口,以下函数同样适用于其他类型。

1
2
3
audioClipPlayable.Pause();     //暂停
audioClipPlayable.Play(); //播放
audioClipPlayable.SetDelay(1); //设置延迟

参考代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
private float i = 0;
private void Update()
{
i += Time.deltaTime;
if (i > 3)
{
if (audioClipPlayable.GetPlayState() == PlayState.Paused)
audioClipPlayable.Play();
else
audioClipPlayable.Pause();
i = 0;
}
}

控制树的时间

因为 XXXPlayable 都是继承自同接口,以下函数同样适用于其他类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public float time;

void Start()
{
playableGraph = PlayableGraph.Create();

clipPlayable = AnimationPlayableUtilities.PlayClip(GetComponent<Animator>(), clip, out playableGraph);
clipPlayable.Pause();

playableGraph.Play();
GraphVisualizerClient.Show(playableGraph);
}

void Update()
{
weight = Mathf.Clamp01(weight);
clipPlayable.SetTime(time); //控制播放进度,无上限超出部分自动循环动画
}

在视频中您可以观察时间的前进与后退直接作用于当前动画节点:

使用 PlayableBehaviour

顾名思义 PlayableBehaviour 就是自己定义的Playable行为,我们实现了一个 PlayQueuePlayable 并且制定一些特殊的逻辑在其中(循环播放clip)。

  1. 我们先看下它是如何初始化的:

    1
    2
    3
    var playQueuePlayable = ScriptPlayable<PlayQueuePlayable>.Create(playableGraph);//泛型工厂
    var playQueue = playQueuePlayable.GetBehaviour();//容器函数 获取饮用
    playQueue.Initialize(clipsToPlay, playQueuePlayable, playableGraph);//用户自定义初始化函数

    从第二行代码中使用了泛型工厂来创建 playablebehavior 我们可以看出它是一个通用的容器,而并是开箱即用的树节点。

  2. 容器初始化完毕,将它设置为主动画分支的下一级节点,操作完毕。

    1
    2
    3
    var playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());
    playableOutput.SetSourcePlayable(playQueuePlayable,0);
    playableGraph.Play();

那么现在我们已经了解了如何使用这个容器。现在开始探究容器的内部实现:

1
2
3
4
5
6
7
8
9
10
public virtual void OnGraphStart(Playable playable){}
public virtual void OnGraphStop(Playable playable){}
public virtual void OnPlayableCreate(Playable playable){}
public virtual void OnPlayableDestroy(Playable playable){}
public virtual void OnBehaviourDelay(Playable playable, FrameData info){}
public virtual void OnBehaviourPlay(Playable playable, FrameData info){}
public virtual void OnBehaviourPause(Playable playable, FrameData info){}
public virtual void PrepareData(Playable playable, FrameData info){}
public virtual void PrepareFrame(Playable playable, FrameData info){}
public virtual void ProcessFrame(Playable playable, FrameData info, object playerData){}

我们可以看到,动画周期内的大部分检测与判断我们都能在这里进行.

最后我们再看一下自定义类 PlayQueuePlayable 的行为逻辑。

  1. 上文提到的 Initialize 函数,这里动态创建了 AnimationClipPlayable 并且指派端口链接到了 PlayQueuePlayable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public void Initialize(AnimationClip[] clipsToPlay, Playable owner, PlayableGraph graph)
    {
    owner.SetInputCount(1);
    mixer = AnimationMixerPlayable.Create(graph, clipsToPlay.Length);
    graph.Connect(mixer, 0, owner, 0);

    for (int clipIndex = 0 ; clipIndex < mixer.GetInputCount() ; ++clipIndex)
    {
    graph.Connect(AnimationClipPlayable.Create(graph, clipsToPlay[clipIndex]), 0, mixer, clipIndex);
    }
    }
  2. 重写 PrepareFrame 函数,用来检测动画帧在播放前的逻辑(一帧调一次) 这里需要避免复杂的检测逻辑,我们实现了简单的轮播逻辑:让动画一个接一个的切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private int m_CurrentClipIndex = -1;
private float m_TimeToNextClip;
private Playable mixer;

override public void PrepareFrame(Playable owner, FrameData info)
{
if (mixer.GetInputCount() == 0)
return;

// Advance to next clip if necessary
m_TimeToNextClip -= (float)info.deltaTime;

if (m_TimeToNextClip <= 0.0f)
{
m_CurrentClipIndex++;

if (m_CurrentClipIndex >= mixer.GetInputCount())
m_CurrentClipIndex = 0;

var currentClip = (AnimationClipPlayable)mixer.GetInput(m_CurrentClipIndex);

currentClip.SetTime(0);
m_TimeToNextClip = currentClip.GetAnimationClip().length;
}

for (int clipIndex = 0 ; clipIndex < mixer.GetInputCount(); ++clipIndex)
{
if (clipIndex == m_CurrentClipIndex)
mixer.SetInputWeight(clipIndex, 1.0f);
else
mixer.SetInputWeight(clipIndex, 0.0f);
}
}

小结

本章节带大家过了一遍Playables API的基本使用,并且抛砖引玉的进行了一些技巧讲解,相信经过本章节的学习大家一定对 Playables API 充满了兴趣,在下一章我们讲讲更详细的 手部IK 与 分层动画 的运用。我们可以借此实现人物的攀爬/射击/复杂运动等。

参考

🔗 UNITY官方微信号相关

🔗 UNITY官方文档相关