天天看點

unity3d:協程實作原理(轉),IEnumerator,yield,編輯器下協程

IEnumerator

IEnumerator是所有非泛型枚舉器的基接口。換而言之就是IEnumerator定義了一種适用于任意集合的疊代方式。任意一個集合隻要實作自己的IEnumerator,它的使用者就可以通過IEnumerator疊代集合中的元素

public interface IEnumerator
{
    object Current { get; }

    bool MoveNext();
    void Reset();
}      

1.Current屬性可以擷取集合中目前疊代位置的元素

2.MoveNext方法将目前疊代位置推進到下一個位置,如果成功推進到下一個位置則傳回true,否則已經推進到集合的末尾傳回false

3.Reset方法可以将目前疊代位置設定為初始位置(該位置位于集合中第一個元素之前,是以當調用Reset方法後,再調用MoveNext方法,Curren值則為集合的第一個元素)

比如我們經常會使用的foreach關鍵字周遊集合,其實

隻是C#提供的文法糖而已

foreach (var item in collection)
{
   Console.WriteLine(item.ToString());
}      

在編譯時編譯器會将上面的循環轉換為類似于下面的代碼

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())  // 判斷是否成功推進到下一個元素(可了解為集合中是否還有可供疊代的元素)
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    } finally
    {
        // dispose of enumerator.
    }
}      

yield

yield是C#的關鍵字,其實就是快速定義疊代器的文法糖。隻要是yield

出現在其中的方法就會被編譯器自動編譯成一個疊代器,對于這樣的函數可以稱之為疊代器函數。疊代器函數的傳回值就是自動生成的疊代器類的一個對象

  1. yield return語句可以傳回一個值,表示疊代得到的目前元素
  2. yield break語句可以用來終止疊代,表示目前沒有可被疊代的元素了

Unity協程機制的實作原理

協程是一種比線程更輕量級的存在,協程可完全由使用者程式控制排程。協程可以通過yield方式進行排程轉移執行權,排程時要能夠儲存上下文,在排程回來的時候要能夠恢複。這是不是和上面“停住”代碼然後又原位恢複的執行效果很像?沒錯,Unity實作協程的原理,就是通過yield return生成的IEnumerator再配合控制何時觸發MoveNext來實作了執行權的排程

具體而言,Unity每通過MonoBehaviour.StartCoroutine啟動一個協程,就會獲得一個IEnumerator(StartCoroutine的參數就是IEnumerator,參數是方法名的重載版本也會通過反射拿到該方法對應的IEnumerator)。并在它的遊戲循環中,根據條件判斷是否要執行MoveNext方法。而這個條件就是根據IEnumerator的Current屬性獲得的,即yield return傳回的值。

在啟動一個協程時,Unity會先調用得到的IEnumerator的MoveNext一次,以拿到IEnumerator的Current值。是以每啟動一個協程,協程函數會立即執行到第一個yield return處然後“停住”。

對于不同的Current類型(一般是YieldInstruction的子類),Unity已做好了一些預設處理,比如:

  1. 如果Current是null,就相當于什麼也不做。在下一次遊戲循環中,就會調用MoveNext。是以yield return null就起到了等待一幀的作用
  2. 如果Current是WaitForSeconds類型,Unity會擷取它的等待時間,每次遊戲循環中都會判斷時間是否到了,隻有時間到了才會調用MoveNext。是以yield return WaitForSeconds就起到了等待指定時間的作用
  3. 如果Current是UnityWebRequestAsyncOperation類型,它是AsyncOperation的子類,而AsyncOperation有isDone屬性,表示操作是否完成,隻有isDone為true時,Unity才會調用MoveNext。對于UnityWebRequestAsyncOperation而言,隻有請求完成了,才會将isDone屬性設定為true。
