Coroutine,你究竟幹了什麼?
使用Unity已經有一段時間了,對於Component、GameObject之類的概念也算是有所瞭解,而指令碼方面從一開始就選定了C#,目前來看還是挺明智的:Boo太小眾,而且支援有限;JS(或著說UnityScript)的話稍稍自由散漫了些,不太符合我們這些略顯嚴謹的程式猿;相比之下,C#各方面都十分沁人心腑,使用起來還是相當舒暢的 :)
就遊戲開發而言,Unity也確實為我們減輕了不少開發負擔、縮短了很多開發流程,但從開發原理上來講,使用Unity你仍然避不開許多傳統的開發技術,譬如幾乎所有遊戲程式都有的Update,在Unity裡就變成了MonoBehaviour的一個成員方法;而另一個幾乎與
依此思路,我持續著自己的Unity學習之路,也逐步驗證著自己上述的觀點,直到有一天,我遇到了Coroutine ……
二. Coroutine是什麼?
延時大概是遊戲程式設計中最司空見慣的需求之一:角色移動控制需要延時、事件觸發需要延時、甚至開啟一個粒子特效有時也需要延時,可以說,延時在遊戲開發中幾乎無處不在 :)有鑑於此,很多的遊戲引擎對於延時控制都提供了很好的支援,譬如在cocos2d-x
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://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這些個特殊型別之上,一直以為這些型別肯定存在某些個貓膩,但實際上,這些型別(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之後,都會對他愛不釋手的~ :)當然,這麼好的東西網上自然早以有了非常優秀的介紹,有興趣的朋友可以仔細看看 :)
好了,就這樣吧,下次再見了~