日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Unity 协程原理探究与实现

發布時間:2023/12/10 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Unity 协程原理探究与实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

  • 一、介紹
  • 二、迭代器
  • 三、原理
  • 四、總結

一、介紹

協程Coroutine在Unity中一直扮演者重要的角色。可以實現簡單的計時器、將耗時的操作拆分成幾個步驟分散在每一幀去運行等等,用起來很是方便。
但是,在使用的過程中有沒有思考過協程是怎么實現的?為什么可以將一段代碼分成幾段在不同幀執行?
本篇文章將從實現原理上更深入的理解協程,最后肯定也要實現我們自己的協程。
關于協程的用法網上有很多介紹,不清楚的話可以看下官方文檔,這里不做贅述。

二、迭代器

在使用協程的時候,我們總是要聲明一個返回值為IEnumerator的函數,并且函數中會包含yield return xxx或者yield break之類的語句。就像文檔里寫的這樣

private IEnumerator WaitAndPrint(float waitTime) {yield return new WaitForSeconds(waitTime);print("Coroutine ended: " + Time.time + " seconds"); }

想要理解IEnumerator和yield就不得不說一下迭代器。迭代器是C#中一個十分強大的功能,只要類繼承了IEnumerable接口或者實現了GetEnumerator()方法就可以使用foreach去遍歷類,遍歷輸出的結果是根據GetEnumerator()的返回值IEnumerator確定的,為了實現IEnumerator接口就不得不寫一堆繁瑣的代碼,而yield關鍵字就是用來簡化這一過程的。是不是很繞,理解這些內容需要花些時間。
不理解也沒關系,目前只需要明白一件事,當在IEnumerator函數中使用yield return語句時,每使用一次,迭代器中的元素內容就會增加一個。就向往列表中添加元素一樣,每Add一次元素內容就會多一個。
先來看看下面這段簡單的代碼

