1. 程式人生 > >Coroutine,你究竟幹了什麼?

Coroutine,你究竟幹了什麼?

  使用Unity已經有一段時間了,對於ComponentGameObject之類的概念也算是有所瞭解,而指令碼方面從一開始就選定了C#,目前來看還是挺明智的:Boo太小眾,而且支援有限;JS(或著說UnityScript)的話稍稍自由散漫了些,不太符合我們這些略顯嚴謹的程式猿;相比之下,C#各方面都十分沁人心腑,使用起來還是相當舒暢的 :)

  就遊戲開發而言,Unity也確實為我們減輕了不少開發負擔、縮短了很多開發流程,但從開發原理上來講,使用Unity你仍然避不開許多傳統的開發技術,譬如幾乎所有遊戲程式都有的Update,在Unity裡就變成了MonoBehaviour的一個成員方法;而另一個幾乎與

Update並重的Init方法,在Unity裡則被換成了Start。可以這麼說,Unity雖然極大的簡化了遊戲開發流程,但從方法原理上來講的話,其實他也並沒有和傳統開發方式存在非常大的差異,Update還是那個UpdateInit還是那個Init,只不過換了一個更簡單的形式而已~

  依此思路,我持續著自己的Unity學習之路,也逐步驗證著自己上述的觀點,直到有一天,我遇到了Coroutine ……

  二. Coroutine是什麼?

  延時大概是遊戲程式設計中最司空見慣的需求之一:角色移動控制需要延時、事件觸發需要延時、甚至開啟一個粒子特效有時也需要延時,可以說,延時在遊戲開發中幾乎無處不在 :)有鑑於此,很多的遊戲引擎對於延時控制都提供了很好的支援,譬如在cocos2d-x

中,CCDelayTime就是專門用來幹這個的,當然,其他引擎也有自己不同的支援方式,但是從實現層面來講,基本都是“標記開始時間,Update中持續更新檢查”這種方法,從程式碼上來看,大抵是這麼一個樣子:

  float delayTime = <time value to delay>;
  float elapsedTime = 0;
            
  void Update(float frameTime) {
      if (elapsedTime >= delayTime) {
          // delay is over here ...
      }
      else {
          elapsedTime += frameTime;
       }
  }

  而在Unity中,我們自然也可以使用這種方法來進行延時,但是相對而言,這種方法並不是最佳實踐,更好的在Unity中實現延時的做法是使用Coroutine,就程式碼上來看的話,大概是這個樣子:

  IEnumerator DelayCoroutine() {

// work before delay

yield return new WaitForSeconds(<time value to delay>);

// work after delay

}

  StartCoroutine(DelayCoroutine());

  沒有什麼elapsedTime之類的變數,甚至沒有什麼Update,你要做的就是寫一個以IEnumerator為返回型別的方法,然後在其中使用yield return這種語法來返回一個WaitForSeconds型別的例項,例項的構造引數就是你想要延時的時間,然後在需要的時候,呼叫StartCoroutine來進行延時即可。

  面對這種從未見過的延時實現方式,雖然程式碼表達上很容易讓人理解,一開始的我卻顯得有些抵觸,首先的一個疑問就是:這Coroutine是什麼?從字面意思上來理解,Coroutine應該就是“協程”的意思,而這所謂的“協程”又是什麼東西?第一個想到的便是Lua中“協程”,Unity中的Coroutine難道也是這個概念嗎?另外的,這Unity“協程”跟執行緒又是一個什麼關係,就其可以進行延時而不影響其他邏輯執行這個特性來看,“協程”是否就是C#執行緒的一個封裝呢?第二個疑問就是返回型別IEnumerator,名字奇怪也就罷了,我還需要使用yield return這種奇怪的方式來進行返回,而且貌似WaitForSeconds也並不是一個所謂IEnumerator的型別,怎麼就可以正常返回呢?第三個疑問,也是最大的一個疑問就是:雖然WaitForSeconds這個型別的名稱意義一目瞭然,但就實現層面來看,其是如何做到延時這項功能的著實讓人摸不著頭腦……

  三. Coroutine大概是這個樣子的……

  隨著自己對C#有了進一步的瞭解,我才慢慢發現,上面所言的那兩個奇怪的IEnumeratoryield return,其實並不是Unity的什麼獨創,相反,他們卻是C#中到處可見的迭代器的構造方式(之一),你也許對於迭代器這個東西沒什麼印象,但實際上,我們可能天天都在使用它!讓我們馬上來看一個最普遍的迭代器運用:

  int[] array = new int[] {1, 2, 3, 4, 5};
            
  foreach (int val in array) {
      // do something
  }

  程式碼非常簡單,不過是使用foreach來遍歷一個整型陣列,而程式碼中我們早已習以為常的foreach其實就是迭代器的語法糖,在真正的執行程式碼中,C#的編譯器會將上面的程式碼改頭換面成這個樣子:

  int[] array = new int[] {1, 2, 3, 4, 5};
            
  IEnumerator e = array.GetEnumerator();
  while (e.MoveNext()) {
      // do something
  }

  上述程式碼首先通過arrayGetEnumerator方法來獲取array的一個“迭代器”,然後通過“迭代器”的MoveNext方法進行依次遍歷,而這“迭代器”實際上就是之前那個稍顯奇怪的IEnumerator型別!而至於yield return,其實是C# 2.0新引進的一種實現迭代器模式的簡便語法,在之前的C# 1.0中,如果要實現一個完整的迭代器,我們必須要分別實現IEnumerableIEnumerator這兩個介面,過程略顯枯燥繁瑣,而藉助yield return,這兩個步驟我們都可以省略!譬如我們寫下了如下的程式碼:

IEnumerator Test() {
    yield return 1;
    yield return 2;
    yield return 3;
}

那麼C#編譯器就會幫你自動生成類似下面的這些程式碼(不準確,僅作示意):

public class InnerEnumerable : IEnumerable {
    public class InnerEnumerator : IEnumerator {
        int[] array = new int[] {1, 2, 3};
        int currentIndex = -1;
                
        public bool MoveNext() {
            ++currentIndex;
            return currentIndex < array.Length;
        }
                
        public Object Current {
            get { return array[currentIndex]; }
        }
                    
        public void Reset() {
            throw new Exception("unsurport");
        }
    }
            

public IEnumerator GetEnumerator() {
        return new InnerEnumerator();
    }
}        
        

IEnumerator Test() {
     InnerEnumerable e = new InnerEnumerable();
     return e.GetEnumerator();
 }

  當然,實際的迭代器程式碼實現遠非如此簡單,但原理上基本可以看做是一個有限狀態機,有興趣的朋友可以看看更深入的一些介紹,譬如這裡這裡

  OK,讓我們繼續回到Unity,通過上面的這些分析,我們大概就肯定了這麼一點:Unity其實是使用了迭代器來實現延時的,像IEnumerator、yield return等的使用皆是為了配合C#中迭代器的語法,其與什麼多執行緒之類的概念並沒有多少關係,但是目前我仍然還是不能理解之前的那個最大疑問:雖然迭代器可以保留執行狀態以便下次繼續往下執行,但是他本身並沒有提供什麼機制來達到延時之類的效果,像foreach這種語句,雖然使用了迭代器,但實際上也是一股腦兒執行完畢的,並不存在延時一說,那麼在Unity中,為什麼簡單的返回一個WaitForSeconds就可以呢?

  三 Coroutine原來如此 :)

  看來答案應該是在WaitForSeconds這個型別身上了~經過簡單的一些搜尋,我找到了這麼一篇帖子,內容便是如何自己實現一個簡單的WaitForSeconds,大體上的思路便是使用迴圈yield return null這種方法來達到延時的目的,直接抄一段帖子中的示例程式碼:

using UnityEngine; 

using System.Collections; 

public class TimerTest : MonoBehaviour { 

    IEnumerator Start () {

        yield return StartCoroutine(MyWaitFunction (1.0f));

        print ("1");

        yield return StartCoroutine(MyWaitFunction (2.0f));

        print ("2");

    }

