找回密码
 立即注册
查看: 322|回复: 2

[简易教程] 使用Playable API定制自己的动画系统

[复制链接]
发表于 2021-12-3 13:29 | 显示全部楼层 |阅读模式
进阶教程
Playable API是Unity官方提供的一种新的创建工具、动画系统或其他游戏机制的方式。Playable可以通过一组API来创建一个Graph,而每个Graph可以由多个树形结构组成,每个树状结构都由一个Output节点作为根节点,叶子结点则由各种Playable组成。
详细的介绍大家可以参考官方文档。
下面直接介绍怎么利用Playable API来定制自己的动画系统,其中参考了Unity官方案例SimpleAnimation。GitHub工程地址:https://github.com/Unity-Technologies/SimpleAnimation
播放一段简单的动画

首先自定义一个CustomAnimationcontroller组件,然后需要创建一个PlayableGraph对象以及一个AnimationPlayableOutput节点。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

[RequireComponent(typeof(Animator))]
public class CustomAnimationController : MonoBehaviour
{
    private PlayableGraph m_Graph;
    private AnimationPlayableOutput m_Output;

    void Start()
    {

    }

   void Update()
    {
        
    }

    private void OnDestroy()
    {

    }
}要播放一段动画,我们还必须要创建一个AnimationClip引用。然后,我们就可以构建一个动画树了。首先调用PlayableGraph.Create()方法构建一个PlayableGraph实例,并设置实例的时间模式为DirectorUpdateMode.GameTime。这样,PlayableGraph的时间更新是基于Time.time的,这样可以保证PlayableGraph的时间与游戏的时间保持同步。然后调用AnimationPlayableOutput.Create()函数创建一个Output节点,这里需要传递三个参数,分别是PlayableGraph实例,动画树的名字以及游戏对象绑定的Animator组件。
Playable API的两大组成部分是PlayableOutput和Playable,我们的动画对象也需要包装到一个Playable对象中,通过调用AnimationClipPlayable.Create()方法可以返回我们需要的Playable对象,传递的参数是Playable实例以及动画片段。
然后将动画片段的Playable实例作为输出结果传递给output节点,我们的动画树就创建好了。最后调用PlayableGraph实例的Play()方法即可让动画树开始播放。
public AnimationClip clip;
...
void Start()
{
    m_Graph = PlayableGraph.Create();
    m_Graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
    m_Output = AnimationPlayableOutput.Create(m_Graph, "Animation", GetComponent<Animator>());
    AnimationClipPlayable clipPlayable = AnimationClipPlayable.Create(m_Graph, clip);
    m_Output.SetSourcePlayable(clipPlayable);
    m_Graph.Play();
}另外,游戏对象销毁时,我们还要记得销毁PlayableGraph实例,即m_Graph。所以,我们需要在OnDestroy中调用m_Graph的Destroy方法。
PlayableGraph可视化

为了让大家对PlayableGraph可以有个更加直观的认识,这里介绍一个Unity的PlayableGraph Visualizer插件。该工具由于尚未成熟,所以在Unity的PackageManager界面的默认选项中并没有提供下载途径。
正确的下载方式是:

  • 点击菜单选项:Window > Package Manager;
  • 点击Advanted按钮,在下拉列表中选择show preview packages,然后在搜索栏中输入playable;



  • 找到PlayableGraph Visualizer选项然后点击右下角的install按钮;


安装完毕,点击window > analysis > playablegraph即可打开这个插件的显示界面。
如何让PlayableGraph实例显示在这个插件界面上,需要调用GraphVisualizerClient.Show()这个接口,需要传递一个PlayableGraph实例作为输入参数。为了让动画树显示更加直观,这里稍微修改以下代码:
    void Start()
    {
        m_Graph = PlayableGraph.Create("CustomAnimationController");
        GraphVisualizerClient.Show(m_Graph);
        ...
    }点击unity的运行按钮,然后打开我们的插件,这时候可以看到下面的结果,左上角的名字即是我们的playable graph实例的名字。


创建一个动画混合树

我们知道,unity的动画状态机具有transition的功能,两个state之间通过拉一条线段来定义一个transition。Playable API没有transition的概念,但是提供给来应用层基本的Blend功能,而transition的本质是两个动画的Blend。因此,我们可以应用Playable API来实现自己的transition。
创建混合树我们需要用到AnimationMixerPlayable,以及至少两个动画。因此,我们新加一个AnimaitonMixerPlayable对象和一个AnimationClip引用。
    public AnimationClip clip1;
    private PlayableGraph m_Graph;
    private AnimationPlayableOutput m_Output;
    private AnimationMixerPlayable m_Mixer;将两个动画进行混合,其实是由两个动画的input weight来控制的。我们在Start函数中创建AnimationMixerPlayable的实例,创建方法是调用AnimationMixerPlayable的Create函数,需要传递两个参数,分别是PlayableGraph实例以及AnimationMixerPlayable节点的输入节点数量。AnimationMixerPlayable实例创建好之后,我们将它作为输出结果传输给output节点。接下来利用m_Mixer连接两个动画片段,构建完整的动画混合树。
        ...
        m_Mixer = AnimationMixerPlayable.Create(m_Graph, 2);
        m_Output.SetSourcePlayable(m_Mixer);
        AnimationClipPlayable clipPlayable0 = AnimationClipPlayable.Create(m_Graph, clip0);
        AnimationClipPlayable clipPlayable1 = AnimationClipPlayable.Create(m_Graph, clip1);
        m_Graph.Connect(clipPlayable0, 0, m_Mixer, 0);
        m_Graph.Connect(clipPlayable1, 0, m_Mixer, 1);
        m_Mixer.SetInputWeight(0, 1);
        m_Mixer.SetInputWeight(1, 0);
        ...实现两个动画的线性过渡,首先定义一个字段tranTime控制动画的过渡时间,deltaTime表示剩余的过渡时间,然后在Update函数中根据时间修改两个动画的input weight。
    public float tranTime = 2;
    private float leftTime;
    ...
    void Start()
    {
        leftTime = tranTime;
        ...
    }
    void Update()
    {
        if (leftTime > 0)
        {
            leftTime = Mathf.Clamp(leftTime - Time.deltaTime, 0, 1);
            float weight = leftTime / tranTime;
            m_Mixer.SetInputWeight(0, weight);
            m_Mixer.SetInputWeight(1, 1 - weight);
        }
    }

