天天看點

IEnumerator

由于VR的關系,第一次接觸到了Unity3D的項目,對C#Script一些文法不是很了解,特别是IEnumerator yield,在項目中大量被使用,下面談談對它們的了解,文章轉自

作者:王選易,出處:  http://www.cnblogs.com/neverdie/ 歡迎轉載 ,也請保留這段聲明。如果你喜歡這篇文章,請點【推薦】。謝謝! 

IEnumerator

為什麼需要協程

在遊戲中有許多過程(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 = www.assetBundle;
}      

協程是什麼

從程式結構的角度來講,協程是一個有限狀态機,這樣說可能并不是很明白,說到協程(Coroutine),我們還要提到另一樣東西,那就是子例程(Subroutine),子例程一般可以指函數,函數是沒有  狀态 的,等到它return之後,它的所有局部變量就消失了,但是在協程中我們可以在  一個函數裡多次傳回, 局部變量被當作狀态儲存在協程函數中,知道最後一次return,協程的狀态才别清除。 

簡單來說,協程就是:你可以寫一段順序的代碼,然後标明哪裡需要暫停,然後在下一幀或者一段時間後,系統會繼續執行這段代碼。

協程怎麼用?

一個簡單的C#代碼,如下:

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* 做一系列的工作 */
 
        // 在這裡暫停然後在下一幀繼續執行      
yield return null;
    }
}      

協程是怎麼工作的

注意上邊的代碼示例,你會發現一個協程函數的傳回值是IEnumerator,它是一個疊代器,你可以把它當成指向一個序列的某個節點的指針,它提供了兩個重要的接口,分别是Current(傳回目前指向的元素)和MoveNext()(将指針向前移動一個機關,如果移動成功,則傳回true)。IEnumerator是一個interface,是以你不用擔心的具體實作。

通常,如果你想實作一個接口,你可以寫一個類,實作成員,等等。  疊代器塊(iterator block) 是一個友善的方式實作IEnumerator沒有任何麻煩-你隻是遵循一些規則,并實作IEnumerator由編譯器自動生成。 

一個疊代器塊具備如下特征:

  1. 傳回IEnumerator
  2. 使用yield關鍵字

是以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 ;
	yield return ;
	yield return ;
	yield return ;
	yield return ;
	yield return ;
}      

注意上文在疊代的過程中,你會發現,在兩個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(); 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(f);
 
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 < );
  /* ... */}      

繼續閱讀