在學習unity3d的時候很容易看到下面這個例子:
1 void Start () { 2 StartCoroutine(Destroy()); 3 } 4 5 IEnumerator Destroy(){ 6 yield return WaitForSeconds(3.0f); 7 Destroy(gameObject); 8 }
這個函數干的事情很簡單:調用StartCoroutine函數開啟協程,yield等待一段時間后,銷毀這個對象;由於是協程在等待,所以不影響主線程操作。一般來說,看到這里的時候都還不會暈,yield就是延時一段時間以后繼續往下執行唄,恩,學會了,看着還蠻好用的。
====================================================分割線====================================================
當然,yield能干的事情遠遠不止這種簡單的特定時間的延時,例如可以在下一幀繼續執行這段代碼(yield return null),可以在下一次執行FixedUpdate的時候繼續執行這段代碼(yield new WaitForFixedUpdate ();),可以讓異步操作(如LoadLevelAsync)在完成以后繼續執行,可以……可以讓你看到頭暈。
unity3d官方對於協程的解釋是:一個協同程序在執行過程中,可以在任意位置使用yield語句。yield的返回值控制何時恢復協同程序向下執行。協同程序在對象自有幀執行過程中堪稱優秀。協同程序在性能上沒有更多的開銷。StartCoroutine函數是立刻返回的,但是yield可以延遲結果。直到協同程序執行完畢。(原文:The execution of a coroutine can be paused at any point using the yield statement. The yield return value specifies when the coroutine is resumed. Coroutines are excellent when modelling behaviour over several frames. Coroutines have virtually no performance overhead. StartCoroutine function always returns immediately, however you can yield the result. This will wait until the coroutine has finished execution.)
如果只是認為yield用於延時,那么可以用的很順暢;但是若看到yield還有這么多功能,目測瞬間就凌亂了,更不要說活學活用了。不過,如果從原理上進行理解,就很容易理清yield的各種功能了。
C#中的yield
1 public static IEnumerable<int> GenerateFibonacci() 2 { 3 yield return 0; 4 yield return 1; 5 6 int last0 = 0, last1 = 1, current; 7 8 while (true) 9 { 10 current = last0 + last1; 11 yield return current; 12 13 last0 = last1; 14 last1 = current; 15 } 16 }
yield return的作用是在執行到這行代碼之后,將控制權立即交還給外部。yield return之后的代碼會在外部代碼再次調用MoveNext時才會執行,直到下一個yield return——或是迭代結束。雖然上面的代碼看似有個死循環,但事實上在循環內部我們始終會把控制權交還給外部,這就由外部來決定何時中止這次迭代。有了yield之后,我們便可以利用“死循環”,我們可以寫出含義明確的“無限的”斐波那契數列。轉自老趙的博客。
IEnumerable與IEnumerator的區別比較小,在unity3d中只用到IEnumerator,功能和IEnumerable類似。至於他們的區別是什么,網上搜了半天,還是模糊不清,有童鞋能解釋清楚的請留言。不過對於這段代碼對於unity3d中yield的理解已經足夠了。
游戲中需要使用yield的場景
既然要使用yield,就得給個理由吧,不能為了使用yield而使用yield。那么先來看看游戲中可以用得到yield的場景:
-
游戲結算分數時,分數從0逐漸上漲,而不是直接顯示最終分數
- 人物對話時,文字一個一個很快的出現,而不是一下突然出現
-
10、9、8……0的倒計時
-
某些游戲(如拳皇)掉血時血條UI逐漸減少,而不是突然降低到當前血量
…………………………
unity3d中yield應用舉例
首先是官網的一段代碼:
1 using UnityEngine; 2 using System.Collections; 3 4 public class yield1 : MonoBehaviour { 5 6 IEnumerator Do() { 7 print("Do now"); 8 yield return new WaitForSeconds(2); 9 print("Do 2 seconds later"); 10 } 11 void Awake() { 12 StartCoroutine(Do()); 13 print("This is printed immediately"); 14 } 15 16 // Use this for initialization 17 void Start () { 18 19 } 20 21 // Update is called once per frame 22 void Update () { 23 24 } 25 }
這個例子將執行Do,但是Do函數之后的print指令會立刻執行。這個例子沒有什么實際意義,只是為了驗證一下yield確實是有延時的。
下面來看看兩段顯示人物對話的代碼(對話隨便復制了一段內容),功能是一樣的,但是方法不一樣:
1 using UnityEngine; 2 using System.Collections; 3 4 public class dialog_easy : MonoBehaviour { 5 public string dialogStr = "yield return的作用是在執行到這行代碼之后,將控制權立即交還給外部。yield return之后的代碼會在外部代碼再次調用MoveNext時才會執行,直到下一個yield return——或是迭代結束。雖然上面的代碼看似有個死循環,但事實上在循環內部我們始終會把控制權交還給外部,這就由外部來決定何時中止這次迭代。有了yield之后,我們便可以利用“死循環”,我們可以寫出含義明確的“無限的”斐波那契數列。"; 6 public float speed = 5.0f; 7 8 private float timeSum = 0.0f; 9 private bool isShowing = false; 10 // Use this for initialization 11 void Start () { 12 ShowDialog(); 13 } 14 15 // Update is called once per frame 16 void Update () { 17 if(isShowing){ 18 timeSum += speed * Time.deltaTime; 19 guiText.text = dialogStr.Substring(0, System.Convert.ToInt32(timeSum)); 20 21 if(guiText.text.Length == dialogStr.Length) 22 isShowing = false; 23 } 24 } 25 26 void ShowDialog(){ 27 isShowing = true; 28 timeSum = 0.0f; 29 } 30 }
這段代碼實現了在GUIText中逐漸顯示一個字符串的功能,速度為每秒5個字,這也是新手常用的方式。如果只是簡單的在GUIText中顯示一段文字,ShowDialog()函數可以做的很好;但是如果要讓字一個一個蹦出來,就需要借助游戲的循環了,最簡單的方式就是在Update()中更新GUIText。
從功能角度看,這段代碼完全沒有問題;但是從代碼封裝性的角度來看,這是一段很惡心的代碼,因為本應由ShowDialog()完成的功能放到了Update()中,並且在類中還有兩個private變量為這個功能服務。如果將來要修改或者刪除這個功能,需要在ShowDialog()和Update()中修改,並且還可能修改那兩個private變量。現在代碼比較簡單,感覺還不算太壞,一旦Update()中再來兩個類似的的功能,估計寫完代碼一段時間之后自己修改都費勁。
如果通過yield return null實現幀與幀之間的同步,則代碼優雅了很多:
1 using UnityEngine; 2 using System.Collections; 3 4 public class dialog_yield : MonoBehaviour { 5 public string dialogStr = "yield return的作用是在執行到這行代碼之后,將控制權立即交還給外部。yield return之后的代碼會在外部代碼再次調用MoveNext時才會執行,直到下一個yield return——或是迭代結束。雖然上面的代碼看似有個死循環,但事實上在循環內部我們始終會把控制權交還給外部,這就由外部來決定何時中止這次迭代。有了yield之后,我們便可以利用“死循環”,我們可以寫出含義明確的“無限的”斐波那契數列。"; 6 public float speed = 5.0f; 7 8 // Use this for initialization 9 void Start () { 10 StartCoroutine(ShowDialog()); 11 } 12 13 // Update is called once per frame 14 void Update () { 15 } 16 17 IEnumerator ShowDialog(){ 18 float timeSum = 0.0f; 19 while(guiText.text.Length < dialogStr.Length){ 20 timeSum += speed * Time.deltaTime; 21 guiText.text = dialogStr.Substring(0, System.Convert.ToInt32(timeSum)); 22 yield return null; 23 } 24 } 25 }
相關代碼都被封裝到了ShowDialog()中,這么一來,不論是要增加、修改或刪除功能,都變得容易了很多。
根據官網手冊的描述,yield return null可以讓這段代碼在下一幀繼續執行。在ShowDialog()中,每次更新文字以后yield return null,直到這段文字被完整顯示。看到這里,可能有童鞋不解:
- 為什么在協程中也可以用Time.deltaTime?
- 協程中的Time.deltaTime和Update()中的一樣嗎?
- 這樣使用協程,會不會出現與主線程訪問共享資源沖突的問題?(線程的同步與互斥問題)
- yield return null太神奇了,為什么會在下一幀繼續執行這個函數?
- 這段代碼是不是相當於為ShowDialog()構造了一個自己的Update()?
要解釋這些問題,先看看unity3d中的協程是怎么運行的吧。
協程原理分析
本段內容轉自這篇博客,想看的童鞋自己點擊。
首先,請你牢記:協程不是線程,也不是異步執行的。協程和 MonoBehaviour 的 Update函數一樣也是在MainThread中執行的。使用協程你不用考慮同步和鎖的問題。
UnityGems.com給出了協程的定義:
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
協程是一個分部執行,遇到條件(yield return 語句)會掛起,直到條件滿足才會被喚醒繼續執行后面的代碼。Unity在每一幀(Frame)都會去處理對象上的協程。Unity主要是在Update后去處理協程(檢查協程的條件是否滿足):
從上圖的剖析就明白,協程跟Update()其實一樣的,都是Unity每幀對會去處理的函數(如果有的話)。如果MonoBehaviour 是處於激活(active)狀態的而且yield的條件滿足,就會協程方法的后面代碼。還可以發現:如果在一個對象的前期調用協程,協程會立即運行到第一個 yield return 語句處,如果是 yield return null ,就會在同一幀再次被喚醒。如果沒有考慮這個細節就會出現一些奇怪的問題。
注:圖和結論都是從UnityGems.com 上得來的,經過驗證發現與實際不符,D.S.Qiu用的是Unity 4.3.4f1 進行測試的。經過測試驗證,協程至少是每幀的LateUpdate()后去運行。
協程其實就是一個IEnumerator(迭代器),IEnumerator 接口有兩個方法 Current 和 MoveNext() ,迭代器方法運行到 yield return 語句時,會返回一個expression表達式並保留當前在代碼中的位置。 當下次調用迭代器函數時執行從該位置重新啟動。unity3d在每幀做的工作就是:調用協程(迭代器)MoveNext() 方法,如果返回 true ,就從當前位置繼續往下執行。詳情見這篇博客。
如果理解了這張圖,之前顯示人物對話的功能最后提到的那些疑惑也就很容易理解了:
- 協程和Update()一樣更新,自然可以使用Time.deltaTime了,而且這個Time.deltaTime和在Update()當中使用是一樣的效果(使用yield return null的情況下)
- 協程並不是多線程,它和Update()一樣是在主線程中執行的,所以不需要處理線程的同步與互斥問題
- yield return null其實沒什么神奇的,只是unity3d封裝以后,這個協程在下一幀就被自動調用了
- 可以理解為ShowDialog()構造了一個自己的Update(),因為yield return null讓這個函數每幀都被調用了