有啥好处就不空谈了,直接上干货

架构

Playable有很多类型,但是总体上,由两部分组成

  • Playable
  • PlayableOutput

从名字就看得出来,他两是对应的,一个是输入,一个是输出

Playable

作为输入节点,一共支持如下类型

image-20230414214350642

PlayableOutput

作为输出,类型就少很多了,因为从上面的输入来看,我们也只有三种类型的输出

image-20230414214454013

PlayableGraph

输入输出管理器,可以看做是一个管理类,管理我们所有的输入(Playable)和输出(PlayableOutput)

如果使用可视化插件,我们可以发现,一个简单的Graph如下(一个输入,一个输出)

image-20230414214658265

当然,Graph通常是不止两个节点的

image-20230414214734466

简单使用

一个模型,挂在了Animator,没用Animation也没有AnimatorController

image-20230414215028761

image-20230414215021717

public class PlayableTest : MonoBehaviour
{
    public AnimationClip clip;
    private PlayableGraph graph;

    void Start()
    {
        // 创建管理器
        graph = PlayableGraph.Create("Test");
        // 创建一个AnimationClip输入
        var animationClipPlayable = AnimationClipPlayable.Create(graph, clip);
        // 创建Animation输出
        var animationPlayableOutput = AnimationPlayableOutput.Create(graph, "OutPut", GetComponent<Animator>());
        // 连接输入输出
        animationPlayableOutput.SetSourcePlayable(animationClipPlayable);
        // 播放
        graph.Play();
    }
}

当然这么做可能有一点麻烦,官方提供了一个简单的工具类,可以直接播放一个方法

AnimationPlayableUtilities.PlayClip(GetComponent<Animator>(), clip, out PlayableGraph graph);

Graph是可以设置更新频率的

通过方法PlayableGraph.SetTimeUpdateMode

  • DSPClock 基于DSP,与声音同步
  • GameTime 基于Time.time,如果timeScale是0,那么也会停止
  • UnscaledGameTime 同上,不过不会停止
  • Manual 手动更新,需要自己调用PlayableGraph.Evaluate()

PlayableGraph Visualizer

一个可视化的插件,很好用,Add From Git Url : com.unity.playablegraph-visualizer

然后就可以在Window里打开了

image-20230414221059224

BlendTree

混合树,在状态机里可以很方便的使用,可以通过AnimationMixerPlayable实现

public class PlayableTest : MonoBehaviour
{
    public AnimationClip walk;
    public AnimationClip run;

    [Range(0,1)]
    public float weight;
    
    private PlayableGraph graph;
    private AnimationMixerPlayable mixerPlayable;
    
    void Start()
    {
        // 创建管理器
        graph = PlayableGraph.Create("Test");
        // 走路 输入
        var walkPlayable = AnimationClipPlayable.Create(graph, walk);
        // 跑步输入
        var runPlayable = AnimationClipPlayable.Create(graph, run);
        // 混合输入,目前需要的input是2
        mixerPlayable = AnimationMixerPlayable.Create(graph, 2);

        // 将2个Clip输入连接到混合节点
        graph.Connect(walkPlayable, 0, mixerPlayable, 0);
        graph.Connect(runPlayable, 0, mixerPlayable, 1);
        
        // 创建Animation输出
        var animationPlayableOutput = AnimationPlayableOutput.Create(graph, "OutPut", GetComponent<Animator>());
        // 连接输入,这时候连接的是混合节点
        animationPlayableOutput.SetSourcePlayable(mixerPlayable);
        // 播放
        graph.Play();
    }

    void Update()
    {
        mixerPlayable.SetInputWeight(0, 1 - weight);
        mixerPlayable.SetInputWeight(1, weight);
    }
}

Graph.Connect这个API,稍微有一点抽象

第一个参数是连接对象,第三个参数是连接目标,这两个参数不难理解,就是你想把哪两个Playable连起来

第二个参数,指的是连接对象的输出端口

第四个参数,指的是连接目标的输入端口

也就是说,这个API的作用是,把连接对象的某一个输出端口,连接到目标的某一个输入端口上

Animation Layers

首先我们创建一个骨骼遮罩,只需要露个头

image-20230414224244639

public class PlayableTest : MonoBehaviour
{
    public AnimationClip eyeClose;
    public AnimationClip run;
    // 遮罩
    public AvatarMask faceOnly;
    
    [Range(0,1)]
    public float runWeight;
    [Range(0,1)]
    public float simleWeight;
    
    private PlayableGraph graph;

    private AnimationLayerMixerPlayable animationLayerMixerPlayable;
    
    void Start()
    {
        graph= PlayableGraph.Create("ChanPlayableGraph");
        // 两个动画输入
        var runClipPlayable = AnimationClipPlayable.Create(graph, run);
        var eyeCloseClipPlayable = AnimationClipPlayable.Create(graph, eyeClose);
        // Layer混合
        animationLayerMixerPlayable = AnimationLayerMixerPlayable.Create(graph, 2);

        graph.Connect(runClipPlayable, 0, animationLayerMixerPlayable, 0);//第0层Layer
        graph.Connect(eyeCloseClipPlayable, 0, animationLayerMixerPlayable, 1);//第1层Layer
		
        // 设置遮罩,这是给eyeClose设置的,目的是让这个动画只会影响头部
        animationLayerMixerPlayable.SetLayerMaskFromAvatarMask(1, faceOnly);
        
        // 输出
        var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
        animationOutputPlayable.SetSourcePlayable(animationLayerMixerPlayable);
        graph.Play();
    }

    void Update()
    {
        animationLayerMixerPlayable.SetInputWeight(0, runWeight);
        animationLayerMixerPlayable.SetInputWeight(1, simleWeight);
    }
}

刚开始因为两个Layer的权重都是0,所以啥动画也没有

Animation Controller

Playable同样支持和状态机混用

我们给模型一个状态机,并加入一个跑步的初始动画

image-20230414230253222

public class PlayableTest : MonoBehaviour
{
    public AnimatorController animatorController;
    public AnimationClip runL;
    public AnimationClip runR;

    [Range(-1, 1)] public float directWeight;
    
    private PlayableGraph graph;

    private AnimationMixerPlayable animationMixerPlayable;
    
    void Start()
    {
        graph= PlayableGraph.Create("ChanPlayableGraph");
        // 2输入
        var runL_Playable = AnimationClipPlayable.Create(graph, runL);
        var runR_Playable = AnimationClipPlayable.Create(graph, runR);
        // 1状态机输入
        var animatorControllerPlayable = AnimatorControllerPlayable.Create(graph, animatorController);
        
        // 混合节点
        animationMixerPlayable = AnimationMixerPlayable.Create(graph, 3);
        
        // 连接
        graph.Connect(runL_Playable, 0, animationMixerPlayable, 0);
        graph.Connect(animatorControllerPlayable, 0, animationMixerPlayable, 1);
        graph.Connect(runR_Playable, 0, animationMixerPlayable, 2);

        
        var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
        animationOutputPlayable.SetSourcePlayable(animationMixerPlayable);

        graph.Play();
    }

    void Update()
    {
        var LWeight = directWeight < 0 ? -directWeight : 0;
        var RWeight = directWeight > 0 ? directWeight : 0;
        var runWeight = 1 - LWeight - RWeight;
        
        animationMixerPlayable.SetInputWeight(0, LWeight);
        animationMixerPlayable.SetInputWeight(1, runWeight);
        animationMixerPlayable.SetInputWeight(2, RWeight);
    }
}

进阶一点,还可以自己混合状态机

public class Test : MonoBehaviour
{
    public AnimatorController controller;
    public AnimationClip clip;
    public float fade;

    private PlayableGraph graph;
    private AnimationMixerPlayable animationMixerPlayable;

    private void Awake()
    {
        graph = PlayableGraph.Create("ChanPlayableGraph");
        graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
        
        // 1状态机输入
        var animatorControllerPlayable = AnimatorControllerPlayable.Create(graph, controller);
        
        // 混合节点
        animationMixerPlayable = AnimationMixerPlayable.Create(graph, 2);
        
        // 连接
        graph.Connect(animatorControllerPlayable, 0, animationMixerPlayable, 0);
        
        
        var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
        animationOutputPlayable.SetSourcePlayable(animationMixerPlayable);
        
        animationMixerPlayable.SetInputWeight(0, 1);
        graph.Play();
    }


    private async void PlayAnimation(AnimationClip clip, float fadeTime = 0.25f)
    {
        if (clip.length < fadeTime) 
            return;
        
        // 创建Clip
        var animationClipPlayable = AnimationClipPlayable.Create(graph, clip);
        graph.Connect(animationClipPlayable, 0, animationMixerPlayable, 1);

        // 淡入
        var duration = 0f;
        while (duration < fadeTime)
        {
            duration += Time.deltaTime;
            if (duration > fadeTime)
                duration = fadeTime;
            var lerp = duration / fadeTime;
            animationMixerPlayable.SetInputWeight(0, Mathf.Max(1 - lerp, 0));
            animationMixerPlayable.SetInputWeight(1, lerp);
            await UniTask.Yield();
        }

        // 减去淡入淡出时间后是否还有持续时间
        var remainingTime = clip.length - 2 * fadeTime;
        if (remainingTime > 0) 
            await UniTask.Delay((int)(remainingTime * 1000f));
        
        // 淡出时间
        var fadeOutTime = Mathf.Min(clip.length - fadeTime, fadeTime);
        duration = 0f;
        while (duration < fadeOutTime) 
        {
            duration += Time.deltaTime;
            if (duration > fadeOutTime)
                duration = fadeOutTime;
            var lerp = duration / fadeOutTime;
            animationMixerPlayable.SetInputWeight(0, lerp);
            animationMixerPlayable.SetInputWeight(1, Mathf.Max(1 - lerp, 0));
            await UniTask.Yield();
        }
        
        animationMixerPlayable.SetInputWeight(0, 1);
        graph.Disconnect(animationMixerPlayable, 1);
    }


    void Update()
    {
		graph.Evaluate(Time.deltaTime);
    }
}

这样就可以使用PlayAnimation(clip, fade);播放动画了