优化AnimatorController的拓展性及可维护性。

为什么要重构?

大量的If else参数判断逻辑以及动画过渡判断逻辑(指定了0、1、2三个端口,但本质是新旧两个端口0,1交替使用,之前我们通过额外引入的第三个端口来对单个动画和Blend动画作区分,现在我们希望通过重构封装到抽象节点内,对外保留两个端口)使得代码的可读性和可扩展性很差,Unity自带Playable节点的添加、修改、连接、混合很麻烦,为此我们首先用抽象节点进行封装。

抽象节点

在AnimationController里添加变量来重新标识新旧端口和节点。

    private AnimationNodeBase currentNode;
    private AnimationNodeBase previousNode;
    private int inputPort0 = 0;
    private int inputPort1 = 1;

1.建立AnimationNodeBase 含有inputPort(节点对应连接在Mixer上的端口号)

/// <summary>
/// 动画节点基类
/// </summary>
public abstract class AnimationNodeBase
{
    public int InputPort;
}

2.先重构播放单个动画的逻辑,在Animation Controller建立Play单个动画的函数,对应建立SingleAnimationNode继承AnimationNodeBase,内部封装了Playable的构建过程。

public class SingleAnimationNode : AnimationNodeBase
{
    private AnimationClipPlayable clipPlayable;
    public void Init(PlayableGraph graph,AnimationMixerPlayable outputMixer,AnimationClip animationClip,float speed, int inputPort)
    {
        clipPlayable = AnimationClipPlayable.Create(graph, animationClip);
        clipPlayable.SetSpeed(speed);
        InputPort = inputPort;
        graph.Connect(clipPlayable, 0, outputMixer, 0);
    }
}

3.考虑首次播放,先从对象池里获取一个SingleAnimation的实例,然后调用自身的Init方法进行初始化(Playable的创建,连接,速度设置等),设置好权重。

4.考虑无论是首次还是非首次播放都要执行的逻辑,要更改当currentNode的指向,更新全局speed中存储的值(我们使用Speed就是为了对所有动画进行快速speed设置,在Init中已经对动画的速度进行了设置,为了保持一致,我们要更新全局Speed中的值,但不用走属性在设置一遍动画速度,只是更新记录即可)然后播放当前动画。


    /// <summary>
    /// 播放单个动画
    /// </summary>
    public void PlaySingleAnimation(AnimationClip animationClip, float speed = 1, bool refreshAnimation = false, float transtionFixedTime = 0.25f)
    {
        SingleAnimationNode singleAnimationNode = null;
        if (currentNode == null) //首次播放
        {
            singleAnimationNode = PoolManager.Instance.GetObject<SingleAnimationNode>();
            singleAnimationNode.Init(graph, mixer, animationClip, speed, inputPort0);
            mixer.SetInputWeight(inputPort0, 1);


        }
        else
        {
            
        }

        this.speed = speed; //只需要把记录值更新一下即可,在Init时实际每个动画都已经设置好了速度,不用使用属性再赋值
        currentNode = singleAnimationNode;
        if (graph.IsPlaying() == false) graph.Play();


    }

5.考虑非首次播放

首先解决refresh刷新问题。通过强转为SingleMode判断当前节点是不是blend节点(是的话强转失败为null),只有在都为SingleMode单个动画点、refreshAnimation为false且当前动画和新播放的动画Clip一样的情况才不需要刷新直接return。

        else
        {
            SingleAnimationNode preNode = currentNode as SingleAnimationNode;
            if (preNode != null && !refreshAnimation && preNode.GetAnimationClip() == animationClip) return;

        }

抽象节点需要对外提供获取AnimationCLip的方法。

    public AnimationClip GetAnimationClip()
    {
        return clipPlayable.GetAnimationClip();
    }

销毁旧节点腾出端口(使用对象池回收)。这时新加的节点一定是放在新端口上即InputPort1上的,在当前节点还没有更新之前,用previousNode记录,一旦播放新动画后,原先的新节点就会变成旧节点。

			//else{}
            //销毁当前可能被占用的Node
            DestoryNode(previousNode);
            singleAnimationNode = PoolManager.Instance.GetObject<SingleAnimationNode>();
            singleAnimationNode.Init(graph, mixer, animationClip, speed, inputPort1);
            previousNode = currentNode;
            StartTransitionAnimation(transtionFixedTime);

如何保证InputPort1始终是新端口呢,在原来重构之前我们通过IsPlayable1来标识0号端口的新旧程度,实际上每次播放,当前端口就会由新->旧或者由旧->新,为此我们交换端口标识InputPort0和InputPort1所代表的实际端口号即可,放在动画过渡的协程里面做。

alt

这里有一点还要理解,每个抽象节点持有的InputPort代表实际其连接在Mixer上的哪一个部分,而InputPort0和InputPort1是用来标识新旧端口的,端口是谁不重要,只要有区分即可,所以抽象节点的InputPort一旦根据新旧端口指定后就不会变了,而InputPort0(old)和InputPort1(new)则会不断变化。

    /// <summary>
    /// 动画过渡
    /// </summary>
    /// <param name="fixedTime">过渡的时间间隔,越小过渡速度越快</param>
    /// <param name="currentIsClipPlayable1"></param>
    /// <returns></returns>
    private IEnumerator TransitionAnimation(float fixedTime)
    {
        // 交换端口号
        int temp = inputPort0;
        inputPort0 = inputPort1;
        inputPort1 = temp;
        //硬切判断
        if (fixedTime == 0)
        {
            mixer.SetInputWeight(inputPort1, 0);
            mixer.SetInputWeight(inputPort0, 1);

        }
        //当前的权重
        float currentWeight = 1;
        float speed = 1 / fixedTime;

        while (currentWeight > 0)
        {
            //yield return null; 不能放这
            currentWeight = Mathf.Clamp01(currentWeight - Time.deltaTime * speed);
            mixer.SetInputWeight(inputPort1, currentWeight); // 减少
            mixer.SetInputWeight(inputPort0, 1 - currentWeight); //增加
            yield return null;
        }

        transitionCoroutine = null;
    }

现在参数也不用传端口号了,因为已经固定了新旧端口,指定InputPort1和InputPort0即可。

    public float Speed
    {
        get => speed;
        set
        {
            currentNode.SetSpeed(value);
        }
    }

对Speed属性也进行优化,原来通过Speed更改全局动画速度,现在CLipPlayable被包在了抽象节点里面,为此我们交由每个抽象节点子类自己去设置,对象池的回收也是如此。

        if (Input.GetKey(KeyCode.T))
        {
            player.PlayAnimation("Walk");
        }
        else
        {
            player.PlayAnimation("Idle");
        }

目前但个动画的播放已经重构完毕,主要对端口号进行了精简,固定了新旧两个端口,同时也对ClipPlayable的创建,回收进行了一层抽象节点的封装,为了方便测试,在Idle_State下进行两个动画的切换。下一节将对Blend动画进行重构。

alt

如图,每次切换动画后,旧的Playable权重降为0,并被保存在previousNode中,下一次切换时被移除,原来的currentNode编程previousNode,新播放的动画变成currentNode。