using(UnityWebRequest webRequest = UnityWebRequest.Get()
{
    yield return webRequest.SendWebRequest();
    if(webRequest.isNetworkError)
    {
        Debug.Log("Error " + webRequest.error);
    }
    else
    {
        Debug.Log("Received " + webRequest.downloadHandler.text);
    }
}

namespace UnityEngine.Networking
{
    //
    // 摘要:
    //     Asynchronous operation object returned from UnityWebRequest.SendWebRequest().
    //     You can yield until it continues, register an event handler with AsyncOperation.completed,
    //     or manually check whether it's done (AsyncOperation.isDone) or progress (AsyncOperation.progress).
    [NativeHeader("Modules/UnityWebRequest/Public/UnityWebRequestAsyncOperation.h")]
    [NativeHeader("UnityWebRequestScriptingClasses.h")]
    [UsedByNativeCode]
    public class UnityWebRequestAsyncOperation : AsyncOperation
    {
        public UnityWebRequestAsyncOperation();

        //
        // 摘要:
        //     Returns the associated UnityWebRequest that created the operation.
        public UnityWebRequest webRequest { get; }
    }
}      

編輯器下協程

檔案浏覽器

​​ https://github.com/iwiniwin/unity-remote-file-explorer​​

Editor Coroutines version 0.0.1-preview.2

​​ https://docs.unity3d.com/Packages/[email protected]/manual/index.html​​ 對于不同的Current類型,生成不同的data,滿足data的條件,執行MoveNext

public void Set(object yield)
            {
                if (yield == data.current)
                    return;

                var type = yield.GetType();
                var dataType = DataType.None;
                double targetTime = -1;

                if(type == typeof(EditorWaitForSeconds))
                {
                    targetTime = EditorApplication.timeSinceStartup + (yield as EditorWaitForSeconds).WaitTime;
                    dataType = DataType.WaitForSeconds;
                }
                else if(type == typeof(EditorCoroutine))
                {
                    dataType = DataType.EditorCoroutine;
                }
                else if(type == typeof(AsyncOperation) || type.IsSubclassOf(typeof(AsyncOperation)))
                {
                    dataType = DataType.AsyncOP;
                }

                data = new ProcessorData { current = yield, targetTime = targetTime, type = dataType };
            }

            public bool MoveNext(IEnumerator enumerator)
            {
                bool advance = false;
                switch (data.type)
                {
                    case DataType.WaitForSeconds:
                        advance = data.targetTime <= EditorApplication.timeSinceStartup;
                        break;
                    case DataType.EditorCoroutine:
                        advance = (data.current as EditorCoroutine).m_IsDone;
                        break;
                    case DataType.AsyncOP:
                        advance = (data.current as AsyncOperation).isDone;
                        break;
                    default:
                        advance = data.current == enumerator.Current; //a IEnumerator or a plain object was passed to the implementation
                        break;
                }

                if(advance)
                {
                    data = default(ProcessorData);
                    Debug.Log("enumerator.CurrentBefore:" + enumerator.Current);
                    bool isHasNext = enumerator.MoveNext();
                    Debug.Log("enumerator.Current:" + enumerator.Current + "--isHasNext:" + isHasNext );
                    
                    return isHasNext;
                }
                return true;
            }      

測試

IEnumerator CountEditorUpdates()
    {
        yield return new EditorWaitForSeconds(10); //第1個current
        ++m_Updates;       //代碼塊a 滿足第1個cureent條件,cureent執行MoveNext後執行
        Debug.Log(m_Updates);
        yield return new EditorWaitForSeconds(8);//第2個 curent
        ++m_Updates;//代碼塊b  滿足第2個cureent條件,cureent執行MoveNext後執行
        Debug.Log(m_Updates);
    }      

輸出

unity3d:協程實作原理(轉),IEnumerator,yield,編輯器下協程
  1. 第0個進入的是個yield set 為null,接着立馬執行MoveNext,current = yield return new EditorWaitForSeconds(10); //第1個current
  2. 在EditorUpdate中檢測滿足 第1個current的時間條件,然後執行current.MoveNext,執行了代碼塊a,接着current = yield return new EditorWaitForSeconds(8);//第2個 curent
  3. 在EditorUpdate中檢測滿足第2個current的條件, 然後執行current.MoveNext,執行代碼塊b,并把current推到了集合的末尾,現在是推到了集合的的最後的,isHasNext 為false,清除掉EditorUpdate的該委托。但是實際沒推成功,current還是指向第2個current

源碼

繼續閱讀