我上一遍文章里提到了利用装饰思想来写一个协程帮助类,来实现动画组合的解耦。然后这个类由于直接使用了IEnumerator,会导致一个BUG就是传入参数不能随着动画结果更新,结果是动画协程执行的参数并不是我们期望的执行到该点的值,而是一开始生成协程链参数的初始值。解决思路就是使用委托,通过封装委托,来保证动画协程的生成一定是执行到该点后才生成。除此之外,Iem调用链条也很不直观,因此考虑使用运算符重载是实现协程链的组织。
首先,我们需要一个unity的协程帮助类。因为unity的协程以及startCoroutine都需要MonoBehaviour支持的,但是生成我们的链条时,不可能生成无数个gameobject来挂接,因此使用单例模式,实现一个IEMHelper帮助类来实际实现协程的逻辑组织。而另外一个IEM类,则纯粹是委托数据的封装,通过重载运算符来实现链条组织。
第一步,需要实现动画协程函数的封装。考虑到动画协程参数的多样性,通过使用模板方法来实现这一点。首先定义一个接口基类:
public abstract class IEM
{
abstract public IEnumerator ITERATOR
{
get;
}
public Coroutine END()
{
return IEMHelper.Instance._END(this);
}
}
这个类就两个功能,一个是获得当前迭代器,另一个是通过iemHELper 执行协程链条。然后利用c#的func模板来封装各种不同参数的迭代器委托:
public class FunITR : IEM
{
Func<IEnumerator> fun = null;
public FunITR(Func<IEnumerator> f)
{
fun = f;
}
public override IEnumerator ITERATOR
{
get
{
return fun();
}
}
}
当然类似还有FunITR<T>,FunITR<T,S>等。取决于自己的迭代器函数有几个参数。这样,就完成了IEM对迭代器函数的数据封装。
第二步就是重载操作符,来实现不同逻辑的链条组合。比如,定义+运算部的意义为顺序执行。A+B的意思是先执行A,在执行B;-运算符的意义为反序执行,A-B的意思是在执行A之前,先执行B。*表示并行运行的概念,即A和B同时运行。例如:
public static IEM operator +(IEM iem1,IEM iem2)
{
return IEM.ITR<IEM, IEM>(IEMHelper.Instance._NEXT, iem1, iem2);
}
注意这里是使用iemHelper的辅助函数来实际帮助实现链条的逻辑。IEMHelper大致像这个样子:
public class IEMHelper : Singleton<IEMHelper>
{
public IEnumerator _NEXT(IEM first, IEM next)
{
yield return StartCoroutine(first.ITERATOR);
yield return next.ITERATOR;
}
}
注意这里的Singleton模板要从MonoBehaviour继承,并实现生一个Gameobject然后挂接脚本的功能,不然startCoroutine不会生效。
由于能重载的操作符只有二元运算符,所以涉及条件分支的逻辑只能用函数来实现。比如,定义static IEM IF(BFUN bf, IEM t, IEM f)的函数来实现分支。其中BFUN是一个返回bool值的委托封装。具体实现可参考IEM如何通过模板方法封装迭代器函数。
考虑到运算符重载可以针对不同的参数类型进行重载。可以进一步实现更多的功能。比如可以定义:IEM+float 表示动画执行后暂停多少秒,IEM-float 表示该动画执行前,需要等待多少秒等等。通过Action<>模板还可以进一步封装纯逻辑函数,将不含动画执行的函数代码也纳入链条。如果自己利用c#多播委托写一个逻辑事件的集中管理类,还可以进一步封装事件分发的代码,更可以自由控制实现引发动画的执行的并发、先后顺序等。
至此,可以自由利用运算符以及运算规则来自由组织我们的动画协程顺序逻辑了。比如写一个棋子交换逻辑:
IEM d = IEM.ITR<Chess>(g.MoveTo, l) * IEM.ITR<Chess>(l.MoveTo, g)
+ IEM.IF(CheckSwitching, switchElimeta, switchback);
StartCoroutine(d.ITERATOR);
上面伪代码链条 d的定义是,交换g和l两个棋子,位置移动动画完毕后,执行checkSwitching函数判断是否可以消除,如果判断结果为真,则执行switchElimeta消除的动画链条,否则,执行switchBack将棋子交换回来的动画链条。显然,可以随意定义多个链条进行随意组合,最后,一次使用startCoroutine执行最终链条即可。
这个系统的优势在于,可以自由组织动画顺序逻辑,并且可以进一步在这个基础上实现动画逻辑组织的脚本化。