天天看點

C#疊代器的實作和應用(三)——Unity的協程分析以及實作自己的協程

文章目錄

      • 一、Unity協程簡單回顧
      • 二、Unity協程的分析
        • 1. C#疊代器
        • 2 遊戲循環
        • 3. 協程實作的核心邏輯
      • 三、協程的實作設計
        • 1. 協程的實作設計
        • 2. 協程類的執行邏輯
        • 3. 疊代器棧在每一次MoveNext的運作流程圖
        • 4. 兩個簡單類的實作思路
      • 四、協程代碼實作
        • 1. 輔助類Debug
        • 2. CorotineEngine類
        • 3. Coroutine類
        • 4. WaitForSeconds類
        • 5. WaitUntil 類
        • 7. 測試結果
      • 總結

在前面兩篇文章,我們了解了C#疊代器的基礎知識,分析了延遲執行的本質,并實作了兩個LINQ常用擴充,在這一篇裡,我将解析Unity中的協程功能并實作一個自己的協程功能

前置知識回顧:

C#疊代器的實作和應用(一)——基礎篇

C#疊代器的實作和應用(二)——延遲執行、流式處理與兩個基本LINQ擴充的實作

我在github上存放了一份完整的項目,有需要的也可以研究檢視,歡迎各路大佬朋友指正。

git連結

一、Unity協程簡單回顧

協程是Unity中非常好用和常用的功能之一,它利用C#的疊代器,在主線程中實作了一個并發的效果,雖然Unity的協程和一些其他語言中提供的協程在使用上存在比較大的差别(但又哪有兩個完全一樣的協程呢,lua和golang這兩個語言原生的協程使用也存在差別)。

我們這裡編寫幾個最簡單的協程。

public class CoroutineTest : MonoBehaviour {

	void Start () {
        StartCoroutine(TestNull());
        StartCoroutine(TestWaitUntil());
    }

    bool condition = false;
   IEnumerator TestNull()
    {
        Debug.Log("[" + Time.time + "]\t" + "Test Null 1");
        yield return null;
        Debug.Log("[" + Time.time + "]\t" + "Test Null 2");
        yield return TestWaitForSeconds();

    }
    IEnumerator TestWaitForSeconds()
    {
        Debug.Log("[" + Time.time + "]\t" + "TestWaitForSeconds : start WaitForSeconds");
        yield return new WaitForSeconds(5);
        condition = true;
        Debug.Log("[" + Time.time + "]\t" + "TestWaitForSeconds : stop WaitForSeconds");
        yield return 2;
    }

    IEnumerator TestWaitUntil()
    {
        Debug.Log("[" + Time.time + "]\t" + "start WaitUntil");
        yield return new WaitUntil(() => condition);
        Debug.Log("[" + Time.time + "]\t" + "stop WaitUntil");
    }

           
  • TestNull

    方法中使用

    yield return null

    中斷了一次,然後嵌套了

    TestWaitForSeconds

    方法
  • TestWaitForSeconds

    方法中建立了一個

    WaitForSeconds

    的協程,等待五秒,完成之後再将成員變量

    condition

    的值變為true,最後再使用

    yield return 2

    傳回一個2
  • TestWaitUntil

    方法建立了一個

    WaitUntil

    協程,當

    condition

    為true時進入下一步
  • Start

    方法是Unity自帶的“魔法函數”,在腳本初始化完成後被調用,在這個方法中使用

    StartCoroutine

