协程(Coroutine)是Unity项目开发中经常使用的一个特性,其可以让代码以分时的方式执行,这样可以避免某帧中的复杂操作阻塞当前帧而导致给用户带来不流畅的体验。协程其不是C#的语言特性,但在实现中用到了C#中的迭代器等特性,因而如果能对实现机制其有更深入的理解的话也可以使得对协程的使用更加自如。
1. C#中 IEnumerator,IEnumerable
迭代器(IEnumerator)提供了一种可迭代对象(IEnumerable)的无差别访问接口,其主要有三个接口:public object Current {get;}
public bool MoveNext()
public void Reset()
实现了上述三个接口后,再配合IEnumerable对象(需要实现IEnumerator GetEnumerator()的接口),就可以实现
foreach(Object obj in IEnumerableObject){
// Some operations
}
像这样的遍历访问,很是方便。
2. 通过迭代器块便捷地实现迭代器
通过代码实现迭代器的话需要实现三个方法,较为麻烦,因而在C#中提供了一种通过迭代器块快速实现迭代器的方法。yield所在的代码段其实不同于普通的代码,C#编译器遇到yield之后会做特殊处理,然后将其编译为具有迭代器逻辑类型的代码。比如:
public class IEnumeratorObject : IEnumerator{}public void Func1(){ // Some codes}public void Func2(){ // Some codes}public void FuncN(){ // Some codes}IEnumerator TestCode(){ Func1(); yield return new IEnumeratorObject(); Func2(); yield return new IEnumeratorObject(); //... FuncN(); yield return new IEnumeratorObject();}public class CompliedIEnumeratorObject : IEnumerator{ // execute counter int mExecuteCounter = 1; public object Current { get { return null; } } public bool MoveNext() { if (mExecuteCounter == 1) { Func1(); mExecuteCounter++; return true; } else if (mExecuteCounter == 2) { Func2(); mExecuteCounter++; return true; } //... else if (mExecuteCounter == N) { FuncN(); mExecuteCounter++; return true; } return false; } public void Reset() { }}
3. Unity中基于迭代器的Coroutine
在C#中来看的话迭代器提供了可迭代对象的访问接口,然后除此之外看起来并没有更多的用处。但是在Unity中将该特性做了更多的利用,很好的使用了迭代器来实现了复杂代码的分片执行。比如一段较为复杂的操作,里边含有了较多的逻辑与运算:public void ComplexFunc(){ int counter = 0; while (counter < 10) { // Some computation may cost 5 ms Func(); }}比如其中的Func可能会耗时5ms,整个ComplexFunc就会消耗50ms,如果在游戏的一帧中完成上述操作的话必定会造成当前帧的卡顿,带来不好的游戏体验。如果该操作不是必需要阻塞执行的话,那么一个更好的方法是将ComplexFunc中的操作分散开来执行,比如在每一帧中我们执行一次Func,然后整个操作会在5帧之后完成,这样的话游戏就会对玩家有更流畅的表现。当然,为了达到此目的,我们可以添加计数器,然后在每个Update中调用对应的Func直到对应的计算结束。但是这里有些问题:
- 需要自己管理复杂计算之间的相互关系,特别是每两次计算之间需要进行数据传递时;
- 需要实现多种分时计算的方案,比如连续帧执行,跨帧执行,间隔一定时间执行,甚至上面的这些分时方式的任意组合;
- Coroutine为一个引擎底层的实现类,其中含有一个Run函数(或Update)主要来做调用时的执行操作,该方法用在MonoBehavior的更新中会调用,当然,这里的调用是在MonoBehavior的Updata、FixedUpdate和LateUpdate之外,其间的具体顺序可以参考Unity的官方文档。
- StartCoroutine调用时传入的IEnumerator也会经过不同的编译,其中的每一个yield return 都会对应一个YieldInstruction的对象,比如WaitForSeconds等,Unity会将其处理成对应的延时对象,比如下定义的一个IEnumerator:
IEnumerator TestCode(){ Func1(); yield return new WaitForSeconds(0.5f); Func2(); yield return new WaitForSeconds(0.5f); //... FuncN(); yield return new IEnumeratorObject();}public class CompliedIEnumeratorObject : IEnumerator{ // execute counter int mExecuteCounter = 1; public object Current { get { return null; } } public bool MoveNext() { if (mExecuteCounter == 1) { Func1(); mExecuteCounter++; return true; } else if (mExecuteCounter == 2) { if (LastDelayIsOver() == false) { return true; } Func2(); mExecuteCounter++; return true; } //... else if (mExecuteCounter == N) { if (LastDelayIsOver() == false) { return true; } FuncN(); mExecuteCounter++; return true; } return false; } public void Reset() { }}每个派生自YieldInstruction的对象在编译后会对应一个类似于LastDelayIsOver这样的一个时间跨度判断操作,如果延时成功结束后则进行下一步的操作。
Coroutine::Run(){ MoveNext();}
如此一来,结合C#本身的迭代器特性和Unity中的YieldInstruction对象就可以实现对复杂计算函数的分时操作分解。
4. Unity5.3中的CustomYieldInstruction
通过上述分析,Coroutine的原理已经清楚,如果想实现自定义分时方式的话只需要在YieldInstruction的基础上实现类似于WaitForSeconds的对象即可。但是,在Unity5.3之前的YieldInstrction并没有对引擎外部开放,派生之后并没有什么卵用。不过这一限制在Unity5.3之后的版本中被放开,引擎中添加了一个CustomYieldInstructon的对象来实现自定义的YieldInstruction(延时)对象。而根据上述Coroutine的实现原理,我们也可以直接实现相应的IEnumerator类型的对象来达到同样的目的(注:在Unity5.3之前的版本中通过迭代器也不能实现自定义延时)
public class Wait4Seconds : IEnumerator{ float mSeconds = 0.0f; float mStartTime = 0.0f; public Wait4Seconds(float seconds) { mSeconds = seconds; mStartTime = Time.realtimeSinceStartup; } public object Current { get { return 0; } } public bool MoveNext() { if (Time.realtimeSinceStartup < mStartTime + mSeconds) { return true; } else { return false; } } public void Reset() { return; }}public class CoroutineTest : MonoBehavior{ // Use this for initialization void Start() { StartCoroutine(YieldEnumerator()); } IEnumerator YieldEnumerator() { Debug.Log("=================================== YieldEnumerator 1: " + Time.realtimeSinceStartup); yield return new Wait4Seconds(1.0f); Debug.Log("=================================== YieldEnumerator 2: " + Time.realtimeSinceStartup); }}
上述代码在Unity5.3中执行的结果就是第二个Log会比第一个延迟1秒后打印,也即成功的实现了自定义的延时对象。
综述,Unity中的Coroutine是一种很好的特性,在项目实践中需要合理的使用起来,让它发挥更大的作用。