unity 協程 詳細說明
阿新 • • 發佈:2019-02-10
前言:unity協程(coroutine) 其實就是一個列舉器 的封裝。下面將會說明協成的實現原理。本文件將會從c#列舉器到unity協成過程一步步去做說明,幫你深入理解unity 協成(coroutine)。demo下載地址1.c#列舉器是什麼?其實你只要用過List泛型列表遍歷元素(foreach),你就會用到列舉器 。如下面指令碼:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass yeidTest : MonoBehaviour { void Start() { listForeachTest(); } List<int> listForeach = new List<int>(); private void listForeachTest() { for (int i = 0; i < 5; i++) { listForeach.Add(i); } foreach (var item in listForeach) { Debug.Log("列舉元素:"+item); } }}
控制檯輸出:測試程式碼demo中的 列舉器舉例子
只要能使用foreach遍歷元素的型別都會用到列舉器,如Dictionary<>,ArrayList ,List<>等型別。2.列舉器到底是什麼呢?
foreach 關鍵字在編譯後將會編譯成如下形式的程式碼:
測試程式碼:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass yeidTest : MonoBehaviour { void Start() { listForeachTest(); } List<int> listForeach = new List<int>(); private void listForeachTest() { for (int i = 0; i < 5; i++) { listForeach.Add(i); }IEnumerator ie = listForeach.GetEnumerator(); while (ie.MoveNext()) { Debug.Log("列舉元素:" + ie.Current); } //foreach (var item in listForeach) //{ // Debug.Log("列舉元素:"+item); //} }}
控制檯輸出:while迴圈和foreach效果一樣。在下面的講解中我們列舉元素將使用while迴圈的方法列舉元素,不再使用foreach關鍵字。以便更好的說明協程。看到IEnumerator大家就眼熟了吧。實現協程的IEnumerator就是列舉器的介面。f12 檢視IEnumerator介面的定義using System.Runtime.InteropServices;namespace System.Collections{ [ComVisible(true)] [Guid("496B0ABF-CDEE-11D3-88E8-00902754C43A")] public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }}
成員說明:Current 遍歷當前型別時,儲存當前元素。MoveNext() 每呼叫一次,移動到下一個元素,返回下一個元素是否為空Reset() 重置到列表最開始列舉器就是實現IEnumerator介面,通過MoveNext()獲取下一個元素來遍歷每個元素的方法。
3. yield關鍵字yieldreturn返回集合(如連結串列List<>)的一個元素,並且移動到下一個元素。
特別注意:如果一個類定義了一個IEnumerator 返回值的GetEnumerator()方法,那麼這個類就可以列舉成員。如下程式碼:class IEnumeratorTest{ public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }}
現在就可以使用foreach迭代集合了。測試程式碼demo中如下:
所有程式碼:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass yeidTest : MonoBehaviour { void Start() { IEnumeratorTest enumeratorTest = new IEnumeratorTest(); foreach (var item in enumeratorTest) { Debug.Log(item); } }}class IEnumeratorTest{ public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }}控制檯列印:
上面的class IEnumeratorTest{ public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }}類將會被編譯成如下類似的程式碼,yield型別為IEnumeratorTest類的一個內部類Enumerator,外部類IEnumeratorTest的GetEnumerator()方法例項化並返回一個新的yield型別。在yield型別中,變數state定義當前迭代位置,每次MoveNext()方法後,改變當前迭代位置為下一個元素位置,並且設定current為當前迭代位置的一個物件。一下程式碼,主要看MoveNext()就行:publicclass IEnumeratorTest{ public IEnumerator GetEnumerator() { return new Enumerator(0); } public class Enumerator : IEnumerator<object>, IEnumerator, IDisposable { private int state; private object current; public Enumerator(int state) { this.state = state; } public object Current { get { return current; } } public void Dispose() { } public bool MoveNext() { switch (state) { case 0: state++; //改變當前迭代位置為下一個元素位置 current = 1; //當前迭代位置的物件 return true; case 1: state++; //改變當前迭代位置為下一個元素位置 current = 2; //當前迭代位置的物件 return true; case 2: state++; //改變當前迭代位置為下一個元素位置 current = "列舉器"; //當前迭代位置的物件 return true; } return false; } public void Reset() { } }}
測試程式碼demo中如下:
所有測試程式碼如下:using System;using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass YeildWhat : MonoBehaviour { void Start() { IEnumeratorTest enumeratorTest = new IEnumeratorTest(); #region 使用foreach 和下面while迴圈是一樣的---因為最終會編譯成while迴圈,上面已經說明了 //foreach (var item in enumeratorTest) //{ // Debug.Log(item); //} #endregion IEnumerator ie = enumeratorTest.GetEnumerator(); while (ie.MoveNext()) { Debug.Log("列舉元素:" + ie.Current); } }}publicclass IEnumeratorTest{ public IEnumerator GetEnumerator() { return new Enumerator(0); } public class Enumerator : IEnumerator<object>, IEnumerator, IDisposable { private int state; private object current; public Enumerator(int state) { this.state = state; } public object Current { get { return current; } } public void Dispose() { } public bool MoveNext() { switch (state) { case 0: state++; //改變當前迭代位置為下一個元素位置 current = 1; //當前迭代位置的物件 return true; case 1: state++; //改變當前迭代位置為下一個元素位置 current = 2; //當前迭代位置的物件 return true; case 2: state++; //改變當前迭代位置為下一個元素位置 current = "列舉器"; //當前迭代位置的物件 return true; } return false; } public void Reset() { } }}
將程式碼copy到指令碼測試:
列印如下:
列舉器其實就是通過每次呼叫MoveNext()方法,來改變集合中位當前元素位置,來一個個遍歷元素。類似通過更改下標來獲取元素。yield關鍵字其實是實現IEnumerator 等介面如MoveNext()方法的編譯器自動編譯的關鍵字。當然你也可以自己實現MoveNext()等方法,如果你不怕麻煩。5.1列舉器的封裝到協程上面我們對列舉器IEnumerator列舉器介面和yield關鍵字做出瞭解釋說明,開始對協程(coroutine)解釋說明。通過unity 的update()模擬協程。修改上面程式碼,不使用while迴圈或者foreach關鍵字列舉元素。我們將在update()時時重新整理來列舉所有元素。原理就是上面所說的每次MoveNext()方法後,改變當前迭代位置為下一個元素位置。測試程式碼demo中如下:
程式碼如下:using System;using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass update2Coroutine : MonoBehaviour { Update2CoroutineTest update2CoroutineTest = new Update2CoroutineTest(); IEnumerator e; public update2Coroutine() { e = update2CoroutineTest.GetEnumerator(); } // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (e.MoveNext()) { } }}publicclass Update2CoroutineTest{ public IEnumerator GetEnumerator() { Debug.Log("協程:"+1); yield return 0; Debug.Log("協程:" + 2); yield return 0; Debug.Log("協程:" + "列舉器"); yield return 0;
}}
控制檯列印:
5.2協程現在我們在用startcoroutine()方法啟動協程。測試程式碼demo中如下:
程式碼如下:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass CoroutineTest : MonoBehaviour { // Use this for initialization void Start () { CoroutineJsTest coroutineJsTest = new CoroutineJsTest(); StartCoroutine(coroutineJsTest.GetEnumerator()); } // Update is called once per frame void Update () { }}publicclass CoroutineJsTest{ public IEnumerator GetEnumerator() { Debug.Log("協程:"+1); yield return 0; Debug.Log("協程:" + 2); yield return 0; Debug.Log("協程:" + "列舉器"); yield return 0; }}
控制檯列印:從上面可以看出 ,在update()方法模擬協程和使用unity自帶StartCoroutine()方法啟動協程效果差不多。看來unity實現的StartCoroutine()啟動協程和我們update()模擬是一樣的。但是也不確定到底是不是通過類似update()方法實現的。反編譯 UnityEngine.dll程式集也沒有找到具體實現方法。。。。。但是唯一確定的一點就是unity也是通過列舉一步步執行程式塊的。類似update()模擬的協程,每次遇到yieldreturn ,就執行yield 型別的MoveNext()方法,改變當前迭代位置為下一個元素位置。等待下一次MoveNext()方法呼叫。StartCoroutine()方法會不停的呼叫MoveNext()方法方法(這樣就類似於foreach)。直到列舉結束。但是注意的是,yieldreturn 後面跟的值除了unity自帶的類(如:new WaitForSeconds(0.2f)。整合自 YieldInstruction)和協程語句塊(返回值為 IEnumerator的方法 ) ,其他值 沒有意義(yieldreturn 0和yieldreturn null其實都是一樣的,只是遇到yieldreturn就做相同處理,不會去處理後面跟的值了)。下面測試一下update()與協程等待時間首相列印協程執行的時間間隔和update()的時間間隔測試程式碼demo中如下:
程式碼如下:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass UpDateAndCoroutine : MonoBehaviour { ///<summary> 是否列印update的時間間隔</summary> public bool isGetUpdateDeltaTime; ///<summary> 是否列印協程的間隔時間</summary> public bool isPrintCoroutineTime; ///<summary> 設定協程執行的時間間隔</summary> public float waitTime; // Use this for initialization void Start () { updateCurrentTime= Time.realtimeSinceStartup; cotoutineCurrentTime = Time.realtimeSinceStartup; StartCoroutine(GetEnumerator()); } float updateDeltaTime = 0; float updateCurrentTime = 0; // Update is called once per frame void Update () { if (isGetUpdateDeltaTime) { getUpdateDeltaTime(); } } void getUpdateDeltaTime() { updateDeltaTime = Time.realtimeSinceStartup - updateCurrentTime; updateCurrentTime = Time.realtimeSinceStartup; Debug.Log("deltaTime:" + updateDeltaTime); } float cotoutineDeltaTime = 0; float cotoutineCurrentTime = 0; public IEnumerator GetEnumerator() { for (; ;) { updateDeltaTime = Time.realtimeSinceStartup - updateCurrentTime; updateCurrentTime = Time.realtimeSinceStartup; Debug.Log("deltaTime:" + updateDeltaTime); yield return new WaitForSeconds(waitTime); } }}
首先設定協程的時間間隔0.5秒和是否列印協程時間間隔,設定如下:
控制檯列印:
現在只打印update的時間間隔,設定如下:
列印如下,update時間間隔在0.015左右。好的現在時間間隔都有了。現在我們設定協程的時間間隔小於update的時間間隔,這裡我設定為0.009f:列印:你會發現不管你設定的協程時間間隔多小(前提小於update的時間間隔),列印的時間和update的時間非常接近,這說明你的協程時間間隔最小就是update的時間間隔。不可能再短了。即使你設定的比update時間間隔小。協程也只會執行update 的時間間隔了。正好也驗證了上面所說的:在update()方法模擬協程和使用unity自帶StartCoroutine()方法啟動協程效果差不多。看來unity實現的StartCoroutine()啟動協程和我們update()模擬是一樣的。但是也不確定到底是不是通過類似update()方法實現的。轉載請標註文章來源。尊重他人的勞動成果。
控制檯輸出:測試程式碼demo中的 列舉器舉例子
只要能使用foreach遍歷元素的型別都會用到列舉器,如Dictionary<>,ArrayList
foreach 關鍵字在編譯後將會編譯成如下形式的程式碼:
IEnumerator ie = listForeach.GetEnumerator();
while (ie.MoveNext())
{
Debug.Log("列舉元素:" + ie.Current);
}
測試程式碼demo中如下:測試程式碼:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass yeidTest
控制檯輸出:while迴圈和foreach效果一樣。在下面的講解中我們列舉元素將使用while迴圈的方法列舉元素,不再使用foreach關鍵字。以便更好的說明協程。看到IEnumerator大家就眼熟了吧。實現協程的IEnumerator就是列舉器的介面。f12 檢視IEnumerator介面的定義using System.Runtime.InteropServices;namespace System.Collections{ [ComVisible(true)] [Guid("496B0ABF-CDEE-11D3-88E8-00902754C43A")] public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }}
成員說明:Current 遍歷當前型別時,儲存當前元素。MoveNext() 每呼叫一次,移動到下一個元素,返回下一個元素是否為空Reset() 重置到列表最開始列舉器就是實現IEnumerator介面,通過MoveNext()獲取下一個元素來遍歷每個元素的方法。
3. yield關鍵字yieldreturn返回集合(如連結串列List<>)的一個元素,並且移動到下一個元素。
特別注意:如果一個類定義了一個IEnumerator 返回值的GetEnumerator()方法,那麼這個類就可以列舉成員。如下程式碼:class IEnumeratorTest{ public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }}
現在就可以使用foreach迭代集合了。測試程式碼demo中如下:
所有程式碼:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass yeidTest : MonoBehaviour { void Start() { IEnumeratorTest enumeratorTest = new IEnumeratorTest(); foreach (var item in enumeratorTest) { Debug.Log(item); } }}class IEnumeratorTest{ public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }}控制檯列印:
4.yeild解釋說明
包含yield語句的方法或屬性稱為迭代塊。如上面程式碼: public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }這個語句塊在編譯時將會編譯成一個yield型別,其中包含一個狀態機。yield型別實現 IEnumerator和IDisposable介面的屬性和方法。如果你感到迷惑,就編譯成IL中間語言看一下。這裡就不做說明了,上張圖,看一下大體明白就行:上面的class IEnumeratorTest{ public IEnumerator GetEnumerator() { yield return 1; yield return 2; yield return "列舉器"; }}類將會被編譯成如下類似的程式碼,yield型別為IEnumeratorTest類的一個內部類Enumerator,外部類IEnumeratorTest的GetEnumerator()方法例項化並返回一個新的yield型別。在yield型別中,變數state定義當前迭代位置,每次MoveNext()方法後,改變當前迭代位置為下一個元素位置,並且設定current為當前迭代位置的一個物件。一下程式碼,主要看MoveNext()就行:publicclass IEnumeratorTest{ public IEnumerator GetEnumerator() { return new Enumerator(0); } public class Enumerator : IEnumerator<object>, IEnumerator, IDisposable { private int state; private object current; public Enumerator(int state) { this.state = state; } public object Current { get { return current; } } public void Dispose() { } public bool MoveNext() { switch (state) { case 0: state++; //改變當前迭代位置為下一個元素位置 current = 1; //當前迭代位置的物件 return true; case 1: state++; //改變當前迭代位置為下一個元素位置 current = 2; //當前迭代位置的物件 return true; case 2: state++; //改變當前迭代位置為下一個元素位置 current = "列舉器"; //當前迭代位置的物件 return true; } return false; } public void Reset() { } }}
測試程式碼demo中如下:
所有測試程式碼如下:using System;using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass YeildWhat : MonoBehaviour { void Start() { IEnumeratorTest enumeratorTest = new IEnumeratorTest(); #region 使用foreach 和下面while迴圈是一樣的---因為最終會編譯成while迴圈,上面已經說明了 //foreach (var item in enumeratorTest) //{ // Debug.Log(item); //} #endregion IEnumerator ie = enumeratorTest.GetEnumerator(); while (ie.MoveNext()) { Debug.Log("列舉元素:" + ie.Current); } }}publicclass IEnumeratorTest{ public IEnumerator GetEnumerator() { return new Enumerator(0); } public class Enumerator : IEnumerator<object>, IEnumerator, IDisposable { private int state; private object current; public Enumerator(int state) { this.state = state; } public object Current { get { return current; } } public void Dispose() { } public bool MoveNext() { switch (state) { case 0: state++; //改變當前迭代位置為下一個元素位置 current = 1; //當前迭代位置的物件 return true; case 1: state++; //改變當前迭代位置為下一個元素位置 current = 2; //當前迭代位置的物件 return true; case 2: state++; //改變當前迭代位置為下一個元素位置 current = "列舉器"; //當前迭代位置的物件 return true; } return false; } public void Reset() { } }}
將程式碼copy到指令碼測試:
列印如下:
列舉器其實就是通過每次呼叫MoveNext()方法,來改變集合中位當前元素位置,來一個個遍歷元素。類似通過更改下標來獲取元素。yield關鍵字其實是實現IEnumerator 等介面如MoveNext()方法的編譯器自動編譯的關鍵字。當然你也可以自己實現MoveNext()等方法,如果你不怕麻煩。5.1列舉器的封裝到協程上面我們對列舉器IEnumerator列舉器介面和yield關鍵字做出瞭解釋說明,開始對協程(coroutine)解釋說明。通過unity 的update()模擬協程。修改上面程式碼,不使用while迴圈或者foreach關鍵字列舉元素。我們將在update()時時重新整理來列舉所有元素。原理就是上面所說的每次MoveNext()方法後,改變當前迭代位置為下一個元素位置。測試程式碼demo中如下:
程式碼如下:using System;using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass update2Coroutine : MonoBehaviour { Update2CoroutineTest update2CoroutineTest = new Update2CoroutineTest(); IEnumerator e; public update2Coroutine() { e = update2CoroutineTest.GetEnumerator(); } // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (e.MoveNext()) { } }}publicclass Update2CoroutineTest{ public IEnumerator GetEnumerator() { Debug.Log("協程:"+1); yield return 0; Debug.Log("協程:" + 2); yield return 0; Debug.Log("協程:" + "列舉器"); yield return 0;
}}
控制檯列印:
5.2協程現在我們在用startcoroutine()方法啟動協程。測試程式碼demo中如下:
程式碼如下:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass CoroutineTest : MonoBehaviour { // Use this for initialization void Start () { CoroutineJsTest coroutineJsTest = new CoroutineJsTest(); StartCoroutine(coroutineJsTest.GetEnumerator()); } // Update is called once per frame void Update () { }}publicclass CoroutineJsTest{ public IEnumerator GetEnumerator() { Debug.Log("協程:"+1); yield return 0; Debug.Log("協程:" + 2); yield return 0; Debug.Log("協程:" + "列舉器"); yield return 0; }}
控制檯列印:從上面可以看出 ,在update()方法模擬協程和使用unity自帶StartCoroutine()方法啟動協程效果差不多。看來unity實現的StartCoroutine()啟動協程和我們update()模擬是一樣的。但是也不確定到底是不是通過類似update()方法實現的。反編譯 UnityEngine.dll程式集也沒有找到具體實現方法。。。。。但是唯一確定的一點就是unity也是通過列舉一步步執行程式塊的。類似update()模擬的協程,每次遇到yieldreturn ,就執行yield 型別的MoveNext()方法,改變當前迭代位置為下一個元素位置。等待下一次MoveNext()方法呼叫。StartCoroutine()方法會不停的呼叫MoveNext()方法方法(這樣就類似於foreach)。直到列舉結束。但是注意的是,yieldreturn 後面跟的值除了unity自帶的類(如:new WaitForSeconds(0.2f)。整合自 YieldInstruction)和協程語句塊(返回值為 IEnumerator的方法 ) ,其他值 沒有意義(yieldreturn 0和yieldreturn null其實都是一樣的,只是遇到yieldreturn就做相同處理,不會去處理後面跟的值了)。下面測試一下update()與協程等待時間首相列印協程執行的時間間隔和update()的時間間隔測試程式碼demo中如下:
程式碼如下:using System.Collections;using System.Collections.Generic;using UnityEngine;publicclass UpDateAndCoroutine : MonoBehaviour { ///<summary> 是否列印update的時間間隔</summary> public bool isGetUpdateDeltaTime; ///<summary> 是否列印協程的間隔時間</summary> public bool isPrintCoroutineTime; ///<summary> 設定協程執行的時間間隔</summary> public float waitTime; // Use this for initialization void Start () { updateCurrentTime= Time.realtimeSinceStartup; cotoutineCurrentTime = Time.realtimeSinceStartup; StartCoroutine(GetEnumerator()); } float updateDeltaTime = 0; float updateCurrentTime = 0; // Update is called once per frame void Update () { if (isGetUpdateDeltaTime) { getUpdateDeltaTime(); } } void getUpdateDeltaTime() { updateDeltaTime = Time.realtimeSinceStartup - updateCurrentTime; updateCurrentTime = Time.realtimeSinceStartup; Debug.Log("deltaTime:" + updateDeltaTime); } float cotoutineDeltaTime = 0; float cotoutineCurrentTime = 0; public IEnumerator GetEnumerator() { for (; ;) { updateDeltaTime = Time.realtimeSinceStartup - updateCurrentTime; updateCurrentTime = Time.realtimeSinceStartup; Debug.Log("deltaTime:" + updateDeltaTime); yield return new WaitForSeconds(waitTime); } }}
首先設定協程的時間間隔0.5秒和是否列印協程時間間隔,設定如下:
控制檯列印:
現在只打印update的時間間隔,設定如下:
列印如下,update時間間隔在0.015左右。好的現在時間間隔都有了。現在我們設定協程的時間間隔小於update的時間間隔,這裡我設定為0.009f:列印:你會發現不管你設定的協程時間間隔多小(前提小於update的時間間隔),列印的時間和update的時間非常接近,這說明你的協程時間間隔最小就是update的時間間隔。不可能再短了。即使你設定的比update時間間隔小。協程也只會執行update 的時間間隔了。正好也驗證了上面所說的:在update()方法模擬協程和使用unity自帶StartCoroutine()方法啟動協程效果差不多。看來unity實現的StartCoroutine()啟動協程和我們update()模擬是一樣的。但是也不確定到底是不是通過類似update()方法實現的。轉載請標註文章來源。尊重他人的勞動成果。