优化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所代表的实际端口号即可,放在动画过渡的协程里面做。
这里有一点还要理解,每个抽象节点持有的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动画进行重构。
如图,每次切换动画后,旧的Playable权重降为0,并被保存在previousNode中,下一次切换时被移除,原来的currentNode编程previousNode,新播放的动画变成currentNode。