IEnumerator TestCoroutine() {yield return null; //返回內容為nullyield return 1; //返回內容為1yield return "sss"; //返回內容為"sss"yield break; //跳出,類似普通函數中的return語句yield return 999; //由于break語句,該內容無法返回 }void Start() {IEnumerator e = TestCoroutine();while (e.MoveNext()){Debug.Log(e.Current); //依次輸出枚舉接口返回的值} } /* 枚舉接口的定義 public interface IEnumerator {object Current{get;}bool MoveNext();void Reset(); }*//*運行結果: Null 1 sss */

首先注意注釋部分枚舉接口的定義
Current屬性為只讀屬性,返回枚舉序列中的當前位的內容
MoveNext()把枚舉器的位置前進到下一項,返回布爾值,新的位置若是有效的,返回true;否則返回false
Reset()將位置重置為原始狀態

再看下Start函數中的代碼,就是將yield return 語句中返回的值依次輸出。
第一次MoveNext()后,Current位置指向了yield return 返回的null,該位置是有效的(這里注意區分位置有效和結果有效,位置有效是指當前位置是否有返回值,即使返回值是null;而結果有效是指返回值的結果是否為null,顯然此處返回結果是無意義的)所以MoveNext()返回值是true;
第二次MoveNext()后,Current新位置指向了yield return 返回的1,該位置是有效的,MoveNext()返回true
第三次MoveNext()后,Current新位置指向了yield return 返回的"sss",該位置也是有效的,MoveNext()返回true
第四次MoveNext()后,Current新位置指向了yield break,無返回值,即位置無效,MoveNext()返回false,至此循環結束

最后輸出的運行結果跟我們分析是一致的。關于C#是如何實現迭代器的功能,有興趣的可以看下容器類源碼中關于迭代器部分的實現就明白了,MSDN上也有關于迭代器的詳細講解。

三、原理

先來回顧下Unity的協程具體有些功能:

  • 將協程代碼中由yield return語句分割的部分分配到每一幀去執行。
  • yield return 后的值是等待類(WaitForSeconds、WaitForFixedUpdate)時需要等待相應時間。
  • yield return 后的值還是協程(Coroutine)時需要等待嵌套部分協程執行完畢才能執行接下來內容。
  • // case 1 IEnumerator Coroutine1() {//do something xxx //假如是第N幀執行該語句yield return 1; //等一幀//do something xxx //則第N+1幀執行該語句 }// case 2 IEnumerator Coroutine2() {//do something xxx //假如是第N秒執行該語句yield return new WaitForSeconds(2f); //等兩秒 //do something xxx //則第N+2秒執行該語句 }// case 3 IEnumerator Coroutine3() {//do something xxxyield return StartCoroutine(Coroutine1()); //等協程Coroutine1執行完 //do something xxx }

    好了,知道了IEnumerator函數和yield return語法之后,在看到上面幾個協程的功能,是不是對如何實現協程有點頭緒了?

    case1 : 分幀

    實現分幀執行之前,先將上述迭代器的代碼簡單修改下,看下輸出結果

    IEnumerator TestCoroutine() {Debug.Log("TestCoroutine 1");yield return null;Debug.Log("TestCoroutine 2");yield return 1; }void Start() {IEnumerator e = TestCoroutine();while (e.MoveNext()){Debug.Log(e.Current); //依次輸出枚舉接口返回的值} } /*運行結果 TestCoroutine 1 Null TestCoroutine 2 1 */

    前面有說過,每次MoveNext()后會返回yield return后的內容,那yield return之前的語句怎么辦呢?
    當然也執行啊,遇到yield return語句之前的內容都會在MoveNext()時執行的。
    到這里應該很清楚了,只要把MoveNext()移到每一幀去執行,不就實現分幀執行幾段代碼了么!

    既然要分配在每一幀去執行,那當然就是Update和LateUpdate了。這里我個人喜歡將實現代碼放在LateUpdate之中,為什么呢?因為Unity中協程的調用順序是在Update之后,LateUpdate之前,所以這兩個接口都不夠準確;但在LateUpdate中處理,至少能保證協程是在所有腳本的Update執行完畢之后再去執行。

    現在可以實現最簡單的協程了

    IEnumerator e = null; void Start() {e = TestCoroutine(); }void LateUpdate() {if (e != null){if (!e.MoveNext()){e = null;}} }IEnumerator TestCoroutine() {Log("Test 1");yield return null; //返回內容為nullLog("Test 2");yield return 1; //返回內容為1Log("Test 3");yield return "sss"; //返回內容為"sss"Log("Test 4");yield break; //跳出,類似普通函數中的return語句Log("Test 5");yield return 999; //由于break語句,該內容無法返回 }void Log(object msg) {Debug.LogFormat("<color=yellow>[{0}]</color>{1}", Time.frameCount, msg.ToString()); }


    再來看看運行結果,黃色中括號括起來的數字表示當前在第幾幀,很明顯我們的協程完成了每一幀執行一段代碼的功能。

    case2: 延時等待

    要是完全理解了case1的內容,相信你自己就能完成“延時等待”這一功能,其實就是加了個計時器的判斷嘛!
    既然要識別自己的等待類,那當然要獲取Current值根據其類型去判定是否需要等待。假如Current值是需要等待類型,那就延時到倒計時結束;而Current值是非等待類型,那就不需要等待,直接MoveNext()執行后續的代碼即可。
    這里著重說下“延時到倒計時結束”。既然知道Current值是需要等待的類型,那此時肯定不能在執行MoveNext()了,否則等待就沒用了;接下來當等待時間到了,就可以繼續MoveNext()了。可以簡單的加個標志位去做這一判斷,同時驅動MoveNext()的執行。

    private void OnGUI() {if (GUILayout.Button("Test")) //注意:這里是點擊觸發,沒有放在start里,為什么?{enumerator = TestCoroutine();} }void LateUpdate() {if (enumerator != null){bool isNoNeedWait = true, isMoveOver = true;var current = enumerator.Current;if (current is MyWaitForSeconds){MyWaitForSeconds waitable = current as MyWaitForSeconds;isNoNeedWait = waitable.IsOver(Time.deltaTime);}if (isNoNeedWait){isMoveOver = enumerator.MoveNext();}if (!isMoveOver){enumerator = null;}} }IEnumerator TestCoroutine() {Log("Test 1");yield return null; //返回內容為nullLog("Test 2");yield return 1; //返回內容為1Log("Test 3");yield return new MyWaitForSeconds(2f); //等待兩秒 Log("Test 4"); }


    運行結果里黃色表示當前幀,青色是當前時間,很明顯等待了2秒(雖然有少許誤差但總體不影響)。
    上述代碼中,把函數觸發放在了Button點擊中而不是Start函數中?
    這是因為我是用Time.deltaTime去做計時,假如放在了Start函數中,Time.deltaTime會受Awake這一幀執行時間影響,時間還不短(我測試時有0.1s左右),導致運行結果有很大誤差,不到2秒就結束了,有興趣的可以自己試一下~

    case3: 協程嵌套等待

    協程嵌套等待也就是下面這種樣子,在實際情況中使用的也不少。

    IEnumerator Coroutine1() {//do something xxxyield return null;//do something xxxyield return StartCoroutine(Coroutine2()); //等待Coroutine2執行完畢//do something xxxyield return 3; }IEnumerator Coroutine2() {//do something xxxyield return null;//do something xxxyield return 1;//do something xxxyield return 2; }

    實現原理的話基本與延時等待完全一致,這里我就不貼例子代碼了,最后會放出完整工程的。
    需要注意下協程嵌套時的執行順序,先執行完內層嵌套代碼再執行外層內容;即更新結束條件時要先更新內層協程(上例Coroutine2)在更新外層協程(上例Coroutine1)。

    四、總結

    前一節只是把每塊內容的原理用例子代碼實現了一下,實際使用中這樣肯定不行,需要更通用的接口。
    我按照Unity的接口方式把上述這些功能用相同名稱封裝了一下,并做了一些測試樣例與Unity原生接口運行結果作對比
    下圖是最后一個測試樣例的代碼和運行結果,可以看出表現是完全一致的。

    //Hi是命名空間 private void OnGUI() {GUILayout.BeginHorizontal();if (GUILayout.Button("自己 嵌套的協程")){Hi.CoroutineMgr.Instance.StartCoroutine(TestNesting());}GUILayout.Space(20);if (GUILayout.Button("Unity 嵌套的協程")){StartCoroutine(UnityNesting());}GUILayout.EndHorizontal(); }IEnumerator TestNesting() {Log("Nesting 1");yield return Hi.CoroutineMgr.Instance.StartCoroutine(TestNesting__());Log("Nesting 2"); }IEnumerator TestNesting__() {Log("Nesting__ 1");yield return Hi.CoroutineMgr.Instance.StartCoroutine(TestNormalCoroutine());Log("Nesting__ 2");yield return Hi.CoroutineMgr.Instance.StartCoroutine(TestWaitFor());Log("Nesting__ 3"); }IEnumerator UnityNesting() {LogWarn("UnityNesting 1");yield return StartCoroutine(UnityTesting__());LogWarn("UnityNesting 2"); }IEnumerator UnityTesting__() {LogWarn("UnityTesting__ 1");yield return StartCoroutine(UnityNormalCoroutine());LogWarn("UnityTesting__ 2");yield return StartCoroutine(UnityWaitFor());LogWarn("UnityTesting__ 3"); }void Log(string message) {Debug.LogFormat("<color=yellow>[{0}]</color>-<color=cyan>[{1}]</color>{2}", Time.frameCount,System.DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), message); }void LogWarn(string message) {Debug.LogWarningFormat("<color=yellow>[{0}]</color>-<color=cyan>[{1}]</color>{2}",Time.frameCount, System.DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), message); }

    最后放上工程地址GitHub。目前只是實現了常用的部分接口,足以滿足日常使用,但像停止協程接口還未實現(后續會補上),感興趣的可以自己完善。本篇文章有什么問題歡迎大家討論、指出~~~

    轉載于:https://www.cnblogs.com/yespi/p/9847533.html

    總結

    以上是生活随笔為你收集整理的Unity 协程原理探究与实现的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。