在學習unity3d的時候很容易看到下面這個例子:html
1 void Start () { 2 StartCoroutine(Destroy()); 3 } 4 5 IEnumerator Destroy(){ 6 yield return WaitForSeconds(3.0f); 7 Destroy(gameObject); 8 }
這個函數乾的事情很簡單:調用StartCoroutine函數開啓協程,yield等待一段時間後,銷燬這個對象;因爲是協程在等待,因此不影響主線程操做。通常來講,看到這裏的時候都還不會暈,yield就是延時一段時間之後繼續往下執行唄,恩,學會了,看着還蠻好用的。java
====================================================分割線====================================================express
固然,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的場景ui
既然要使用yield,就得給個理由吧,不能爲了使用yield而使用yield。那麼先來看看遊戲中能夠用獲得yield的場景:
遊戲結算分數時,分數從0逐漸上漲,而不是直接顯示最終分數
十、九、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,直到這段文字被完整顯示。看到這裏,可能有童鞋不解:
要解釋這些問題,先看看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 ,就從當前位置繼續往下執行。詳情見這篇博客。
若是理解了這張圖,以前顯示人物對話的功能最後提到的那些疑惑也就很容易理解了: