【Unity3D基礎教程】給初學者看的Unity教程(五):詳解Unity3D中的協程(Coroutine)
為什麼需要協程
在遊戲中有許多過程(Process)需要花費多個邏輯幀去計算。
- 你會遇到“密集”的流程,比如說尋路,尋路計算量非常大,所以我們通常會把它分割到不同的邏輯幀去進行計算,以免影響遊戲的幀率。
- 你會遇到“稀疏”的流程,比如說遊戲中的觸發器,這種觸發器大多數時候什麼也不做,但是一旦被呼叫會做非常重要的事情(比圖說遊戲中自動開啟的門就是在門前放了一個Empty Object作為trigger,人到門前就會觸發事件)。
不管什麼時候,如果你想建立一個能夠歷經多個邏輯幀的流程,但是卻不使用多執行緒,那你就需要把一個任務來分割成多個任務,然後在下一幀繼續執行這個任務。
比如,A*演算法是一個擁有主迴圈的演算法,它擁有一個open list來記錄它沒有處理到的節點,那麼我們為了不影響幀率,可以讓A*演算法在每個邏輯幀中只處理open list中一部分節點,來保證幀率不被影響(這種做法叫做time slicing)。
再比如,我們在處理網路傳輸問題時,經常需要處理非同步傳輸,需要等檔案下載完畢之後再執行其他任務,一般我們使用回撥來解決這個問題,但是Unity使用協程可以更加自然的解決這個問題,如下邊的程式:
private IEnumerator Test() { WWW www = new WWW(ASSEST_URL);yield return www; AssetBundle bundle =
協程是什麼
從程式結構的角度來講,協程是一個有限狀態機,這樣說可能並不是很明白,說到協程(Coroutine),我們還要提到另一樣東西,那就是子例程(Subroutine),子例程一般可以指函式,函式是沒有狀態的,等到它return之後,它的所有區域性變數就消失了,但是在協程中我們可以在一個函式裡多次返回,區域性變數被當作狀態儲存在協程函式中,知道最後一次return,協程的狀態才別清除。
簡單來說,協程就是:你可以寫一段順序的程式碼,然後標明哪裡需要暫停,然後在下一幀或者一段時間後,系統會繼續執行這段程式碼。
協程怎麼用?
一個簡單的C#程式碼,如下:
IEnumerator LongComputation() { while(someCondition) { /* 做一系列的工作 */ // 在這裡暫停然後在下一幀繼續執行
yield return null; } }
協程是怎麼工作的
注意上邊的程式碼示例,你會發現一個協程函式的返回值是IEnumerator,它是一個迭代器,你可以把它當成指向一個序列的某個節點的指標,它提供了兩個重要的介面,分別是Current(返回當前指向的元素)和MoveNext()(將指標向前移動一個單位,如果移動成功,則返回true)。IEnumerator是一個interface,所以你不用擔心的具體實現。
通常,如果你想實現一個介面,你可以寫一個類,實現成員,等等。迭代器塊(iterator block)是一個方便的方式實現IEnumerator沒有任何麻煩-你只是遵循一些規則,並實現IEnumerator由編譯器自動生成。
一個迭代器塊具備如下特徵:
- 返回IEnumerator
- 使用yield關鍵字
所以yield關鍵詞是幹啥的?它宣告序列中的下一個值或者是一個無意義的值。如果使用yield x(x是指一個具體的物件或數值)的話,那麼movenext返回為true並且current被賦值為x,如果使用yield break使得movenext()返回false。
那麼我舉例如下,這是一個迭代器塊:
public void Consumer() { foreach(int i in Integers()) { Console.WriteLine(i.ToString()); } } public IEnumerable<int> Integers() { yield return 1; yield return 2; yield return 4; yield return 8; yield return 16; yield return 16777216; }
注意上文在迭代的過程中,你會發現,在兩個yield之間的程式碼只有執行完畢之後,才會執行下一個yield,在Unity中,我們正是利用了這一點,我們可以寫出下面這樣的程式碼作為一個迭代器塊:
IEnumerator TellMeASecret(){ PlayAnimation("LeanInConspiratorially"); while(playingAnimation) yield return null; Say("I stole the cookie from the cookie jar!"); while(speaking) yield return null; PlayAnimation("LeanOutRelieved"); while(playingAnimation) yield return null; }
然後我們可以使用下文這樣的客戶程式碼,來呼叫上文的程式,就可以實現延時的效果。
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { // do whatever you like }
協程是如何實現延時的?
如你所見,yield return返回的值並不一定是有意義的,如null,但是我們更感興趣的是,如何使用這個yield return的返回值來實現一些有趣的效果。
Unity聲明瞭YieldInstruction來作為所有返回值的基類,並且提供了幾種常用的繼承類,如WaitForSeconds(暫停一段時間繼續執行),WaitForEndOfFrame(暫停到下一幀繼續執行)等等。更巧妙的是yield 也可以返回一個Coroutine真身,Coroutine A返回一個Coroutine B本身的時候,即等到B做完了再執行A。下面有詳細說明:
Normal coroutine updates are run after the Update function returns. A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes. Different uses of Coroutines: yield; The coroutine will continue after all Update functions have been called on the next frame. yield WaitForSeconds(2); Continue after a specified time delay, after all Update functions have been called for the frame yield WaitForFixedUpdate(); Continue after all FixedUpdate has been called on all scripts yield WWW Continue after a WWW download has completed. yield StartCoroutine(MyFunc); Chains the coroutine, and will wait for the MyFunc coroutine to complete first.
實現延時的關鍵程式碼是在StartCoroutine裡面,以為筆者也沒有見過Unity的原始碼,那麼我只能猜想StartCoroutine這個函式的內部構造應該是這樣的:
List<IEnumerator> unblockedCoroutines; List<IEnumerator> shouldRunNextFrame; List<IEnumerator> shouldRunAtEndOfFrame; SortedList<float, IEnumerator> shouldRunAfterTimes; foreach(IEnumerator coroutine in unblockedCoroutines){ if(!coroutine.MoveNext()) // This coroutine has finished continue; if(!coroutine.Current is YieldInstruction) { // This coroutine yielded null, or some other value we don't understand; run it next frame. shouldRunNextFrame.Add(coroutine); continue; } if(coroutine.Current is WaitForSeconds) { WaitForSeconds wait = (WaitForSeconds)coroutine.Current; shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine); } else if(coroutine.Current is WaitForEndOfFrame) { shouldRunAtEndOfFrame.Add(coroutine); } else /* similar stuff for other YieldInstruction subtypes */} unblockedCoroutines = shouldRunNextFrame;
還有些更好玩的?
第一個有趣的地方是,yield return可以返回任意YieldInstruction,所以我們可以在這裡加上一些條件判斷:
YieldInstruction y; if(something) y = null;else if(somethingElse) y = new WaitForEndOfFrame();else y = new WaitForSeconds(1.0f); yield return y;
第二個,由於一個協程只是一個迭代器塊而已,所以你也可以自己遍歷它,這在一些場景下很有用,例如在對協程是否執行加上條件判斷的時候:
IEnumerator DoSomething(){ /* ... */} IEnumerator DoSomethingUnlessInterrupted(){ IEnumerator e = DoSomething(); bool interrupted = false; while(!interrupted) { e.MoveNext(); yield return e.Current; interrupted = HasBeenInterrupted(); }}
第三個,由於協程可以yield協程,所以我們可以自己建立一個協程函式,如下:
IEnumerator UntilTrueCoroutine(Func fn){ while(!fn()) yield return null;} Coroutine UntilTrue(Func fn){ return StartCoroutine(UntilTrueCoroutine(fn));} IEnumerator SomeTask(){ /* ... */ yield return UntilTrue(() => _lives < 3); /* ... */}
總結
這篇文章大部分是我從這篇部落格裡面翻譯過來的,這是我見過最棒的一篇關於Coroutine的部落格,所以我把它翻譯過來與大家分享,希望你能喜歡。
相關推薦
【Unity3D基礎教程】給初學者看的Unity教程(零):如何學習Unity3D
cos 詳解 component lock index unity3d遊戲 design 技術棧 log 【Unity3D基礎教程】給初學者看的Unity教程(零):如何學習Unity3D http://www.cnblogs.com/neverdie/p/How_To_
【Unity3D基礎教程】給初學者看的Unity教程(六):理解Unity的新GUI系統(UGUI)
理解UGUI的基礎架構 UGUI是Unity在4.6中引入的新的GUI系統,與傳統的中介軟體NGUI相比,這套新GUI系統有幾個核心亮點: 放棄了Atlas的概念,使用Packing Tag的方式來進行圖集的規劃 放棄了depth來確定UI顯示層級的概念,使用Hierarchy的SiblingIndex
【Unity3D基礎教程】給初學者看的Unity教程(二):所有指令碼元件的基類 -- MonoBehaviour的前世今生
引子 上一次我們講了GameObject,Compoent,Time,Input,Physics,其中Time,Input,Physics都是Unity中的全域性變數。GameObject是遊戲中的基本物件。GameObject是由Component組合而成的,GameObject本身必須有
【Unity3D基礎教程】給初學者看的Unity教程(五):詳解Unity3D中的協程(Coroutine)
為什麼需要協程 在遊戲中有許多過程(Process)需要花費多個邏輯幀去計算。 你會遇到“密集”的流程,比如說尋路,尋路計算量非常大,所以我們通常會把它分割到不同的邏輯幀去進行計算,以免影響遊戲的幀率。 你會遇到“稀疏”的流程,比如說遊戲中的觸發器,這種觸發器大多數時候什麼也不做,但
【Unity3D基礎教程】給初學者看的Unity教程(三):通過製作Flappy Bird瞭解Native 2D中的Sprite,Animation
引子 上一次我們講了MonoBehaviour的前世今生,瞭解了遊戲中的每一個GameObjec都是由指令碼控制的,這一次我們開始將Unity中Native 2D中的Sprite,並且使用Animation來讓Sprite動起來。 在接下來的幾篇部落格裡,我會通過做一個Flappy Bird來講解
【Unity3D基礎教程】給初學者看的Unity教程(四):通過製作Flappy Bird瞭解Native 2D中的RigidBody2D和Collider2D
引子 認識RigidBody 當RigidBody2D的質量屬性被設定為0時,剛體的質量變為無限大,此時剛體相當於靜態剛體,永遠一動不動。但是在Unity中你是無法把一個RigidBody2D的質量設定為0的,所以,當你想建立一個靜態剛體時,只需要建立碰撞器,而不需要建立RigidBo
【Unity3D基礎教程】給初學者看的Unity教程(七):在Unity中構建健壯的單例模式(Singleton)
該部落格中的程式碼均出自我的開源專案 : 迷你微信 為什麼需要單例模式 遊戲中需要單例有以下幾個原因: 我們需要在遊戲開始前和結束前做一些操作,比如網路的連結和斷開,資源的載入和解除安裝,我們一般會把這部分邏輯放在單例裡。 單例可以控制初始化和銷燬順序,而靜態變數和場景中的GameObject都無法控制
【Unity3D基礎教程】給初學者看的Unity教程(一):GameObject,Compoent,Time,Input,Physics
Unity3D重要模組的類圖 最近剛剛完成了一個我個人比較滿意的小專案:【深入Cocos2d-x】使用MVC架構搭建遊戲Four,在這個遊戲中,我使用了自己搭建的MVC架構來製作一個遊戲,做到了比較好的SoC(關注點分離)。但是苦於Cocos2d-x沒有一個比較完善的編輯器,所以我開始學習另一個非常流行
【庫函式版本】基於STM32F103的MPU6050的原始資料讀取程式詳解
因為我的部落格已經對I2C協議的詳細過程已經做了一個例子!所以這個MPU6050的程式我將使用庫函式完成! 第一步:硬體連線: 第二步:初始化I2C埠的函式: /***PB6/PB7 埠初始化****/ static void I2C_GPIO_Config(voi
Senparc.Weixin.MP SDK 微信公眾平臺開發教程(二十一):在小程式中使用 WebSocket (.NET Core)
本文將介紹如何在 .NET Core 環境下,藉助 SignalR 在小程式內使用 WebSocket。關於 WebSocket 和 SignalR 的基礎理論知識不在這裡展開,已經有足夠的參考資料,例如參考 SignalR 的官方教程:https://docs.microsoft.com/zh-cn/a
【給小白看的Java教程】第二十六章,可勝列舉:列舉
###列舉的誕生歷史(瞭解) 在服裝行業,衣服的分類根據性別可以表示為三種情況:男裝、女裝、中性服裝。 private ? type; public void setType(? type){ this.type = type } 需求:定義一個變數來表
【翻譯】給初學者的 Neural Networks / 神經網路 介紹
在這篇文章中,我會向大家簡要的介紹下 Neural Networks / 神經網路; 可以作為 Machine Learning / 機器學習 和 Deep Learning / 深度學習 的入門愛好者參考; 我們文章中會盡量用簡短的,零基礎的方式來向大家介紹。 作為 Black Box /
【計算機基礎知識】【JDK環境安裝配置】JDK安裝教程
目錄 一、JDK安裝教程 點開連結你應該看到如下圖所示的介面: 2.點選上圖中箭頭所指的地方,會出現下面的這個介面,此時你需要根據你的電腦系統來進行對應的版本進行選擇,在選擇版本和下載之前你需要首先接收協議,具體介面如下圖所示: 3.雙擊以後進行J
【redis 基礎學習】(六)Redis HyperLogLog
內存 ger detail edi 統計 固定 ogl per ren 摘自:http://www.mayou18.com/detail/o6M0v9mi.html Redis HyperLogLog 結構講解 Redis 在 2.8.9 版本添加了 HyperLog
【Java基礎總結】反射
cto 調用構造 lan 調用方法 arm tde ins java 傳遞數據 1. 什麽是反射 Class、Method、Field、Constructor,它們是反射對象。它們是類、方法、成員變量、構造器,在內存中的形式。 也就是萬物皆對象!類是類型、方法是類型、成
【Java基礎總結】多線程
none 加載 引用 rup 什麽 true 執行過程 lose 好處 1. java中實現多線程的兩種方式 1 //第一種:繼承Thread類,重寫run()方法 2 class ThreadTest1 extends Thread{ 3 publ
【Java基礎總結】字符串
col contains pan nbsp index split 內存區域 反轉 bytes 1. java內存區域(堆區、棧區、常量池) 2. String length() //長度 //獲取子串位置 indexOf(subStr) las
【Python基礎 day21】random & 序列化 & 異常處理 & Os
lena 否則 ive 參數 isf pen before 自動 pyc random模塊:選擇功能只能對序列類型進行叠代 >>> import random #隨機生成小數 >>> random.random()#random.ran
【linux操作系統】linux查看有哪些用戶
http 分享圖片 lin img /etc/ gpo word ima clas linux系統如何查看有哪些用戶? 命令:cat /etc/passwd(不是password) 【linux操作系統】linux查看有哪些用戶
【Lua基礎學習】微信三公源碼搭建---Lua基礎數據類型
浮點 賦值 源碼 boolean 語言 pos 線路 mce blog 微信三公源碼搭建Q1446595067 官網:h5.haozibbs.com 數據類型 介紹 Lua是動態類型語言,變量不要類型定義,只需要為變量賦值。 值可以存儲在變量中,作為參數傳遞或結果返回。 L