    IEnumerator MyWaitFunction (float delay) {

        float timer = Time.time + delay;

        while (Time.time < timer) {

            yield return null;

        }

    }

}

  也就是說,如果我們在程式碼中寫下了如下的延時語句:

   yield return WaitForSeconds(1.0f);

  那麼在邏輯上,其大概等價於下面的這些語句:

   float timer = Time.time + 1.0f;

   while (Time.time < timer) {

       yield return null;

   }

  而完成這些操作的,很可能便是WaitForSeconds的建構函式,因為每次延時我們都就地生成(new)了一個WaitForSeconds例項。

  然而使用ILSpy檢視WaitForSeconds實現原始碼的結果卻又讓我迷惑:WaitForSeconds的建構函式非常簡單,似乎僅是記錄一個時間變數罷了,根本就不存在什麼Whileyield之類的東西,而其父類YieldInstruction則更簡單,就是單純的一個空類……另外的,WWW這個Unity內建型別的使用方式也同樣讓我不解:

using UnityEngine;

using System.Collections;

public class Example : MonoBehaviour {

    public string url = "http://images.earthcam.com/ec_metros/ourcams/fridays.jpg";

    IEnumerator Start() {

        WWW www = new WWW(url);

        yield return www;

        renderer.material.mainTexture = www.texture;

    }

}

  在上面的示例程式碼中,yield return www;這條語句可以做到直到url對應資源下載完畢才繼續往下執行(迭代),效果上類似於WaitForSeconds,但是WWW本身卻又不像WaitForSeconds那樣是個YieldInstruction,而且在使用上也是首先建立例項,然後直接yield 返回引用,按照這種做法,即便WWW的建構函式使用了上面的那種迴圈yield return null的方法,實際上也達不到我們想要的等待效果;再者便是語法上的一些細節,首先如果我們需要使用yield return的話,返回型別就必須是IEnumerable(<T>)或者IEnumerator(<T>)之一,而C#中的建構函式是沒有返回值的,顯然不符合這個原則,所以實際上在建構函式中我們無法使用什麼yield return,另外的一點,雖然上述帖子中的方法可以實現自己的延時操作,但每次都必須進行StartCoroutine操作(如果沒有也起不到延時效果),這一點也與一般的WaitForSeconds使用存在差異……

  後來看到了這篇文章,才大抵讓我有所釋懷:之前自己的種種猜測都聚焦在類似WaitForSeconds這些個特殊型別之上,一直以為這些型別肯定存在某些個貓膩,但實際上,這些型別(WaitForSecondsWWW之類)都是“非常正常”的型別,並沒有什麼與眾不同之處,而讓他們顯得與眾不同的,其實是StartCoroutine這個我過去一直忽略的傢伙!

  原理其實很簡單,WaitForSeconds本身是一個普通的型別,但是在StartCoroutine中,其被特殊對待了,一般而言,StartCoroutine就是簡單的對某個IEnumerator 進行MoveNext()操作,但如果他發現IEnumerator其實是一個WaitForSeconds型別的話,那麼他就會進行特殊等待,一直等到WaitForSeconds延時結束了,才進行正常的MoveNext呼叫,而至於WWW或者WaitForFixedUpdate等型別,StartCoroutine也是同樣的特殊處理,如果用程式碼表示一下的話,大概是這個樣子:

foreach(IEnumerator coroutine in coroutines)

{

    if(!coroutine.MoveNext())

        // This coroutine has finished

        continue;

    if(!coroutine.Current is YieldInstruction)

    {

        // This coroutine yielded null, or some other value we don't understand; run it next frame.

        continue;

    }

    if(coroutine.Current is WaitForSeconds)

    {

        // update WaitForSeconds time value

    }

    else if(coroutine.Current is WaitForEndOfFrame)

    {

        // this iterator will MoveNext() at the end of the frame

    }

    else /* similar stuff for other YieldInstruction subtypes or WWW etc. */

}

基於上述理論,我們就可以來實現自己的WaitForSeconds了:

首先是CoroutineManager,我們通過他來實現類似於StartCoroutine的功能:

//

//    <maintainer>Hugo</maintainer>

//    <summary>simple coroutine manager class</summary>

//

using UnityEngine;

using System.Collections.Generic;

