1. 程式人生 > >Unity協程解析——狀態機實現的程式碼分步執行

Unity協程解析——狀態機實現的程式碼分步執行

Unity協程的效果

協程是一個分部執行,遇到條件(yield return 語句)會掛起,直到條件滿足才會被喚醒繼續執行後面的程式碼。

Unity在每一幀(Frame)都會去處理物件上的協程。Unity主要是在Update後去處理協程(檢查協程的條件是否滿足)

協程跟Update()其實一樣的,都是Unity每幀對會去處理的函式(如果有的話),至少是每幀的LateUpdate()後去執行。

在Unity的指令碼宣告週期中示意如下:
Unity指令碼宣告週期

Unity協程的宣告和使用

示例如下:

using UnityEngine;
using System.Collections;
 
public class Example : MonoBehaviour {
    // 保持一個執行指令碼的引用
    private IEnumerator coroutine;
 
    void Start () {
        print("Starting " + Time.time);
        //方法1:儲存協程的引用,開啟協程;
        {
            coroutine = WaitAndPrint(3.0f);
            StartCoroutine(coroutine);
        }
        //方法2:把協程的方法作為字串,開啟協程
        {
            StartCoroutine("WaitAndPrint",3.0f);
        }
        print("Done " + Time.time);
    }
 
    // 每隔3秒,列印一次
    // 由yield來掛起(暫停)函式
    public IEnumerator WaitAndPrint(float waitTime) {
        while (true) {
            yield return new WaitForSeconds(waitTime);
            print("WaitAndPrint " + Time.time);
        }
    }
}

可見,協程是通過IEnumerator實現的,每當yield return後就會掛起直到滿足條件才繼續執行後面的程式碼。

IEnumerator內部的實現

例如對於語句:

using System;  
using System.Collections;  
  
class Test  
{  
    static IEnumerator GetCounter()  
    {  
        for (int count = 0; count < 10; count++)  
        {  
            yield return count;  
        }  
    }  
}  

C#編譯器會對應生成:

internal class Test  
{  
    // Note how this doesn't execute any of our original code  
    private static IEnumerator GetCounter()  
    {  
        return new <GetCounter>d__0(0);  
    }  
  
    // Nested type automatically created by the compiler to implement the iterator  
    [CompilerGenerated]  
    private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable  
    {  
        // Fields: there'll always be a "state" and "current", but the "count"  
        // comes from the local variable in our iterator block.  
        private int <>1__state;  
        private object <>2__current;  
        public int <count>5__1;  
  
        [DebuggerHidden]  
        public <GetCounter>d__0(int <>1__state)  
        {  
            this.<>1__state = <>1__state;  
        }  
  
        // Almost all of the real work happens here  
        private bool MoveNext()  
        {  
            switch (this.<>1__state)  
            {  
                case 0:  
                    this.<>1__state = -1;  
                    this.<count>5__1 = 0;  
                    while (this.<count>5__1 < 10)//這裡針對迴圈處理  
                    {  
                        this.<>2__current = this.<count>5__1;  
                        this.<>1__state = 1;  
                        return true;  
                    Label_004B:  
                        this.<>1__state = -1;  
                        this.<count>5__1++;  
                    }  
                    break;  
  
                case 1:  
                    goto Label_004B;  
            }  
            return false;  
        } 
        ...//省略了一些方法
    }  
}  

狀態機實現程式碼分步執行

如何實現“當下次再呼叫MoveNext()方法時,我們的方法會繼續從上一個yield return語句處開始執行。”?

根據上部分C#編譯器生成的程式碼我們可以總結到:通過狀態機。

對於沒有for、while等迴圈的語句,是通過構造多個switch case語句,通過狀態切換來達到分部執行程式碼。

private bool MoveNext()
{
    switch(this.<>1_state)
    {
        case 0:
            this.<>1_state = -1;
            Console.WriteLine("第一個yield return 前的程式碼);
            this.<>2_current = 1;
            this.<>1_state = 1;
            
          case 1:
            this.<>1_state = -1;
            Console.WriteLine("第二個yield return 前的程式碼);
            this.<>2_current = 2;
            this.<>1_state = 2;
            
          case 2:
            this.<>1_state = -1;
            Console.WriteLine("第二個yield return 後的程式碼);
            break;
    }
    return false;
}

而對於迴圈語句,使用goto語句來實現狀態切換:

private bool MoveNext()
{
    switch(this.<>1_state)
    {
        case 0:
            this.<>1_state = -1;
            this.<i>5_1 = 0;
            while(this.<i>5_1 < 10)
            {
                this.<>2_current = this.<i>5_1;
                this.<>1_state = 1;
                return true;
             Label_0046:
                 this.<>1_state = -1;
                 this.<i>5_1++;
            }
            break;
          
          case 1:
              goto Label_0046;
    }
    return false;
}

參考前文中的圖Unity指令碼生命週期,Unity在每幀都會呼叫某個指令碼上的協程,即呼叫協程的MoveNext函式,當MoveNext函式返回false則結束協程,否則如果返回 true ,就從當前位置繼續往下執行。

參考

  1. 《Unity 3D指令碼程式設計 使用C#語言開發跨平臺遊戲》第八章
  2. Unity3D協程進階-原理剖析: https://blog.csdn.net/yupu56/article/details/52791952