创建PlayableBehaviour

PlayableBehaviour是一个供用户自定义行为的类。我们可以对Playable进行直接的访问和控制。同时它也定义了一些回调函数来捕捉一些事件。例如:开始播放时的事件、销毁事件,我们想监控这些事件时就离不开PlayableBehaviour这个类。
更重要的是,它提供了一些在每一帧的动画计算流程上的回调。例如:PrepareFrame函数会在Prepare frame这个阶段回调。我们就可以在每一帧对Playable中的元素进行访问和设置,例如:Time和Weight。可以说这是在做自定义Blend中不可缺失的功能。
接下来,我们将实现在自定义的PlayableBehaviour类中控制所有在PlayableGraph中的节点。新建一个CustomAnimationControllerPlayable类,并继承自PlayableBehabiour。
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

public class CustomAnimationControllerPlayable : PlayableBehaviour
{
    override public void PrepareFrame(Playable owner, FrameData info)
    {
        
    }
}首先我们重新组织一下代码的功能,我们将把构建动画树的工作移植到CustomAnimationControllerPlayable中来。为此,我们新建一个Initialize函数,输入参数有构建动画树的动画数组,CustomAnimationControllerPlayable从属的Playable以及PlayableGraph实例。新建三个私有属性,分别表示当前播放的动画的指引,距离下个动画剩余的时间以及AnimationMixerPlayable对象,该对象将作为从属Playable的输入节点。

    private int m_CurrentClipIndex = -1;
    private float m_TimeToNextClip;
    private Playable mixer;
    public void Initialize(AnimationClip[] clipsToPlay, Playable owner, PlayableGraph graph)
    {
        owner.SetInputCount(1);
        mixer = AnimationMixerPlayable.Create(graph, clipsToPlay.Length);
        graph.Connect(mixer, 0, owner, 0);
        owner.SetInputWeight(0, 1);
        for (int clipIndex = 0 ; clipIndex < mixer.GetInputCount() ; ++clipIndex)
        {
            graph.Connect(AnimationClipPlayable.Create(graph, clipsToPlay[clipIndex]), 0, mixer, clipIndex);
            mixer.SetInputWeight(clipIndex, 1.0f);
        }
    }
    ...重写PrepareFrame函数,完成的工作是顺序播放所有的动画,在播放完一个动画之后自动播放在一个动画,当播放完所有动画之后又回重复从第一个动画开始播放。
    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);
            // Reset the time so that the next clip starts at the correct position
            currentClip.SetTime(0);
            m_TimeToNextClip = currentClip.GetAnimationClip().length;
        }
        // Adjust the weight of the inputs
        for (int clipIndex = 0 ; clipIndex < mixer.GetInputCount(); ++clipIndex)
        {
            if (clipIndex == m_CurrentClipIndex)
                mixer.SetInputWeight(clipIndex, 1.0f);
            else
                mixer.SetInputWeight(clipIndex, 0.0f);
        }
    }相应修改一下CustomAnimationControllor脚本,代码如下:
    public AnimationClip[] clipsToPlay;

    PlayableGraph m_Graph;

    void Start()

    {

        m_Graph = PlayableGraph.Create();

        var custPlayable = ScriptPlayable<CustomAnimationControllerPlayable>.Create(m_Graph);

        var playQueue = custPlayable.GetBehaviour();

        playQueue.Initialize(clipsToPlay, custPlayable, m_Graph);

        var playableOutput = AnimationPlayableOutput.Create(m_Graph, "Animation", GetComponent<Animator>());

        playableOutput.SetSourcePlayable(custPlayable);
        playableOutput.SetSourceInputPort(0);

        m_Graph.Play();

    }

    void OnDisable()

    {

        // Destroys all Playables and Outputs created by the graph.

        m_Graph.Destroy();

    }

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-12-3 13:29 | 显示全部楼层
没太看明白,大佬有更具体的文章介绍这个嘛?
发表于 2021-12-3 13:35 | 显示全部楼层
这个已经介绍的非常详细啦[思考]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-5 18:06 , Processed in 0.103321 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表