unity3D應用隨筆,StartCoroutine,協程的概念

原文:http://blog.csdn.net/tkokof1/article/details/11842673


一 引子

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

 就遊戲開發而言,Unity也確實爲我們減輕了不少開發負擔、縮短了很多開發流程,但從開發原理上來講,使用Unity你仍然避不開許多傳統的開發技術, 譬如幾乎所有遊戲程序都有的Update,在Unity裏就變成了MonoBehaviour的一個成員方法;而另一個幾乎與Update並重的Init 方法,在Unity裏則被換成了Start。可以這麼說,Unity雖然極大的簡化了遊戲開發流程,但從方法原理上來講的話,其實他也並沒有和傳統開發方 式存在非常大的差異,Update還是那個Update,Init還是那個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#有了進一步的瞭解,我才慢慢發現,上面所言的那兩個奇怪的IEnumerator和yield 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
 }

 上述代碼首先通過array的GetEnumerator方法來獲取array的一個“迭代器”,然後通過“迭代器”的MoveNext方法進行依次遍 歷,而這“迭代器”實際上就是之前那個稍顯奇怪的IEnumerator類型!而至於yield return,其實是C# 2.0新引進的一種實現迭代器模式的簡便語法,在之前的C# 1.0中,如果要實現一個完整的迭代器,我們必須要分別實現IEnumerable和IEnumerator這兩個接口,過程略顯枯燥繁瑣,而藉助 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的構造函數非常簡單,似乎僅是記錄一個時 間變量罷了,根本就不存在什麼While、yield之類的東西,而其父類YieldInstruction則更簡單,就是單純的一個空類……另外 的,WWW這個Unity內建類型的使用方式也同樣讓我不解:

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour {
   public string url = "http://www.baidu.com/logo.gif";
   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這些個特殊類型之上,一直以爲這些類型肯定存在某 些個貓膩,但實際上,這些類型(WaitForSeconds、WWW之類)都是“非常正常”的類型,並沒有什麼與衆不同之處,而讓他們顯得與衆不同的, 其實是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之後,都會對他愛不釋 手的~ :)當然,這麼好的東西網上自然早以有了非常優秀的介紹,有興趣的朋友可以仔細看看 :)

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章