    方法開啟了兩個協程。

來檢視一下輸出結果:

C#疊代器的實作和應用(三)——Unity的協程分析以及實作自己的協程

很容易看到,WaitForSeconds啟動後,等待了五秒(注意start WaitForSeconds和stop WaitForSeconds的時間差異),之後

condition

被置為true,WaitUntil也測試通過了。但這個過程并不會導緻程式卡住,如果你在

update

方法中同樣進行log,會發現

update

方法中的log和協程中的log是同步的,整體的表現得就像是異步進行了這些操作一樣——但這些操作的寫法又是同步的,甚至連運作都是在主線程進行的。

如果對Unity的協程不熟悉,想了解關于Unity協程的更具體内容和使用方法,可以檢視官方手冊:協程;

二、Unity協程的分析

應該幾乎所有的unity開發者都會使用unity的協程,但它是如何實作的呢?

其實很簡單——兩個關鍵知識點:C#疊代器和遊戲循環

下面我将逐個拆解實作原理。

1. C#疊代器

因為這是關于C#疊代器的系列文章,是以其實在前兩篇文章中我就已經對C#的疊代器特點及應用作出了一些講解,而Unity協程的實作就依賴了在前兩篇文章中所講的知識:延遲處理和yiled 簡化疊代器編寫。

Unity的協程有兩種形式,一種是以

IEnumerator

為傳回值的方法,另一種是繼承了

IEnumerator

的類型,

IEnumerator

這個接口我們已經很很熟悉了,說白了,Unity的“協程”本質上就是一個疊代器。

2 遊戲循環

遊戲循環是幾乎所有遊戲都存在的核心元件,與普通應用不同的是,在整個遊戲過程中,遊戲循環是重中之重。

在Robert Nystrom寫的《遊戲程式設計模式》一書中就專門有一個講遊戲循環的章節,裡面有這樣一段話:

假如有哪個模式是本書最無法山羊的,那麼非遊戲循環模式莫屬。遊戲循環模式是遊戲程式設計模式中的精髓。幾乎所有的遊戲中都包含着它,無一雷同,相比而言那些非遊戲程式中卻難見它的身影。

同樣,在Jason Gregory著的《遊戲引擎架構》一書中,也專門留出了篇幅對遊戲循環進行講解。

不過吹了這麼多,遊戲循環到底是個什麼東西?

簡單來說,就是在遊戲啟動後的Main函數中運作的一個死循環,每一幀都進行一定時間的暫停,防止程序卡死,類似于下面這樣:

while (true)
   {
   	ProcessInput();//檢測輸入
	Update();//更新畫面
	Render();//渲染畫面
    Thread.Sleep(17);
   }
           

而我們所有的代碼,都在這個循環中反複運作。

機智的小夥伴這時候一定意識到了,這個

Update

其實就是Unity裡

MonoBehaviour

類中的

Update

當然,實際的遊戲循環要更加複雜。下面這是Unity的

MonoBehaviour

生命周期圖中的一部分 (完整周期點選檢視),其中

Input events

Game logic

Scene rendering

Gizmo rendering

等等都是屬于遊戲循環中的一部分。

C#疊代器的實作和應用(三)——Unity的協程分析以及實作自己的協程

如果對遊戲循環感興趣,推薦閱讀我前面提到過的兩本書:

《遊戲程式設計模式》《遊戲引擎架構》

3. 協程實作的核心邏輯

那麼這個遊戲循環跟協程的實作有什麼關系呢?

我們把遊戲循環和疊代器合在一起看:

  • 遊戲循環是一個具有在每一幀都進行一次調用的死循環
  • C#疊代器是一個需要被多次調用以驅動運作并且擷取值的集合
  • C#疊代器中可以封裝具體的操作,在每次疊代時被調用
  • C#疊代器可以使用

    yield

    來簡化封裝操作

有了這些條件,我們就可以做到使用遊戲循環來驅動疊代器 , 使用yield來編寫函數以自動封裝操作,也可以直接編寫具體的疊代器,通過傳入操作來篩選疊代器的結束條件。

三、協程的實作設計

理論到前面為止,下面我們可以開始實作自己的協程了——如果還沒有消化,記得再傳回去了解一下疊代器的功能特點哦!

1. 協程的實作設計

關于協程是如何運作的,在前面已經解析了,那麼我們來設計一下協程的具體運作方式。

  • 在我們的設計裡,以

    IEnumerator

    接口為核心,

    WaitUntil

    WaitForSeconds

    兩個類均實作接口

    IEnumerator

    作為協程進行調用;
  • Coroutine

    CorotineEngine

    是我們的核心工具類, 其中

    Coroutine

    是協程的包裝,

    CorotineEngine

    是驅動

    Coroutine

    的工具類;
  • CorotineEngine

    中包含一個

    StartCoroutine

    方法,傳入一個疊代器,在這個方法中将使用

    Coroutine

    對疊代器進行包裝,建立一個

    Corotutine

    類型的變量并儲存在

    coroutines

    中;每一次

    CoroutineUpdate

    方法被調用,都将驅動

    Coroutine

    運作;
  • Coroutine

    中一個傳回值為bool的

    MoveNext

    成員函數,當

    MoveNext

    的傳回值為false時,說明這個協程已經運作結束。

下面是簡單的類圖

2. 協程類的執行邏輯

  • 前面有提到,我們的核心執行工具類是

    Coroutine

    CorotineEngine

    ,

    CorotineEngine

    接近于Unity中

    MonoBehaviour

    的協程部分,它包含開始協程運作的

    StartCoroutine

    函數,并傳回一個

    Coroutine

    執行個體;
  • 在每一次

    CorotineEngine

    Update

    被調用時,所有

    coroutines

    中的包裝器都會被驅動運作一次

    MoveNext

    ,當

    MoveNext

    函數的傳回值為false,那麼說明這個協程已經運作結束,這個協程将會被從清單中移除;
  • Coroutine

    類通過傳入一個疊代器進行建立,每一個

    Coroutine

    執行個體都包含一個用于儲存目前運作的疊代器的棧,在疊代器被驅動運作時有以下幾種情況:
    1. 如果目前疊代器棧為空,不包含任何疊代器,那麼此疊代器已運作結束,協程的

      MoveNext

      傳回

      false

    2. 如果目前疊代器棧最上層的疊代器的

      MoveNext

      傳回

      false

      ,那麼表明此疊代器已疊代結束,此時将這個疊代器從棧中彈出,再傳回第一個判斷
    3. 如果目前疊代器棧最上層的疊代器的

      MoveNext

      傳回

      true

      ,并且

      Current

      也是一個疊代器,那麼将把這個疊代器壓入棧,再傳回第一個判斷,并且此協程的

      MoveNext

      傳回

      true

    4. 如果目前疊代器棧最上層的疊代器的

      MoveNext

      傳回

      true

      ,并且

      Current

      不是一個疊代器,那麼此協程的

      MoveNext

      傳回

      true

3. 疊代器棧在每一次MoveNext的運作流程圖

C#疊代器的實作和應用(三)——Unity的協程分析以及實作自己的協程

4. 兩個簡單類的實作思路

  • WaitUntil

    在構造函數中傳入一個傳回值為bool的lamda表達式并儲存,每次調用

    MoveNext

    時運作一次這個lamda表達式,需要注意的是,因為MoveNext為false時表示運作結束,是以要對并傳回這個值進行取反。
  • WaitForSeconds

    因為直接使用控制台程式運作 ,沒有Unity自帶的Time類,是以我使用一c#的

    DateTime

    進行計時;

    WaitUntil

    思路很簡單,在建立時傳入一個以秒為機關的時間,同時使用

    DateTime.Now.AddSeconds

    來計算結束時間,之後在

    MoveNext

    中将目前時間與結束時間對比,如果目前時間超過了結束時間,那麼MoveNext傳回false,表示疊代結束;

    在疊代器中current僅用于判斷是否需要壓棧,是以随意傳回一個引用類型的值即可——建議不要傳回一個值類型,因為會造成一次拆裝箱損耗。

解析到此結束,後面就是代碼啦。

四、協程代碼實作

1. 輔助類Debug

為了友善檢視時間,設計了Debug類用于Log

class Debug
    {
        public static void Log(string source, string format = "", params object[] args)
        {
            var f = string.Format("[{0}]\t[{1}] : {2}", DateTime.Now.ToString("HH:mm:ss fff"), source, format);
            Console.WriteLine(f, args);
        }
    }
           

2. CorotineEngine類

我們的簡單引擎,有用于儲存協程的List、用于更新的Update方法、用于驅動協程的UpdateCoroutine方法、添加協程的StartCoroutine方法,簡單來說,可以把這個類看成MonoBehaviour中關于協程的那一部分。

class CoroutineEngine
    {
        public CoroutineEngine()
        {
            Debug.Log("CoroutineEngine", "setup");
        }
        List<Coroutine> coroutines = new List<Coroutine>();
        public void Update()
        {
            //Debug.Log("CoroutineEngine", "Update");
        }

