Unity3d IEnumerator 協程的理解
由於VR的關係,第一次接觸到了Unity3D的專案,對C#Script一些語法不是很瞭解,特別是IEnumerator yield,在專案中大量被使用,下面談談對它們的理解,文章轉自
為什麼需要協程
在遊戲中有許多過程(Process)需要花費多個邏輯幀去計算。
- 你會遇到“密集”的流程,比如說尋路,尋路計算量非常大,所以我們通常會把它分割到不同的邏輯幀去進行計算,以免影響遊戲的幀率。
- 你會遇到“稀疏”的流程,比如說遊戲中的觸發器,這種觸發器大多數時候什麼也不做,但是一旦被呼叫會做非常重要的事情(比圖說遊戲中自動開啟的門就是在門前放了一個Empty Object作為trigger,人到門前就會觸發事件)。
不管什麼時候,如果你想建立一個能夠歷經多個邏輯幀的流程,但是卻不使用多執行緒,那你就需要把一個任務來分割成多個任務,然後在下一幀繼續執行這個任務。
比如,A*演算法是一個擁有主迴圈的演算法,它擁有一個open list來記錄它沒有處理到的節點,那麼我們為了不影響幀率,可以讓A*演算法在每個邏輯幀中只處理open list中一部分節點,來保證幀率不被影響(這種做法叫做time slicing)。
再比如,我們在處理網路傳輸問題時,經常需要處理非同步傳輸,需要等檔案下載完畢之後再執行其他任務,一般我們使用回撥來解決這個問題,但是Unity使用協程可以更加自然的解決這個問題,如下邊的程式:
private IEnumerator Test()
{
WWW www = new WWW(ASSEST_URL);
yield return www;
AssetBundle bundle =
協程是什麼
從程式結構的角度來講,協程是一個有限狀態機,這樣說可能並不是很明白,說到協程(Coroutine),我們還要提到另一樣東西,那就是子例程(Subroutine),子例程一般可以指函式,函式是沒有 狀態 的,等到它return之後,它的所有區域性變數就消失了,但是在協程中我們可以在 一個函式裡多次返回, 區域性變數被當作狀態儲存在協程函式中,知道最後一次return,協程的狀態才別清除。
簡單來說,協程就是:你可以寫一段順序的程式碼,然後標明哪裡需要暫停,然後在下一幀或者一段時間後,系統會繼續執行這段程式碼。
協程怎麼用?
一個簡單的C#程式碼,如下:
IEnumerator LongComputation()
{
while(someCondition)
{
/* 做一系列的工作 */
// 在這裡暫停然後在下一幀繼續執行
yield return null;
}
}
協程是怎麼工作的
注意上邊的程式碼示例,你會發現一個協程函式的返回值是IEnumerator,它是一個迭代器,你可以把它當成指向一個序列的某個節點的指標,它提供了兩個重要的介面,分別是Current(返回當前指向的元素)和MoveNext()(將指標向前移動一個單位,如果移動成功,則返回true)。IEnumerator是一個interface,所以你不用擔心的具體實現。
通常,如果你想實現一個介面,你可以寫一個類,實現成員,等等。 迭代器塊(iterator block) 是一個方便的方式實現IEnumerator沒有任何麻煩-你只是遵循一些規則,並實現IEnumerator由編譯器自動生成。
一個迭代器塊具備如下特徵:
- 返回IEnumerator
- 使用yield關鍵字
所以yield關鍵詞是幹啥的?它宣告序列中的下一個值或者是一個無意義的值。如果使用yield x(x是指一個具體的物件或數值)的話,那麼movenext返回為true並且current被賦值為x,如果使用yield break使得movenext()返回false。
那麼我舉例如下,這是一個迭代器塊:
publicvoid Consumer()
{
foreach(int i in Integers())
{
Console.WriteLine(i.ToString());
}
}
public IEnumerable<int> Integers()
{
yield return 1;
yield return 2;
yield return 4;
yield return 8;
yield return 16;
yield return 16777216;
}
注意上文在迭代的過程中,你會發現,在兩個yield之間的程式碼只有執行完畢之後,才會執行下一個yield,在Unity中,我們正是利用了這一點,我們可以寫出下面這樣的程式碼作為一個迭代器塊:
IEnumerator TellMeASecret(){
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null;
Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null;
PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}
然後我們可以使用下文這樣的客戶程式碼,來呼叫上文的程式,就可以實現延時的效果。
IEnumerator e = TellMeASecret();
while(e.MoveNext()) {
// do whatever you like
}
協程是如何實現延時的?
如你所見,yield return返回的值並不一定是有意義的,如null,但是我們更感興趣的是,如何使用這個yield return的返回值來實現一些有趣的效果。
Unity聲明瞭YieldInstruction來作為所有返回值的基類,並且提供了幾種常用的繼承類,如WaitForSeconds(暫停一段時間繼續執行),WaitForEndOfFrame(暫停到下一幀繼續執行)等等。更巧妙的是yield 也可以返回一個Coroutine真身,Coroutine A返回一個Coroutine B本身的時候,即等到B做完了再執行A。下面有詳細說明:
Normal coroutine updates are run after the Update function returns. A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes. Different uses of Coroutines:
yield; The coroutine will continue after all Update functions have been called on the next frame.
yield WaitForSeconds(2); Continue after a specified time delay, after all Update functions have been called for the frame
yield WaitForFixedUpdate(); Continue after all FixedUpdate has been called on all scripts
yield WWW Continue after a WWW download has completed.
yield StartCoroutine(MyFunc); Chains the coroutine, and will wait for the MyFunc coroutine to complete first.
實現延時的關鍵程式碼是在StartCoroutine裡面,以為筆者也沒有見過Unity的原始碼,那麼我只能猜想StartCoroutine這個函式的內部構造應該是這樣的:
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
foreach(IEnumerator coroutine in unblockedCoroutines){
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.
shouldRunNextFrame.Add(coroutine);
continue;
}
if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */}
unblockedCoroutines = shouldRunNextFrame;
還有些更好玩的?
第一個有趣的地方是,yield return可以返回任意YieldInstruction,所以我們可以在這裡加上一些條件判斷:
YieldInstruction y;
if(something)
y = null;else if(somethingElse)
y = new WaitForEndOfFrame();else
y = new WaitForSeconds(1.0f);
yield return y;
第二個,由於一個協程只是一個 迭代器塊 而已,所以你也可以自己遍歷它,這在一些場景下很有用,例如在對協程是否執行加上條件判斷的時候:
IEnumerator DoSomething(){
/* ... */}
IEnumerator DoSomethingUnlessInterrupted(){
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}}
第三個,由於協程可以yield協程,所以我們可以自己建立一個協程函式,如下:
IEnumerator UntilTrueCoroutine(Func fn){
while(!fn()) yield return null;}
Coroutine UntilTrue(Func fn){
return StartCoroutine(UntilTrueCoroutine(fn));}
IEnumerator SomeTask(){
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */}
總結
這篇文章大部分是我從 這篇部落格 裡面翻譯過來的,這是我見過最棒的一篇關於Coroutine的部落格,所以我把它翻譯過來與大家分享,希望你能喜歡。