做者:王選易,出處:http://www.cnblogs.com/neverdie/ 歡迎轉載,也請保留這段聲明。若是你喜歡這篇文章,請點【推薦】。謝謝!html
在遊戲中有許多過程(Process)須要花費多個邏輯幀去計算。算法
無論何時,若是你想建立一個可以歷經多個邏輯幀的流程,可是卻不使用多線程,那你就須要把一個任務來分割成多個任務,而後在下一幀繼續執行這個任務。cookie
好比,A*算法是一個擁有主循環的算法,它擁有一個open list來記錄它沒有處理到的節點,那麼咱們爲了避免影響幀率,可讓A*算法在每一個邏輯幀中只處理open list中一部分節點,來保證幀率不被影響(這種作法叫作time slicing)。網絡
再好比,咱們在處理網絡傳輸問題時,常常須要處理異步傳輸,須要等文件下載完畢以後再執行其餘任務,通常咱們使用回調來解決這個問題,可是Unity使用協程能夠更加天然的解決這個問題,以下邊的程序:多線程
private IEnumerator Test() { WWW www = new WWW(ASSEST_URL); yield return www; AssetBundle bundle = www.assetBundle; }
從程序結構的角度來說,協程是一個有限狀態機,這樣說可能並非很明白,說到協程(Coroutine),咱們還要提到另外一樣東西,那就是子例程(Subroutine),子例程通常能夠指函數,函數是沒有狀態的,等到它return以後,它的全部局部變量就消失了,可是在協程中咱們能夠在一個函數裏屢次返回,局部變量被看成狀態保存在協程函數中,知道最後一次return,協程的狀態才別清除。less
簡單來講,協程就是:你能夠寫一段順序的代碼,而後標明哪裏須要暫停,而後在下一幀或者一段時間後,系統會繼續執行這段代碼。異步
一個簡單的C#代碼,以下:函數
IEnumerator LongComputation() { while(someCondition) { /* 作一系列的工做 */ // 在這裏暫停而後在下一幀繼續執行
yield return null; } }
注意上邊的代碼示例,你會發現一個協程函數的返回值是IEnumerator,它是一個迭代器,你能夠把它當成指向一個序列的某個節點的指針,它提供了兩個重要的接口,分別是Current(返回當前指向的元素)和MoveNext()(將指針向前移動一個單位,若是移動成功,則返回true)。IEnumerator是一個interface,因此你不用擔憂的具體實現。spa
一般,若是你想實現一個接口,你能夠寫一個類,實現成員,等等。迭代器塊(iterator block)是一個方便的方式實現IEnumerator沒有任何麻煩-你只是遵循一些規則,並實現IEnumerator由編譯器自動生成。線程
一個迭代器塊具有以下特徵:
因此yield關鍵詞是幹啥的?它聲明序列中的下一個值或者是一個無心義的值。若是使用yield x(x是指一個具體的對象或數值)的話,那麼movenext返回爲true而且current被賦值爲x,若是使用yield break使得movenext()返回false。
那麼我舉例以下,這是一個迭代器塊:
public void 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;
固然了,咱們還能夠爲YieldInstruction添加各類的子類,好比一個很容易想到的就是yield return new WaitForNotification(「GameOver」)來等待某個消息的觸發,關於Unity的消息機制能夠參考這篇文章:【Unity3D技巧】在Unity中使用事件/委託機制(event/delegate)進行GameObject之間的通訊 (二) : 引入中間層NotificationCenter。
第一個有趣的地方是,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的博客,因此我把它翻譯過來與你們分享,但願你能喜歡。