協程Coroutine在Unity中一直扮演者重要的角色。能夠實現簡單的計時器、將耗時的操做拆分紅幾個步驟分散在每一幀去運行等等,用起來非常方便。
可是,在使用的過程當中有沒有思考過協程是怎麼實現的?爲何能夠將一段代碼分紅幾段在不一樣幀執行?
本篇文章將從實現原理上更深刻的理解協程,最後確定也要實現咱們本身的協程。
關於協程的用法網上有不少介紹,不清楚的話能夠看下官方文檔,這裏不作贅述。html
在使用協程的時候,咱們老是要聲明一個返回值爲IEnumerator的函數,而且函數中會包含yield return xxx或者yield break之類的語句。就像文檔裏寫的這樣java
private IEnumerator WaitAndPrint(float waitTime) { yield return new WaitForSeconds(waitTime); print("Coroutine ended: " + Time.time + " seconds"); }
想要理解IEnumerator和yield就不得不說一下迭代器。迭代器是C#中一個十分強大的功能,只要類繼承了IEnumerable接口或者實現了GetEnumerator()方法就可使用foreach去遍歷類,遍歷輸出的結果是根據GetEnumerator()的返回值IEnumerator肯定的,爲了實現IEnumerator接口就不得不寫一堆繁瑣的代碼,而yield關鍵字就是用來簡化這一過程的。是否是很繞,理解這些內容須要花些時間。
不理解也不要緊,目前只須要明白一件事,當在IEnumerator函數中使用yield return語句時,每使用一次,迭代器中的元素內容就會增長一個。就嚮往列表中添加元素同樣,每Add一次元素內容就會多一個。
先來看看下面這段簡單的代碼git
IEnumerator TestCoroutine() { yield return null; //返回內容爲null yield return 1; //返回內容爲1 yield return "sss"; //返回內容爲"sss" yield break; //跳出,相似普通函數中的return語句 yield return 999; //因爲break語句,該內容沒法返回 } void Start() { IEnumerator e = TestCoroutine(); while (e.MoveNext()) { Debug.Log(e.Current); //依次輸出枚舉接口返回的值 } } /* 枚舉接口的定義 public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }*/ /*運行結果: Null 1 sss */
首先注意註釋部分枚舉接口的定義
Current屬性爲只讀屬性,返回枚舉序列中的當前位的內容
MoveNext()把枚舉器的位置前進到下一項,返回布爾值,新的位置如果有效的,返回true;不然返回false
Reset()將位置重置爲原始狀態github
再看下Start函數中的代碼,就是將yield return 語句中返回的值依次輸出。
第一次MoveNext()後,Current位置指向了yield return 返回的null,該位置是有效的(這裏注意區分位置有效和結果有效,位置有效是指當前位置是否有返回值,即便返回值是null;而結果有效是指返回值的結果是否爲null,顯然此處返回結果是無心義的)因此MoveNext()返回值是true;
第二次MoveNext()後,Current新位置指向了yield return 返回的1,該位置是有效的,MoveNext()返回true
第三次MoveNext()後,Current新位置指向了yield return 返回的"sss",該位置也是有效的,MoveNext()返回true
第四次MoveNext()後,Current新位置指向了yield break,無返回值,即位置無效,MoveNext()返回false,至此循環結束ide
最後輸出的運行結果跟咱們分析是一致的。關於C#是如何實現迭代器的功能,有興趣的能夠看下容器類源碼中關於迭代器部分的實現就明白了,MSDN上也有關於迭代器的詳細講解。函數
先來回顧下Unity的協程具體有些功能:測試
// case 1 IEnumerator Coroutine1() { //do something xxx //假如是第N幀執行該語句 yield return 1; //等一幀 //do something xxx //則第N+1幀執行該語句 } // case 2 IEnumerator Coroutine2() { //do something xxx //假如是第N秒執行該語句 yield return new WaitForSeconds(2f); //等兩秒 //do something xxx //則第N+2秒執行該語句 } // case 3 IEnumerator Coroutine3() { //do something xxx yield return StartCoroutine(Coroutine1()); //等協程Coroutine1執行完 //do something xxx }
好了,知道了IEnumerator函數和yield return語法以後,在看到上面幾個協程的功能,是否是對如何實現協程有點頭緒了?ui
實現分幀執行以前,先將上述迭代器的代碼簡單修改下,看下輸出結果3d
IEnumerator TestCoroutine() { Debug.Log("TestCoroutine 1"); yield return null; Debug.Log("TestCoroutine 2"); yield return 1; } void Start() { IEnumerator e = TestCoroutine(); while (e.MoveNext()) { Debug.Log(e.Current); //依次輸出枚舉接口返回的值 } } /*運行結果 TestCoroutine 1 Null TestCoroutine 2 1 */
前面有說過,每次MoveNext()後會返回yield return後的內容,那yield return以前的語句怎麼辦呢?
固然也執行啊,遇到yield return語句以前的內容都會在MoveNext()時執行的。
到這裏應該很清楚了,只要把MoveNext()移到每一幀去執行,不就實現分幀執行幾段代碼了麼!code
既然要分配在每一幀去執行,那固然就是Update和LateUpdate了。這裏我我的喜歡將實現代碼放在LateUpdate之中,爲何呢?由於Unity中協程的調用順序是在Update以後,LateUpdate以前,因此這兩個接口都不夠準確;但在LateUpdate中處理,至少能保證協程是在全部腳本的Update執行完畢以後再去執行。
如今能夠實現最簡單的協程了
IEnumerator e = null; void Start() { e = TestCoroutine(); } void LateUpdate() { if (e != null) { if (!e.MoveNext()) { e = null; } } } IEnumerator TestCoroutine() { Log("Test 1"); yield return null; //返回內容爲null Log("Test 2"); yield return 1; //返回內容爲1 Log("Test 3"); yield return "sss"; //返回內容爲"sss" Log("Test 4"); yield break; //跳出,相似普通函數中的return語句 Log("Test 5"); yield return 999; //因爲break語句,該內容沒法返回 } void Log(object msg) { Debug.LogFormat("<color=yellow>[{0}]</color>{1}", Time.frameCount, msg.ToString()); }
再來看看運行結果,黃色中括號括起來的數字表示當前在第幾幀,很明顯咱們的協程完成了每一幀執行一段代碼的功能。
要是徹底理解了case1的內容,相信你本身就能完成「延時等待」這一功能,其實就是加了個計時器的判斷嘛!
既然要識別本身的等待類,那固然要獲取Current值根據其類型去斷定是否須要等待。假如Current值是須要等待類型,那就延時到倒計時結束;而Current值是非等待類型,那就不須要等待,直接MoveNext()執行後續的代碼便可。
這裏着重說下「延時到倒計時結束」。既然知道Current值是須要等待的類型,那此時確定不能在執行MoveNext()了,不然等待就沒用了;接下來當等待時間到了,就能夠繼續MoveNext()了。能夠簡單的加個標誌位去作這一判斷,同時驅動MoveNext()的執行。
private void OnGUI() { if (GUILayout.Button("Test")) //注意:這裏是點擊觸發,沒有放在start裏,爲何? { enumerator = TestCoroutine(); } } void LateUpdate() { if (enumerator != null) { bool isNoNeedWait = true, isMoveOver = true; var current = enumerator.Current; if (current is MyWaitForSeconds) { MyWaitForSeconds waitable = current as MyWaitForSeconds; isNoNeedWait = waitable.IsOver(Time.deltaTime); } if (isNoNeedWait) { isMoveOver = enumerator.MoveNext(); } if (!isMoveOver) { enumerator = null; } } } IEnumerator TestCoroutine() { Log("Test 1"); yield return null; //返回內容爲null Log("Test 2"); yield return 1; //返回內容爲1 Log("Test 3"); yield return new MyWaitForSeconds(2f); //等待兩秒 Log("Test 4"); }
運行結果裏黃色表示當前幀,青色是當前時間,很明顯等待了2秒(雖然有少量偏差但整體不影響)。
上述代碼中,把函數觸發放在了Button點擊中而不是Start函數中?
這是由於我是用Time.deltaTime去作計時,假如放在了Start函數中,Time.deltaTime會受Awake這一幀執行時間影響,時間還不短(我測試時有0.1s左右),致使運行結果有很大偏差,不到2秒就結束了,有興趣的能夠本身試一下~
協程嵌套等待也就是下面這種樣子,在實際狀況中使用的也很多。
IEnumerator Coroutine1() { //do something xxx yield return null; //do something xxx yield return StartCoroutine(Coroutine2()); //等待Coroutine2執行完畢 //do something xxx yield return 3; } IEnumerator Coroutine2() { //do something xxx yield return null; //do something xxx yield return 1; //do something xxx yield return 2; }
實現原理的話基本與延時等待徹底一致,這裏我就不貼例子代碼了,最後會放出完整工程的。
須要注意下協程嵌套時的執行順序,先執行完內層嵌套代碼再執行外層內容;即更新結束條件時要先更新內層協程(上例Coroutine2)在更新外層協程(上例Coroutine1)。
前一節只是把每塊內容的原理用例子代碼實現了一下,實際使用中這樣確定不行,須要更通用的接口。
我按照Unity的接口方式把上述這些功能用相同名稱封裝了一下,並作了一些測試樣例與Unity原生接口運行結果做對比
下圖是最後一個測試樣例的代碼和運行結果,能夠看出表現是徹底一致的。
//Hi是命名空間 private void OnGUI() { GUILayout.BeginHorizontal(); if (GUILayout.Button("本身 嵌套的協程")) { Hi.CoroutineMgr.Instance.StartCoroutine(TestNesting()); } GUILayout.Space(20); if (GUILayout.Button("Unity 嵌套的協程")) { StartCoroutine(UnityNesting()); } GUILayout.EndHorizontal(); } IEnumerator TestNesting() { Log("Nesting 1"); yield return Hi.CoroutineMgr.Instance.StartCoroutine(TestNesting__()); Log("Nesting 2"); } IEnumerator TestNesting__() { Log("Nesting__ 1"); yield return Hi.CoroutineMgr.Instance.StartCoroutine(TestNormalCoroutine()); Log("Nesting__ 2"); yield return Hi.CoroutineMgr.Instance.StartCoroutine(TestWaitFor()); Log("Nesting__ 3"); } IEnumerator UnityNesting() { LogWarn("UnityNesting 1"); yield return StartCoroutine(UnityTesting__()); LogWarn("UnityNesting 2"); } IEnumerator UnityTesting__() { LogWarn("UnityTesting__ 1"); yield return StartCoroutine(UnityNormalCoroutine()); LogWarn("UnityTesting__ 2"); yield return StartCoroutine(UnityWaitFor()); LogWarn("UnityTesting__ 3"); } void Log(string message) { Debug.LogFormat("<color=yellow>[{0}]</color>-<color=cyan>[{1}]</color>{2}", Time.frameCount, System.DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), message); } void LogWarn(string message) { Debug.LogWarningFormat("<color=yellow>[{0}]</color>-<color=cyan>[{1}]</color>{2}", Time.frameCount, System.DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), message); }
最後放上工程地址GitHub。目前只是實現了經常使用的部分接口,足以知足平常使用,但像中止協程接口還未實現(後續會補上),感興趣的能夠本身完善。本篇文章有什麼問題歡迎你們討論、指出~~~