Unity 協程(Coroutine)原理與用法詳解


前言:

協程在Unity中是一個很重要的概念,我們知道,在使用Unity進行游戲開發時,一般(注意是一般)不考慮多線程,那么如何處理一些在主任務之外的需求呢,Unity給我們提供了協程這種方式

為啥在Unity中一般不考慮多線程

  • 因為在Unity中,只能在主線程中獲取物體的組件、方法、對象,如果脫離這些,Unity的很多功能無法實現,那么多線程的存在與否意義就不大了

既然這樣,線程與協程有什么區別呢:

  • 對於協程而言,同一時間只能執行一個協程,而線程則是並發的,可以同時有多個線程在運行
  • 兩者在內存的使用上是相同的,共享堆,不共享棧

其實對於兩者最關鍵,最簡單的區別是微觀上線程是並行的,而協程是串行的,如果你不理解沒有關系,通過下面的解釋你就明白了

關於協程

1,什么是協程

協程,從字面意義上理解就是協助程序的意思,我們在主任務進行的同時,需要一些分支任務配合工作來達到最終的效果

稍微形象的解釋一下,想象一下,在進行主任務的過程中我們需要一個對資源消耗極大的操作時候,如果在一幀中實現這樣的操作,游戲就會變得十分卡頓,這個時候,我們就可以通過協程,在一定幀內完成該工作的處理,同時不影響主任務的進行

2,協程的原理

首先需要了解協程不是線程,協程依舊是在主線程中進行

然后要知道協程是通過迭代器來實現功能的,通過關鍵字IEnumerator來定義一個迭代方法,注意使用的是IEnumerator,而不是IEnumerable:

兩者之間的區別:

  • IEnumerator:是非泛型的,也是協程認可的參數
  • IEnumerable:通過泛型實現的迭代器,協程不使用該迭代器

在迭代器中呢,最關鍵的是yield 的使用,這是實現我們協程功能的主要途徑,通過該關鍵方法,可以使得協程的運行暫停、記錄下一次啟動的時間與位置等等:

關於迭代器的具體解釋:

由於yield 在協程中的特殊性,與關鍵性,我們到后面在單獨解釋,先介紹一下協程如何通過代碼實現

3、協程的使用

首先通過一個迭代器定義一個返回值為IEnumerator的方法,然后再程序中通過StartCoroutine來開啟一個協程即可:

在正式開始代碼之前,需要了解StartCoroutine的兩種重載方式:

  • StartCoroutine(string methodName:這種是沒有參數的情況,直接通過方法名(字符串形式)來開啟協程
  • StartCoroutine(IEnumerator routine:通過方法形式調用
  • StartCoroutine(string methodName,object values):帶參數的通過方法名進行調用

協程開啟的方式主要是上面的三種形式,如果你還是不理解,可以查看下面代碼:

 	//通過迭代器定義一個方法
 	IEnumerator Demo(int i)
    {
        //代碼塊

        yield return 0; 
		//代碼塊
       
    }

    //在程序種調用協程
    public void Test()
    {
        //第一種與第二種調用方式,通過方法名與參數調用
        StartCoroutine("Demo", 1);

        //第三種調用方式, 通過調用方法直接調用
        StartCoroutine(Demo(1));
    }

在一個協程開始后,同樣會對應一個結束協程的方法StopCoroutineStopAllCoroutines兩種方式,但是需要注意的是,兩者的使用需要遵循一定的規則,在介紹規則之前,同樣介紹一下關於StopCoroutine重載:

  • StopCoroutine(string methodName:通過方法名(字符串)來進行
  • StopCoroutine(IEnumerator routine:通過方法形式來調用
  • StopCoroutine(Coroutine routine):通過指定的協程來關閉

剛剛我們說到他們的使用是有一定的規則的,那么規則是什么呢,答案是前兩種結束協程方法的使用上,如果我們是使用StartCoroutine(string methodName)來開啟一個協程的,那么結束協程就只能使用StopCoroutine(string methodName)StopCoroutine(Coroutine routine)來結束協程,可以在文檔中找到這句話:

在這里插入圖片描述

4、關於yield

在上面,我們已經知道yield 的關鍵性,要想理解協程,就要理解yield

如果你了解Unity的腳本的生命周期,你一定對yield 這幾個關鍵詞很熟悉,沒錯,yield 也是腳本生命周期的一些執行方法,不同的yield 的方法處於生命周期的不同位置,可以通過下圖查看:

在這里插入圖片描述
通過這張圖可以看出大部分yield位置UpdateLateUpdate之間,而一些特殊的則分布在其他位置,這些yield 代表什么意思呢,又為啥位於這個位置呢


首先解釋一下位於UpdateLateUpdate之間這些yield 的含義:

  • yield return null; 暫停協程等待下一幀繼續執行

  • yield return 0或其他數字; 暫停協程等待下一幀繼續執行

  • yield return new WairForSeconds(時間); 等待規定時間后繼續執行

  • yield return StartCoroutine("協程方法名");開啟一個協程(嵌套協程)

在了解這些yield的方法后,可以通過下面的代碼來理解其執行順序:

 void Update()
    {
        Debug.Log("001");
        StartCoroutine("Demo");
        Debug.Log("003");

    }
    private void LateUpdate()
    {
        Debug.Log("005");
    }

    IEnumerator Demo()
    {
        Debug.Log("002");

        yield return 0;
        Debug.Log("004");
    }

將上面的腳本掛載到物體上,運行游戲場景,來查看打印的日志,可以看到下面的日志記錄:

在這里插入圖片描述
可以很清晰的看出,協程雖然是在Update中開啟,但是關於yield return null后面的代碼會在下一幀運行,並且是在Update執行完之后才開始執行,但是會在LateUpdate之前執行


接下來看幾個特殊的yield,他們是用在一些特殊的區域,一般不會有機會去使用,但是對於某些特殊情況的應對會很方便

  • yield return GameObject; 當游戲對象被獲取到之后執行
  • yield return new WaitForFixedUpdate():等到下一個固定幀數更新
  • yield return new WaitForEndOfFrame():等到所有相機畫面被渲染完畢后更新
  • yield break; 跳出協程對應方法,其后面的代碼不會被執行

通過上面的一些yield一些用法以及其在腳本生命周期中的位置,我們也可以看到關於協程不是線程的概念的具體的解釋,所有的這些方法都是在主線程中進行的,只是有別於我們正常使用的UpdateLateUpdate這些可視的方法

5、線程幾個小用法

5.1、將一個復雜程序分幀執行:

如果一個復雜的函數對於一幀的性能需求很大,我們就可以通過yield return null將步驟拆除,從而將性能壓力分攤開來,最終獲取一個流暢的過程,這就是一個簡單的應用

舉一個案例,如果某一時刻需要使用Update讀取一個列表,這樣一般需要一個循環去遍歷列表,這樣每幀的代碼執行量就比較大,就可以將這樣的執行放置到協程中來處理:

public class Test : MonoBehaviour
{
    public List<int> nums = new List<int> { 1, 2, 3, 4, 5, 6 };


    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            StartCoroutine(PrintNum(nums));
        }
    }
	//通過協程分幀處理
    IEnumerator PrintNum(List<int> nums)
    {
        foreach(int i in nums)
        {
            Debug.Log(i);
            yield return null;
                 
        }

    }
}

上面只是列舉了一個小小的案例,在實際工作中會有一些很消耗性能的操作的時候,就可以通過這樣的方式來進行性能消耗的分消

5.2、進行計時器工作

當然這種應用場景很少,如果我們需要計時器有很多其他更好用的方式,但是你可以了解是存在這樣的操作的,要實現這樣的效果,需要通過yield return new WaitForSeconds()的延時執行的功能:

	IEnumerator Test()
    {
        Debug.Log("開始");
        yield return new WaitForSeconds(3);
        Debug.Log("輸出開始后三秒后執行我");
    }

5.3、異步加載等功能

只要一說到異步,就必定離不開協程,因為在異步加載過程中可能會影響到其他任務的進程,這個時候就需要通過協程將這些可能被影響的任務剝離出來

常見的異步操作有:

  • AB包資源的異步加載
  • Reaources資源的異步加載
  • 場景的異步加載
  • WWW模塊的異步請求

這些異步操作的實現都需要協程的支持,可以通過我之前的一篇場景加載界面實現的文章來理解該內容:

關於異步的文章:

總結

通過上面的一些操作,相信你應該理解協程的基本原理與用法,以及一些相關的小知識

因為協程本身也是一個比較復雜的概念,所以我的理解也可能有錯誤的地方,如果你發現文章中有哪些不正確的地方,歡迎留言指出< ^ _ ^ >


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM