DungDaj 发表于 2023-2-26 15:08

【Unity】简单使用Playable API控制动画

什么是Playable?

Playable的官方介绍如下:
官方文档为:
此外官方也提供了一个Demo来展示如何使用Playable播放动画:
简单的概况下就是如下几点:

[*]Playable API的一个目的就是为了代替Legacy动画系统的Animation组件,允许动画重定向、Blend Tree等原本不支持的功能。(Blend Tree是比较耗性能的,要慎用)
[*]面向代码层,Playable API可以更直接的访问底层动画系统的接口,我们可以根据项目定制动画系统,而不是使用Animator。在Unity底层,驱动Playable Graph的实际上依然是Animator组件,但是我们完全可以像使用Animation组件一样使用Playable。
[*]可以通过Playable来扩展Timeline的功能。

Playable与Animator的比较

对于一些大型动作类游戏,往往会有很多的State或者Clip,并且Animator状态机是不允许运行时添加、删除动画的,它只能使用OverrideController来替换动画,这就可能导致需要一个巨大的Animator来满足所有可能的状态。以上原因就会导致我们的Animator密密麻麻的像蜘蛛网一样,非常难以维护,同时Override时也会带来非常大的性能问题。
可以参考下面两个分享:
而Playable它就允许在运行时创建、添加和删除Animation,这样也会使得我们的Playable结构非常的简单。
在Animator状态机中,是通过定义变量来间接控制权重的。而在Playable中,你可以直接控制动画的权重和时间以及其他属性,例如:我们可以让二个动画按0.2和0.8的权重混合。因此Playable相比Animator更加的灵活。
Playable还有更强大的融合特性,可以在Clip和AnimatorController之间混合,甚至无数个AnimatorController之间混合。在Animator系统中,二个State machine之间是不能过度的,但是如果使用了Playable,那就是可行的。所以我们完全可以混用Animator状态机和Playable。例如:一些角色的固定动画状态的转变我们可以继续用Animator状态机,而那些需要动态改变的功能我们就用Playable(这个思路很重要)。

Playable架构

Playable API大体上由二部分组成:Playable和PlayableOutput。
Playable是我们这套API的基本组成元素。为了避免Gc alloc,所有的Playable是用Struct实现的,并且继承了IPlayable接口,有如下几种类型:


PlayableOutput同样也都是Struct实现的,继承了IPlayableOutput接口,有如下几种类型:


可以发现使用Playable API不进可以用来控制动画的播放,还可以控制音频的播放,并且我们还可以使用脚本实现一些自定义的Playable。
如下图,就是一个由一个Playable和一个PlayableOutput组成的一个最简单的PlayableGraph。


所谓PlayableGraph我们可以认为是一个Animator或者是一个Manager,我们可以用它定义一系列的Playable和PlayableOutput,管理Playable的生命周期,创建、链接和销毁Playable。
每个PlayableGraph必须包含有至少一个PlayableOutput,而且PlayableOutput必须连接到至少一个Playable上,否则它是没有任何作用的。因此基本上,每一个PlayableOutput都会形成一颗树形结构。如下图,是一个较为复杂的PlayableGraph,里面除了包含动画相关的Playable与PlayableOutput,还包含音频相关和自定义的节点。


<hr/>逼逼叨完大致的理论知识后,从最简单的例子开始,来看看具体怎么使用Playable API来播放动画吧。测试模型使用的chan小姐姐,提供了非常丰富的Animation供我们使用。



chan小姐姐

我们保留小姐姐身上原本带有的Animator组件,因为前面说过Playable是依靠Animator驱动的。但是我们要去除掉关联的Animator Controller,换做使用Playable来控制动画,替代状态机。

播放单个动画

新建一个PlaySingleAnimation组件,挂载在小姐姐身上,我们先来让小姐姐播放一个最简单的待机动画。
前面我们说了,需要一个PlayableGraph来管理Playable以及PlayableOutput,因此我们要先创建一个PlayableGraph对象,方法如下:
PlayableGraph graph = PlayableGraph.Create("ChanPlayableGraph");
//利用SetTimeUpdateMode方法可以设置Graph的更新方式,例如
创建好PlayableGraph后,自然就要往里面添加Playable和PlayableOutput了。由于我们要做的是播放动画而不是播放音频等,因此我们需要的PlayableOutput为AnimationPlayableOutput,可以通过下面方法添加进PlayableGraph:
var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
AnimationPlayableOutput.Create方法传入的参数有PlayableGraph,自定义的节点名称以及Animator组件,这也是为什么前面不直接删除小姐姐身上Animator组件的原因。
PlayableOutput有了,接下来就要添加Playable节点了,由于我们目前只是播放单个动画,不涉及到动画混合与分层这些,因此选用最简单的AnimationClipPlayable即可,创建方法如下:
var idleClipPlayable = AnimationClipPlayable.Create(graph, idleClip);
AnimationClipPlayable.Create方法传入的参数有PlayableGraph以及该节点对应的AnimationClip文件。
接着我们要做连线操作,将Playable与PlayableOutput关联起来,方法如下:
animationOutputPlayable.SetSourcePlayable(idleClipPlayable);
可以简单理解为由OutputPlayable决定将哪个Playable作为输出播放。
设置好了输出的动画后,我们只需要利用PlayableGraph播放它即可,方法如下:
graph.Play();
这样运行后,我们的小姐姐就可以播放待机动画了,如下图:


Unity还提供了一个AnimationPlayableUtilities工具类,可以让我们用更加简短的代码使用Playable API,例如前面这些所有代码都可以使用下面一行来代替:
AnimationPlayableUtilities.PlayClip(GetComponent<Animator>(), idleClip, out PlayableGraph graph);可以发现整个操作都是由代码在运行时完成的,因此相比Animator状态机而言,更加的自由。我们可以直接控制每一个Playable的属性,例如播放速度(AnimationClipPlayable.SetSpeed),暂停动画(AnimationClipPlayable.Pause)等。
我们还可以控制更新频率,这在我们做动画的性能优化时比较常用,例如:那些距离Camera过远的角色我们就可以把动画的更新频率降低。通过PlayableGraph.SetTimeUpdateMode方法我们可以控制动画的更新频率,参数为DirectorUpdateMode枚举,有如下几种:
DSPClock基于DSP(Digital Sound Processing) clock的更新,用于与声音同步。GameTime基于Time.time的更新,当Time.timeScale = 0,动画也会暂停。UnscaledGameTime基于Time.unscaledTime的更新,当Time.timeScale = 0,动画也会继续播放。Manual手动更新,需要手动调用PlayableGraph.Evaluate方法来触发一次更新。注:若使用UnscaledGameTime,需要Animator组件中的Update Mode也设置为Unscaled Time。
若我们想要清空PlayableGraph内所有的节点(Playable和PlayableOutput),可以使用Destroy方法来完成:
graph.Destroy();
若要单独销毁某个Playable节点,可以使用DestroyPlayable方法,单独销毁某个PlayableOutput节点,可以使用DestroyOutput方法。

PlayableGraph Visualizer

可以通过下载官方的graph-visualizer插件,使得我们可以在Unity中利用可视化视图查看我们的PlayableGraph的内容。只需要下载好插件,然后拖入到我们的Unity工程中即可,待编译完成,可以通过 Window->PlayableGraph Visualizer 打开视图,上面的例子运行后的PlayableGraph显示如下:


左上角是我们自定义的PlayableGraph名称,中间就是我们创建的Playable节点,选中某个节点,可以在右边看见更多的详细信息,包括播放状态等等。

Blend Tree

在旧的Legacy动画系统中,是不支持Blend Tree功能的,在Playable系统中,Unity为我们提供了Blend Tree的支持,接下来让我们看看怎么实现它。
首先先了解下Blend Tree的作用,我们从一个简单的例子来理解,比如我们可以通过遥感来控制小姐姐的移动,当遥感的偏移较小时,代表移动速度较慢,我们期望播放走的动画,而当偏移较大,代表移动速度较快,我们期望播放跑的动画。这种需求,我们就可以使用Blend Tree来实现,除此之外,例如根据不同的前进方向切换向前跑,向左跑,向右跑这些动画,都可以使用Blend Tree来实现。下图是Animator状态机中的Blend Tree:

http://pic3.zhimg.com/v2-8f89a15f1e9b6632b9bc6a229b1b9586_r.jpg
我们新建一个PlayBlendTreeAnimation组件,和之前一样先创建好PlayableGraph以及AnimationPlayableOutput。和之前不同的是:在Playable中,我们需要使用AnimationMixerPlayable节点来实现Blend Tree,创建方法如下:
var mixerPlayable = AnimationMixerPlayable.Create(graph, 2);
AnimationMixerPlayable.Create方法中第二个参数,代表我们要将几个动画进行混合,示例里我们就简单的混合走和跑的动画,因此填写2。
接着我们创建两个AnimationClipPlayable,分别关联走和跑的动画:
var walkClipPlayable = AnimationClipPlayable.Create(graph, walkClip);
var runClipPlayable = AnimationClipPlayable.Create(graph, runClip);
然后我们要利用PlayableGraph.Connect方法将AnimationClipPlayable与AnimationMixerPlayable关联起来:
graph.Connect(walkClipPlayable, 0, mixerPlayable, 0);
graph.Connect(runClipPlayable, 0, mixerPlayable, 1);
可以看到参数中除了节点以外,还有些int的值,我们理解成端口号,Connect方法的完整描述如下:
public bool Connect<U, V>(U source, int sourceOutputPort, V destination, int destinationInputPort)
            where U : struct, IPlayable
            where V : struct, IPlayable;
简单来说,我们可以把参数destination当做是一排插座,参数source当做是一个个要插在该插座上的设备。哪个设备插在插座的哪个口子上由destinationInputPort参数决定,若我们的设备可能有不止一条电源线(通常情况下就是一条),那么sourceOutputPort可以决定设备上的哪条电源线插在插座上。
当然了插座的例子并不是很得当,因为生活中电流是从发电站(AnimationPlayableOutput)到家里的插座(AnimationMixerPlayable)再到每个设备(AnimationClipPlayable),而Playable系统中电流的方向正好相反,是从AnimationClipPlayable到AnimationMixerPlayable最后到AnimationPlayableOutput。
与Connect对应的还有Disconnect方法用于断开节点之间的连接关系,方法如下(就不过多介绍了):
public void Disconnect<U>(U input, int inputPort) where U : struct, IPlayable;
最后我们把AnimationMixerPlayable的输出作为输入连接到AnimationPlayableOutput上,并且播放我们的PlayableGraph:
animationOutputPlayable.SetSourcePlayable(mixerPlayable);
graph.Play();
运行后,结构图如下:


但是此时我们的小姐姐并不会跑起来,因为我们并没有一个速度(权重)来告诉AnimationMixerPlayable来播放哪个动画,因此我们还要设置两个AnimationClipPlayable相对应的权重,为了能够在运行时调整速度来调整动画,因此我们把设置权重的逻辑写在Update方法中:
public float speed;

void Update() {
    mixerPlayable.SetInputWeight(0, 1.0f - speed);
    mixerPlayable.SetInputWeight(1, speed);
}
通过AnimationMixerPlayable.SetInputWeight方法设置每个端口上对应Playable的权重。需要注意所有端口上的权重之和不能大于1,否则动画会出现问题。
这样我们就可以通过Speed参数来让小姐姐在走和跑直接任意的切换了:

http://pic4.zhimg.com/v2-04b1471ecb7a32253ad3895806dfa62b_r.jpg
其中碰见一个问题不太了解,希望有大佬可以告知一二~

Transitions

其实上面的例子就为我们展示了从走到跑的过渡过程(Transitions),因此Playable中并没有像Animator中Transitions的功能:



Animator中的Transitions

