1. 程式人生 > >unity 協程 詳細說明

unity 協程 詳細說明

前言: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 關鍵字在編譯後將會編譯成如下形式的程式碼:
  IEnumerator ie = listForeach.GetEnumerator();
        while (ie.MoveNext())
        {
            Debug.Log("列舉元素:" + ie.Current);
        }
測試程式碼demo中如下:

測試程式碼: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 "列舉器";    }}控制檯列印:

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,外部類IEnumeratorTestGetEnumerator()方法例項化並返回一個新的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()方法實現的。轉載請標註文章來源。尊重他人的勞動成果。