        public void CoroutineUpdate()
        {
            for (int i = 0; i < coroutines.Count; i++)
            {
                if (coroutines[i].MoveNext())
                {
                    //Debug.Log("CoroutineEngine", "CoroutineUpdate");
                }
                else
                {
                    Debug.Log("CoroutineEngine", "remove corotine : " + coroutines[i].Name);
                    coroutines.RemoveAt(i);
                    i--;
                }
            }
        }

        public void StartCoroutine(IEnumerator coroutine)
        {
            coroutines.Add(new Coroutine(coroutine));
        }
    }
           

3. Coroutine類

協程類,每一個被建立的協程都會使用一個Coroutine包裝起來,隻要反複調用Coroutine中的MoveNext方法就可以驅動疊代器進行疊代,這個類的MoveNext邏輯相對有一點複雜,我使用遞歸方式來實作,如果一下不了解,可以再回到前面檢視它的運作圖。

class Coroutine
    {
        public string Name { get; private set; }
        public Coroutine(IEnumerator enumerator)
        {
            Name = enumerator.GetType().Name;
            enumerators.Push(enumerator);
        }

        Stack<IEnumerator> enumerators = new Stack<IEnumerator>();
        public bool MoveNext()
        {
            if (enumerators.Count == 0) return false;
            return MoveNext(enumerators.Peek());
        }

        private bool MoveNext(IEnumerator it)
        {
            if(it.MoveNext())
            {
                if(it.Current is IEnumerator)
                {
                    var next = it.Current as IEnumerator;
                    enumerators.Push(next);
                    MoveNext(next);
                }
                return true;
            }
            else
            {
                enumerators.Pop();
                if (enumerators.Count == 0) return false;
                return false || MoveNext(enumerators.Peek());
            }
        }
    }

           

4. WaitForSeconds類

class WaitForSeconds : IEnumerator
    {
        private long targetTicks;
        public WaitForSeconds(float seconds)
        {
            this.targetTicks = DateTime.Now.AddSeconds(seconds).Ticks;
        }
        
        public object Current => null;

        public bool MoveNext()
        {
            return this.targetTicks > DateTime.Now.Ticks;
        }

        public void Reset()
        {
        }
    }
           

5. WaitUntil 類

class WaitUntil : IEnumerator
    {
        Func<bool> condition;
        public WaitUntil(Func<bool> condition)
        {
            if (null == condition) throw new ArgumentNullException("WaitUntil condition is null");
            this.condition = condition;
        }
        public object Current => null;

        public bool MoveNext()
        {
            return !condition();
        }

        public void Reset()
        {
            
        }
    }
           
  1. 測試類
class Program
    {
        static void Main(string[] args)
        {
            var engine = new CoroutineEngine();
            engine.StartCoroutine(TestNull());
            engine.StartCoroutine(TestWaitUntil());
            while (true)//主循環
            {
                engine.Update();
                engine.CoroutineUpdate();
                Thread.Sleep(33);
            }
        }

        static bool condition = false;
        static IEnumerator TestNull()
        {
            Debug.Log("Test Null", "1");
            yield return null;
            Debug.Log("Test Null", "2");
            yield return TestWaitForSeconds();

        }
        static IEnumerator TestWaitForSeconds()
        {
            Debug.Log("TestWaitForSeconds", "start WaitForSeconds");
            yield return new WaitForSeconds(5);
            condition = true;
            Debug.Log("TestWaitForSeconds", "stop WaitForSeconds");
            yield return 2;
        }

        static IEnumerator TestWaitUntil()
        {
            Debug.Log("TestWaitUntil", "start WaitUntil");
            yield return new WaitUntil(() => condition);
            Debug.Log("TestWaitUntil", "stop WaitUntil");
        }
    }
           

7. 測試結果

C#疊代器的實作和應用(三)——Unity的協程分析以及實作自己的協程

總結

以上就是關于C#疊代器的擴充的所有内容了,這一系列文章寫了我很久,主要還是因為準備不足,也對工作量沒有概念,好在有番茄工作法的幫助,一點一點完成了這三篇文章,以後還是要繼續努力。

繼續閱讀