因为Transition本质是二个动画的Blend:将前一个动画的权重从1降到0,后一个动画的权重从0升到1。这样我们可以更加自由的控制两个动画的过渡,除了常用的线性(平滑)过渡以外,我们还可以通过控制过渡时的曲线使得过程更加圆滑。而对于前后二个动画差异较大的情况,我们可以使用冻结过渡,即动画A过渡到动画B时,A就不在更新了,而动画B在更新的同时Weight由0变为1。
Playable给我们带来了更佳灵活与自由的控制方式,而在Animator中想要精确的控制Transition却比较困难。如果想要实现服务器和客户端动画的同步问题。用Animator来做的话,是很难做到完美的,因为Transition你在外部是不能直接控制的。而用Playable的话,我们只需要同步二个动画的ID,以及它们的权重和Transition的持续时间就可以了。

Animation Layers

使用分层动画可以根据身体的不同部位实现复杂的动画组合,例如在FPS游戏中,角色往往会拥有跑步动画和射击动画,然后利用分层的功能我们就可以实现边跑边射击的动画效果(下半身播放跑步动画,上半身播放射击动画)。
Playable API为我们提供了AnimationLayerMixerPlayable节点来实现动画分层的效果,并且可以在运行时动态的增加、删除Layer,它与前面提到的AnimationMixerPlayable的使用方法基本类似,我们新建一个PlayLayerAnimation组件,代码如下:
void Start()
{
    PlayableGraph graph= PlayableGraph.Create("ChanPlayableGraph");
    var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
    var layerMixerPlayable = AnimationLayerMixerPlayable.Create(graph, 2);
    var runClipPlayable = AnimationClipPlayable.Create(graph, runClip);
    var eyeCloseClipPlayable = AnimationClipPlayable.Create(graph, eyeCloseClip);
    graph.Connect(runClipPlayable, 0, layerMixerPlayable, 0);//第一层Layer
    graph.Connect(eyeCloseClipPlayable, 0, layerMixerPlayable, 1);//第二层Layer
    animationOutputPlayable.SetSourcePlayable(layerMixerPlayable);
    layerMixerPlayable.SetLayerMaskFromAvatarMask(1, faceAvatarMask);
    layerMixerPlayable.SetInputWeight(0, 1);
    layerMixerPlayable.SetInputWeight(1, 0.5f);
    graph.Play();
}
该例子中身体动作为一个Layer,面部动作为一个Layer,利用SetLayerMaskFromAvatarMask方法为面部的Layer设置了一个AvatarMask,它可以帮我们屏蔽身体的其他部位。通过上面代码,我们可以实现小姐姐半闭眼的跑步动画:



死鱼眼,哈哈

PlayableGraph结构如下:


可以发现和AnimationMixerPlayable不同的是,在AnimationMixerPlayable中所有混合的动画其权重之和要在0-1之间,而在AnimationLayerMixerPlayable中每层动画的权重都可以在0-1之间。
注:其中PlayableGraph界面中,每条线的明暗程度代表对应节点的权重值,越白说明权重越高。
此外我们还可以利用AnimationLayerMixerPlayable.SetLayerAdditive方法将某层动画指定为Additive Layer,这样改成动画会与上一层动画发生混合。默认情况下,新生成的Layer都不是Additive Layer,而是继承(override)自上一层动画,意味着来自其他层的信息会被忽略。

Playable + AnimatorController

Playable API中为我们提供了AnimatorControllerPlayable来实现Playable与Animator Controller的混合使用。
例如在Animator状态机中Layer是Static的,所以利用Playable和Animator controller混合就可以在运行时起到动态添加Layer的效果。只需要把AnimatorControllerPlayable作为一个输入节点连接到我们的AnimationLayerMixerPlayable上。
并且我们可以让Playable和Controller按一定的权重进行Blend,即把AnimatorControllerPlayable作为一个输入节点连接到我们的AnimationMixerPlayable上。
举个例子,我们修改一下小姐姐原本有的状态机,只播放跑的动画,并且删除Face层,如下:


然后我们新建一个PlayableAddAnimatorController组件,代码如下:
void Start() {
    m_graph = PlayableGraph.Create("ChanPlayableGraph");

    var animationOutputPlayable = AnimationPlayableOutput.Create(m_graph, "AnimationOutput", GetComponent<Animator>());
    //blend 三个动画,想左跑、向右跑、以及AnimatorController里的向前跑
    m_mixerPlayable = AnimationMixerPlayable.Create(m_graph, 3);
    //根据AnimatorController创建AnimatorControllerPlayable
    var controllerPlayable = AnimatorControllerPlayable.Create(m_graph, animatorController);
    var runLeftClipPlayable = AnimationClipPlayable.Create(m_graph, runLeftClip);
    var runRightClipPlayable = AnimationClipPlayable.Create(m_graph, runRightClip);
    m_graph.Connect(runLeftClipPlayable, 0, m_mixerPlayable, 0);
    m_graph.Connect(controllerPlayable, 0, m_mixerPlayable, 1);
    m_graph.Connect(runRightClipPlayable, 0, m_mixerPlayable, 2);

    //动作和面部表情分层
    m_layerMixerPlayable = AnimationLayerMixerPlayable.Create(m_graph, 2);      
    var eyeCloseClipPlayable = AnimationClipPlayable.Create(m_graph, eyeCloseClip);
    m_graph.Connect(m_mixerPlayable, 0, m_layerMixerPlayable, 0);
    m_graph.Connect(eyeCloseClipPlayable, 0, m_layerMixerPlayable, 1);
    m_layerMixerPlayable.SetLayerMaskFromAvatarMask(1, faceAvatarMask);
    m_layerMixerPlayable.SetInputWeight(0, 1);

    animationOutputPlayable.SetSourcePlayable(m_layerMixerPlayable);
    m_graph.Play();
}

void Update() {
    //Blend的权重
    float leftWeight = directWeight < 0 ? -directWeight : 0;
    float rightWeight = directWeight > 0 ? directWeight : 0;
    float forwardWeight = 1 - leftWeight - rightWeight;
    m_mixerPlayable.SetInputWeight(0, leftWeight);
    m_mixerPlayable.SetInputWeight(1, forwardWeight);
    m_mixerPlayable.SetInputWeight(2, rightWeight);

    //面部动作的权重
    m_layerMixerPlayable.SetInputWeight(1, faceWeight);
}
得到的效果为:

http://pic3.zhimg.com/v2-6f50d7c4f96040198f32a468c55e2202_r.jpg
PlayableGraph的结构如下:


其中Animator Controller节点后的子树,代表的就是整个状态机的结构。
除此以外,Playable还可以和Controller互相CrossFade。例如一些动作游戏有不同的武器(近战或者远程),不同的武器攻击触发方式和动画可能都不相同,若使用Animator我们可能就需要把每种武器的效果都放在一个状态机里。但是如果使用Playable加上AnimatorController的方式,我们就可以在每种武器身上关联各自的Animator Controller,当角色拿起武器后,就可以利用Playable动态的去CrossFade到当前武器的动画状态机上。简单来说就是我们可以动态的增加或删除Playable中Animator Controller节点,并且设置其权重等。这可以让大大降低我们的动画系统的复杂度,因为动画的CrossFade不在局限于一个状态机里了。
最后,二个Controller也可以进行混合,例如:我们可以从一个状态机CrossFade到另一个状态机上。
以上就是官方提供的几个Playable节点的简单使用,Audio Playable虽然没有介绍,但是用法基本相似。有关更高阶的Playable用法,例如PlayableBehaviour,后续在介绍~

xiangtingsl 发表于 2023-2-26 15:17

不错呦 关注了

kirin77 发表于 2023-2-26 15:17

很好的宝藏文章

Zephus 发表于 2023-2-26 15:22

赞了

DungDaj 发表于 2023-2-26 15:27

nbnb

Ylisar 发表于 2023-2-26 15:35

请问这个资源是您自己的资源吗,还是官方用例

KaaPexei 发表于 2023-2-26 15:38

asset store里搜chan

Ylisar 发表于 2023-2-26 15:38

值得我细细品味
页: [1]
查看完整版本: 【Unity】简单使用Playable API控制动画