public class CoroutineManager : MonoBehaviour {

public static CoroutineManager Instance {

    get;

private set;

}

List<System.Collections.IEnumerator> m_enumerators = new List<System.Collections.IEnumerator>();

List<System.Collections.IEnumerator> m_enumeratorsBuffer = new List<System.Collections.IEnumerator>();

void Awake() {

    if (Instance == null) {

    Instance = this;

}

else {

    Debug.LogError("Multi-instances of CoroutineManager");

}

}

void LateUpdate() {

    for (int i = 0; i < m_enumerators.Count; ++i) {

// handle special enumerator

if (m_enumerators[i].Current is CoroutineYieldInstruction) {

    CoroutineYieldInstruction yieldInstruction = m_enumerators[i].Current as CoroutineYieldInstruction;

if (!yieldInstruction.IsDone()) {

    continue;

}

}

// other special enumerator here ...

// do normal move next

if (!m_enumerators[i].MoveNext()) {

    m_enumeratorsBuffer.Add(m_enumerators[i]);

continue;

}

}

// remove end enumerator

for (int i = 0; i < m_enumeratorsBuffer.Count; ++i) {

    m_enumerators.Remove(m_enumeratorsBuffer[i]);

}

m_enumeratorsBuffer.Clear();

}

public void StartCoroutineSimple(System.Collections.IEnumerator enumerator) {

m_enumerators.Add(enumerator);

}

}

接著便是我們自己的WaitForSeconds了,不過在此之前我們先來實現WaitForSeconds的基類,CoroutineYieldInstruction

//

//    <maintainer>Hugo</maintainer>

//    <summary>coroutine yield instruction base class</summary>

//

using UnityEngine;

using System.Collections;

public class CoroutineYieldInstruction {

public virtual bool IsDone() {

    return true;

}

}

  很簡單不是嗎?型別僅有一個虛擬的IsDone方法,上面的CoroutineManager就是依據此來進行迭代器迭代的,OK,該是我們的WaitForSeconds上場了:

//

//    <maintainer>Hugo</maintainer>

//    <summary>coroutine wait for seconds class</summary>

//

using UnityEngine;

using System.Collections;

public class CoroutineWaitForSeconds : CoroutineYieldInstruction {

float m_waitTime;

float m_startTime;

public CoroutineWaitForSeconds(float waitTime) {

m_waitTime = waitTime;

m_startTime = -1;

}

public override bool IsDone() {

// NOTE: a little tricky here

if (m_startTime < 0) {

    m_startTime = Time.time;

}

// check elapsed time

return (Time.time - m_startTime) >= m_waitTime;

}

}

原理非常簡單,每次IsDone呼叫時進行累時,直到延時結束,就這麼簡單 :)

寫個簡單的案例來測試一下:

//

//    <maintainer>Hugo</maintainer>

//    <summary>coroutine test case</summary>

//

using UnityEngine;

using System.Collections;

public class CoroutineTest: MonoBehaviour {

void Start() {

// start unity coroutine

StartCoroutine(UnityCoroutine());

    // start self coroutine

CoroutineManager.Instance.StartCoroutineSimple(SelfCoroutine());

}

IEnumerator UnityCoroutine() {

Debug.Log("Unity coroutine begin at time : " + Time.time);

yield return new WaitForSeconds(5);

Debug.Log("Unity coroutine begin at time : " + Time.time);

}

IEnumerator SelfCoroutine() {

Debug.Log("Self coroutine begin at time : " + Time.time);

yield return new CoroutineWaitForSeconds(5);

Debug.Log("Self coroutine begin at time : " + Time.time);

}

}

效果雖然不如原生的WaitForSeconds那麼精確,但也基本符合期望,簡單給張截圖:

四 尾聲

  Coroutine這個東西對於我來說確實比較陌生,其中的迭代原理也困擾了我許久,不少抵觸情緒也“油然而生”(在此自我反省一下),但是經過簡單的一陣子試用,我卻赫然發現自己竟然離不開他了!究其原因,可能是其簡潔高效的特性深深折服了我,想想以前那些個分散於程式碼各處的計時變數和事件邏輯,現在統統都可以做成一個個Coroutine,不僅易於理解而且十分高效,我相信不管是誰,在實際使用了Unity中的Coroutine之後,都會對他愛不釋手的:)當然,這麼好的東西網上自然早以有了非常優秀的介紹,有興趣的朋友可以仔細看看 :)

  好了,就這樣吧,下次再見了~