#遊戲unity#協程

#遊戲unity#協程

因爲對戰系統主要實現的是回合制的戰鬥,是卡牌中常用的方式;而博主從沒有製作過回合制的遊戲,所以去網上查了回合制遊戲的設計思路與整體框架,博主認爲回合制遊戲最重要的點就是交替攻擊,宏觀來看,也就是將雙方的攻擊作爲一個循環,當其中一方血量爲0時,循環終止,對戰結束。在其中,博主偶然發現了一個新鮮的玩意,可以較爲直觀簡潔的實現交替攻擊,那就是——協程!這篇博客,博主先詳細介紹協程(也都是自己的見解,可能會有些不準確,歡迎交流哦!)
一開始當我看到協程的時候,我首先認爲它是跟線程一樣的存在,是在主線程執行時進行的一條程序執行的支線,與主線程執行的沒有太大關係,就像操作系統實驗中做過的線程通信一樣的感覺。然而,在經過自己的學習與嘗試後,發現,我初次的理解是完全錯誤的。(好吧,朋友們可是忽略我上面的話了其實…)
協程並不是線程,協程是運行在主線程中的,是和主線程同步執行的代碼,不同的地方是運行的方法可以被yield return在當前幀進行打斷,到下一幀後可以繼續從被打斷的地方繼續運行。注意它是在每一幀都會被調用的。現在給出一個官網上的解釋:
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
即協程是一個分部執行,遇到條件(yield return 語句)會掛起,直到條件滿足纔會被喚醒繼續執行後面的代碼。Unity在每一幀(Frame)都會去處理對象上的協程。
Unity主要是在Update後去處理協程(檢查協程的條件是否滿足)。
接下來給大家上一個小小的實例,便於理解,場景中有一個空的GameObject對象,其綁定了下面的腳本——

using UnityEngine;
using System.Collections;

public class Test : MonoBehaviour
{
    int frame = 0;

    void Start ()
    {
        this.StartCoroutine(CountDown());
    }

    void Update ()
    {
        Debug.Log("Now is frame: " + (++frame));
    }

    IEnumerator CountDown()
    {
        Debug.Log("step - 1");
        yield return null;
        Debug.Log("step - 2");
        yield return null;
        Debug.Log("step - 3");
        yield return null;
        Debug.Log("step - 4");
    }
}

可以在控臺看到他的輸出,如下圖
這裏寫圖片描述
當進入Start方法時開始啓動協程,這時候協程開始運行,輸出“step1”後遇到第一個yield return後暫停本幀的運行,接下來進入Update方法輸出“frame1”,由於協程調用是在Update之後,所以第二幀開始後,先執行了第二個Update輸出“frame2”,然後從協程的上次暫停處繼續執行,輸出“step2”後遇到第二個yield return後暫停本幀的運行,如此反覆,當輸出“step4”後發現方法已經執行完畢,協程結束。
協程不是線程,也不是異步執行的,是Unity每幀LateUpdate()之後都會去處理的函數。
在網上找到的運行圖如下
這裏寫圖片描述
而yield break的效果會立即中斷協程的運行。從示例代碼也可以看出,協程重要的幾個語法就是yield return, IEnumerator 和 Unity StartCoroutine。
好了,博主覺得大家應該對協程有了初步的瞭解,接下來應該梳理下協程控制的語法結構啦


1.IEnumerator

IEnumerator是枚舉類的一個接口,相信接觸C#的都知道,問題是很多人分不清 IEnumerator 和 IEnumerable,如下是官網上他們的定義:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

public interface IEnumerator
{
    bool MoveNext();
    void Reset();

    Object Current { get; }
}

我覺得從代碼定義上大家也能看出它們一個明顯的區別,我的理解是——
1.IEnumerable是一個更“上層”的接口,而它的“下級”是IEnumerator接口,也就是說,離底層實現更接近的是IEnumerator。
2.IEnumerator object具體實現了遍歷器iterator(通過MoveNext(),Reset(),Current)。
3.從這兩個接口的用詞選擇上,也可以看出其不同:IEnumerable是一個聲明式的接口,聲明實現該接口的class是“可枚舉(enumerable)”的,但並沒有說明如何實現枚舉器(iterator);IEnumerator是一個實現式的接口,IEnumerator object就是一個iterator。
需要注意的一點就是隻要集合保持不變,枚舉數就將保持有效。如果對集合進行了更改(例如添加、修改或刪除元素),則該枚舉數將失效且不可恢復,並且下一次對 MoveNext 或 Reset 的調用將引發 InvalidOperationException。如果在 MoveNext 和 Current 之間修改集合,那麼即使枚舉數已經無效,Current 也將返回它所設置成的元素。
因爲枚舉集合並沒有保護好自己的數據集,也無法阻止其他線程對枚舉集合數據的修改,所以在一定程度上是不安全的,若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。

2.yield關鍵字

在迭代器塊中用於向枚舉數對象提供值或發出迭代結束信號。它的形式爲下列之一:
  

   yield return <expression_r>;
  yield break;

備註 :
  計算表達式並以枚舉數對象值的形式返回;expression_r 必須可以隱式轉換爲迭代器的 yield 類型。
  yield 語句只能出現在 iterator 塊中,該塊可用作方法、運算符或訪問器的體。這類方法、運算符或訪問器的體受以下約束的控制:
  不允許不安全塊。
  方法、運算符或訪問器的參數不能是 ref 或 out。
  yield 語句不能出現在匿名方法中。
  當和 expression_r 一起使用時,yield return 語句不能出現在 catch 塊中或含有一個或多個 catch 子句的 try 塊中。

  yield return 提供了迭代器一個比較重要的功能,即取到一個數據後馬上返回該數據,不需要全部數據裝入數列完畢,這樣有效提高了遍歷效率。
  

yield return new WaitForFixedUpdate();

等待直到下一個固定幀速率更新函數。

yield return new WaitForEndOfFrame();

等待直到所有的攝像機和GUI被渲染完成後,在該幀顯示在屏幕之前。

yield return new WaitForSeconds(1);

在給定的秒數內,暫停協同程序的執行。

3.Unity StartCoroutine

Unity使用 StartCoroutine(routine: IEnumerator): Coroutine 啓動協程,參數必須是 IEnumerator 對象。
其實,StartCoroutine(string methodName)和StartCoroutine(IEnumeratorroutine)都可以開啓一個協程,
區別:使用字符串作爲參數時,開啓協程時最多隻能傳遞一個參數,並且性能消耗會更大一點; 而使用IEnumerator 作爲參數則沒有這個限制。
具體比較深入的理解,推薦一篇博客StartCoroutine


運行時想要終止協程——
1).在Unity3D中,使用StopCoroutine(stringmethodName)來終止該MonoBehaviour指定方法名的一個協同程序,使用StopAllCoroutines()來終止所有該MonoBehaviour可以終止的協同程序。
包括StartCoroutine(IEnumerator routine)的。
2).還有一種方法可以終止協同程序,即將協同程序所在gameobject的active屬性設置爲false,當再次設置active爲ture時,協同程序並不會再開啓;
如是將協同程序所在腳本的enabled設置爲false則不會生效。
需要注意的一個小發現:

using UnityEngine;
using System.Collections;

public class TestCoroutine : MonoBehaviour {

    private bool isStartCall = false;  //Makesure Update() and LateUpdate() Log only once
    private bool isUpdateCall = false;
    private bool isLateUpdateCall = false;
    // Use this for initialization
    void Start () {
        if (!isStartCall)
        {
            Debug.Log("Start Call Begin");
            StartCoroutine(StartCoutine());
            Debug.Log("Start Call End");
            isStartCall = true;
        }

    }
    IEnumerator StartCoutine()
    {

        Debug.Log("This is Start Coroutine Call Before");
        yield return new WaitForSeconds(1f);
        Debug.Log("This is Start Coroutine Call After");

    }
    // Update is called once per frame
    void Update () {
        if (!isUpdateCall)
        {
            Debug.Log("Update Call Begin");
            StartCoroutine(UpdateCoutine());
            Debug.Log("Update Call End");
            isUpdateCall = true;
            this.enabled = false;
            //this.gameObject.SetActive(false);
        }
    }
    IEnumerator UpdateCoutine()
    {
        Debug.Log("This is Update Coroutine Call Before");
        yield return new WaitForSeconds(1f);
        Debug.Log("This is Update Coroutine Call After");
        yield return new WaitForSeconds(1f);
        Debug.Log("This is Update Coroutine Call Second");
    }
    void LateUpdate()
    {
        if (!isLateUpdateCall)
        {
            Debug.Log("LateUpdate Call Begin");
            StartCoroutine(LateCoutine());
            Debug.Log("LateUpdate Call End");
            isLateUpdateCall = true;

        }
    }
    IEnumerator LateCoutine()
    {
        Debug.Log("This is Late Coroutine Call Before");
        yield return null;
        Debug.Log("This is Late Coroutine Call After");
    }
}

先在Update中調用 this.enabled = false; 得到的結果:這裏寫圖片描述
然後把 this.enabled = false; 註釋掉,換成 this.gameObject.SetActive(false); 得到的結果如下:
這裏寫圖片描述
通過設置MonoBehaviour腳本的enabled對協程是沒有影響的,但如果 gameObject.SetActive(false) 則已經啓動的協程則完全停止了,即使在Inspector把gameObject 激活還是沒有繼續執行。
也就是說,我們可以理解爲協程函數的地位完全是跟MonoBehaviour是一個層次的,不受MonoBehaviour的狀態影響,但跟MonoBehaviour腳本一樣受gameObject 控制。
這樣解釋是不是清楚多了?推薦一篇很棒的學習博客協程理解
期待下次,開始寫回合制腳本的博客喲。

發佈了32 篇原創文章 · 獲贊 15 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章