本文包含兩個部分,前半部分是通俗解釋一下Unity中的協程,後半部分講講C#的IEnumerator迭代器web
爲了能通俗的解釋,咱們先用一個簡單的例子來看看協程能夠幹什麼c#
首先,我突發奇想,要實現一個倒計時器,我多是這樣寫的:數組
public class CoroutineTest : MonoBehaviour { public float sumtime = 3; void Update()//Update是每幀調用的 { { sumtime -= Time.deltaTime; if (sumtime <= 0) Debug.Log("Done!"); } } }
咱們知道,寫進 Update() 裏的代碼會被每幀調用一次,多線程
因此,讓總時間sumtime在Update()中每一幀減去一個增量時間Time.deltaTime(能夠理解成幀與幀的間隔時間)就能實現一個簡單的倒計時器app
可是,當咱們須要多個獨立的計時器時,用一樣的思路,咱們的代碼可能就會寫成這樣:函數
public class CoroutineTest : MonoBehaviour { public float sumtime1 = 3; public float sumtime2 = 2; public float sumtime3 = 1; void Update() { sumtime1 -= Time.deltaTime; if (sumtime1 <= 0) Debug.Log("timer1 Done!"); sumtime2 -= Time.deltaTime; if (sumtime2 <= 0) Debug.Log("timer2 Done!"); sumtime3 -= Time.deltaTime; if (sumtime3 <= 0) Debug.Log("timer3 Done!"); } }
重複度很高,計時器越多看的越麻煩oop
而後有朋友可能會提到,咱們是否是能夠用一個循環來解決這個問題性能
for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime) { //nothing } Debug.Log("This happens after 5 seconds");
如今每個計時器變量都成爲for循環的一部分了,這看上去好多了,並且我不須要去單獨設置每個跌倒變量。this
可是線程
可是
可是
咱們知道Update() 是每幀調用一次的,咱們不能把這個循環直接寫進Update() 裏,更不能寫一個方法在Update() 裏調用,由於這至關於每幀開啓一個獨立的循環
那麼有沒有辦法,再Update()這個主線程以外再開一個單獨的線程,幫咱們管理這個計時呢?
好了,你可能知道我想說什麼了,咱們正好能夠用協程來幹這個
先來看一段簡單的協程代碼
public class CoroutineTest : MonoBehaviour { void Start() { StartCoroutine(Count3sec()); } IEnumerator Count3sec() { for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime) yield return 0; Debug.Log("This happens after 3 seconds"); } }
你極可能看不懂上面的幾個關鍵字,但不急,咱們一個個解釋上面的代碼幹了什麼
StartCoroutine(Count3sec());
這一句用來開始咱們的Count3sec方法
而後你可能想問的是
理解如下的話稍有難度,但暫時理解不了問題也不大
IEnumerator 是C#的一個迭代器,你能夠把它當成指向一個序列的某個節點的指針,C#提供了兩個重要的接口,分別是Current(返回當前指向的元素)和 MoveNext()(將指針向前移動一個單位,若是移動成功,則返回true)
一般,若是你想實現一個接口,你能夠寫一個類,實現成員,等等。迭代器塊(iterator block) 是一個方便的方式實現IEnumerator,你只須要遵循一些規則,並實現IEnumerator由編譯器自動生成。
一個迭代器塊具有以下特徵:
那麼yield關鍵字是幹嗎的?它用來聲明序列中的下一個值,或者一個無心義的值。若是使用yield x(x是指一個具體的對象或數值)的話,那麼movenext返回爲true而且current被賦值爲x,若是使用yield break使得movenext()返回false(中止整個協程)
看不太懂?問題不大
你如今只須要理解,上面代碼中,IEnumerator類型的方法Count3sec就是一個協程,而且能夠經過yield關鍵字控制協程的運行
一個協程的執行,能夠在任何地方用yield語句來暫停,yield return的值決定了何時協程恢復執行。通俗點講,當你「yield」一個方法時,你至關於對這個程序說:「如今中止這個方法,而後在下一幀中,從這裏從新開始!」
yield return 0;
而後你可能會問,yield return後面的數字表示什麼?好比yield return 10,是否是表示延緩10幀再處理?
並不!
並不!
並不!
yield return 0表示暫緩一幀,也就是讓你的程序等待一幀,再繼續運行。(不必定是一幀,下面會講到如何控制等待時間)就算你把這個0換成任意的int類型的值,都是都是表示暫停一幀,從下一幀開始執行
它的效果相似於主線程單獨出了一個子線程來處理一些問題,並且性能開銷較小
如今你大體學會了怎麼開啓協程,怎麼寫協程了,來看看咱們還能幹點什麼:
IEnumerator count5times() { yield return 0; Debug.Log("1"); yield return 0; Debug.Log("2"); yield return 0; Debug.Log("3"); yield return 0; Debug.Log("4"); yield return 0; Debug.Log("5"); }
在這個協程中,咱們每隔一幀輸出了一次Hello,固然你也能夠改爲一個循環
IEnumerator count5times() { for (int i = 0; i < 5; i++) { Debug.Log("i+1"); yield return 0; } }
重點來了,有意思的是,你能夠在這裏加一個記錄始末狀態的變量:
public class CoroutineTest : MonoBehaviour { bool isDone = false; IEnumerator count5times() { Debug.Log(isDone); for (int i = 0; i < 5; i++) { Debug.Log("i+1"); yield return 0; } isDone = true; Debug.Log(isDone); } void Start() { StartCoroutine(count5times()); } }
很容易看得出上面的代碼實現了什麼,也就就是咱們一開始的需求,計時器
這個協程方法突出了協程一個「很是有用的,和Update()不一樣的地方:方法的狀態能被存儲,這使得方法中定義的這些變量(好比isUpdate)都會保存它們的值,即便是在不一樣的幀中
再修改一下,就是一個簡單的協程計時器了
public class CoroutineTest : MonoBehaviour { IEnumerator countdown(int count, float frequency) { Debug.Log("countdown START!"); for (int i = 0; i < count; i++) { for (float timer = 0; timer < frequency; timer += Time.deltaTime) yield return 0; } Debug.Log("countdown DONE!"); } void Start() { StartCoroutine(countdown(5, 1.0f)); } }
在上面的例子咱們也能看出,和普通方法同樣,協程方法也能夠帶參數
你甚至能夠經過yield一個WaitForSeconds()更方便簡潔地實現倒計時
public class CoroutineTest : MonoBehaviour { IEnumerator countdown(float sec)//參數爲倒計時時間 { Debug.Log("countdown START!"); yield return new WaitForSeconds(sec); Debug.Log("countdown DONE!"); } void Start() { StartCoroutine(countdown(5.0f)); } }
好了,可能你已經注意到了,yield的用法仍是不少的
在此以前,咱們以前的代碼yield的時候老是用0(或者能夠用null),這僅僅告訴程序在繼續執行前等待下一幀。如今你又學會了用yield return new WaitForSeconds(sec)來控制等待時間,你已經能夠作更多的騷操做了!
協程另外強大的一個功能就是,你甚至能夠yeild另外一個協程,也就是說,你能夠經過使用yield語句來相互嵌套協程,
public class CoroutineTest : MonoBehaviour { IEnumerator SaySomeThings() { Debug.Log("The routine has started"); yield return StartCoroutine(Wait(1.0f)); Debug.Log("1 second has passed since the last message"); yield return StartCoroutine(Wait(2.5f)); Debug.Log("2.5 seconds have passed since the last message"); } IEnumerator Wait(float waitsec) { for (float timer = 0; timer < waitsec; timer += Time.deltaTime) yield return 0; } void Start() { StartCoroutine(SaySomeThings()); } }
yield return StartCoroutine(Wait(1.0f));
這裏的Wait指的是另外一個協程,這至關因而說,「暫停執行本程序,等到直到Wait協程結束」
根據咱們上面講的特性,協程還能像建立計時器同樣方便的控制對象行爲,好比物體運動到某一個位置
IEnumerator MoveToPosition(Vector3 target) { while (transform.position != target) { transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime); yield return 0; } }
咱們還可讓上面的程序作更多,不只僅是一個指定位置,還能夠經過數組來給它指定更多的位置,而後經過MoveToPosition() ,可讓它在這些點之間持續運動。
咱們還能夠再加入一個bool變量,控制在對象運動到最後一個點時是否要進行循環
再把上文的Wait()方法加進來,這樣就能讓咱們的對象在某個點就能夠選擇是否暫停下來,停多久,就像一個正在巡邏的守衛同樣 (這裏沒有實現,各位讀者能夠嘗試本身寫一個)
public class CoroutineTest : MonoBehaviour { public Vector3[] path; public float moveSpeed; void Start() { StartCoroutine(MoveOnPath(true)); } IEnumerator MoveOnPath(bool loop) { do { foreach (var point in path) yield return StartCoroutine(MoveToPosition(point)); } while (loop); } IEnumerator MoveToPosition(Vector3 target) { while (transform.position != target) { transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime); yield return 0; } } IEnumerator Wait(float waitsec) { for (float timer = 0; timer < waitsec; timer += Time.deltaTime) yield return 0; } }
這裏列舉了yield後面能夠有的表達式
null,0,1,...... 暫緩一幀,下一幀繼續執行
StartCoroutine(Another coroutine) - in which case the new coroutine will run to completion before the yielder is resumed 等待另外一個協程暫停
值得注意的是 WaitForSeconds()受Time.timeScale影響,當Time.timeScale = 0f 時,yield return new WaitForSecond(x) 將不會知足
協程就是:你能夠寫一段順序代碼,而後標明哪裏須要暫停,而後在指定在下一幀或者任意間後,系統會繼續執行這段代碼
固然,協程不是真多線程,而是在一個線程中實現的
經過協程咱們能夠方便的作出一個計時器,甚至利用協程控制遊戲物體平滑運動
若是你剛接觸協程,我但願這篇博客能幫助你瞭解它們是如何工做的,以及如何來使用它們
迭代器是C#中一個普通的接口類,相似於C++ iterator的概念,基礎迭代器是爲了實現相似for循環 對指定數組或者對象 的 子元素 逐個的訪問而產生的。
public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
以上是IEnumerator的定義
Current() 的實現應該是返回調用者須要的指定類型的指定對象。
MoveNext() 的實現應該是讓迭代器前進。
Reset() 的實現應該是讓迭代器重置未開始位置
就像上文提到的,C#提供了兩個重要的接口,分別是Current(返回當前指向的元素)和 MoveNext()(將指針向前移動一個單位,若是移動成功,則返回true)固然IEnumerator是一個interface接口,你不用擔憂的具體實現
注意以上用的都是「應該是」,也就是說咱們能夠任意實現一個派生自」 IEnumerator」類的3個函數的功能,可是若是不按設定的功能去寫,可能會形成被調用過程出錯,無限循環
一個簡單的例子,遍歷並打印一個字符串數組:
public string[] m_StrArray = new string[4];
就能夠派生一個迭代器接口的子類
public class StringPrintEnumerator : IEnumerator { private int m_CurPt = -1; private string[] m_StrArray; public StringPrintEnumerator(string[] StrArray) { m_StrArray = StrArray; } ///實現 public object Current { get { return m_StrArray[m_CurPt]; } } public bool MoveNext() { m_CurPt++; if (m_CurPt == m_StrArray.Length) return false; return true; } public void Reset() { m_CurPt = -1; } ///實現END public static void Run() { string[] StrArray = new string[4]; StrArray[0] = "A"; StrArray[1] = "B"; StrArray[2] = "C"; StrArray[3] = "D"; StringPrintEnumerator StrEnum = new StringPrintEnumerator(StrArray); while (StrEnum.MoveNext()) { (string)ObjI = (string)StrEnum.Current; Debug.Log(ObjI); } } }
運行會依次輸出A B C D
可是若是:
不正確的實現Current(返回null,數組下表越界)執行到Debug.Log時候會報錯。
不正確地MoveNext(),可能會出現無限循環(固然若是邏輯上正須要這樣,也是正確的)
不正確地Reset(),下次再用同一個迭代器時候不能正確工做
因此這三個方法如何纔是正確的實現,徹底要根據由上層的調用者約定來寫
C#使用foreach語句取代了每次手寫while(StrEnum.MoveNext())進行遍歷
同時新定了一個接口類來包裝迭代器IEnumerator,也就是IEnumerable,定義爲:
public interface IEnumerable { IEnumerator GetEnumerator(); }
能夠看到IEnumerable接口很是的簡單,只包含一個抽象的方法GetEnumerator(),它返回一個可用於循環訪問集合的IEnumerator對象。
IEnumerable的做用僅僅是須要派生類寫一個返回指定迭代器的實現方法,也就是說IEnumerable僅僅是IEnumerator的一個包裝而已。
那麼返回的IEnumerator對象呢?它是一個真正的集合訪問器,沒有它,就不能使用foreach語句遍歷集合或數組,由於只有IEnumerator對象才能訪問集合中的項,才能進行集合的循環遍歷。
那麼咱們回到foreach
就像上面提到的,foreach須要的是一個定義了IEnumerator GetEnumerator()方法的對象,固然若是他是派生自IEnumerable對象那就更好了。
咱們繼續寫上文的StringPrintEnumerator類
這裏新定義他的IEnumerable派生類MyEnumerable