概述 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
如何使用
Playable基础结构 这两个结构体是我们最需要关注的部分,我们可以结合上文的 PlayableGraph 例图来总结一下规律。 AnimationOutput 是根节点,Playable 是可以通过 后为XXXXXMixerPlayable 来进行组合 或 单独使用,并且至少有一个才能构成最简单的 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);
创建最基础的两动画融合 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);
mixerPlayable.ConnectInput(0 ,clipPlayable1,0 );
mixerPlayable.ConnectInput(1 ,clipPlayable2,0 );
weight = Mathf.Clamp01(weight);
mixerPlayable.SetInputWeight(0 , 1.0 f-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
目前我们已经用过了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
2
3
var playQueuePlayable = ScriptPlayable<PlayQueuePlayable>.Create(playableGraph);
var playQueue = playQueuePlayable.GetBehaviour();
playQueue.Initialize(clipsToPlay, playQueuePlayable, playableGraph);
从第二行代码中使用了泛型工厂来创建 playablebehavior 我们可以看出它是一个通用的容器,而并是开箱即用的树节点。
容器初始化完毕,将它设置为主动画分支的下一级节点,操作完毕。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 的行为逻辑。
上文提到的 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);
}
}
重写 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 ;
m_TimeToNextClip -= (float )info.deltaTime;
if (m_TimeToNextClip <= 0.0 f)
{
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.0 f);
else
mixer.SetInputWeight(clipIndex, 0.0 f);
}
}
小结 本章节带大家过了一遍Playables API的基本使用,并且抛砖引玉的进行了一些技巧讲解,相信经过本章节的学习大家一定对 Playables API 充满了兴趣,在下一章我们讲讲更详细的 手部IK 与 分层动画 的运用。我们可以借此实现人物的攀爬/射击/复杂运动等。
参考 🔗 UNITY官方微信号相关
🔗 UNITY官方文档相关