Unity3D協程(轉)
這篇文章轉自:http://blog.csdn.net/huang9012/article/details/38492937
協程介紹
在Unity中,協程(Coroutines)的形式是我最喜歡的功能之一,幾乎在所有的專案中,我都會使用它來控制運動,序列,以及物件的行為。在這個教程中,我將會說明協程是如何工作的,並且會附上一些例子來介紹它的用法。
協程介紹
Unity的協程系統是基於C#的一個簡單而強大的介面 ,IEnumerator,它允許你為自己的集合型別編寫列舉器。這一點你不必關注太多,我們直接進入一個簡單的例子來看看協程到底能幹什麼。首先,我們來看一下這段簡單的程式碼...
倒計時器
這是一個簡單的指令碼元件,只做了倒計時,並且在到達0的時候log一個資訊。
using Unity Engine; using System.Collections; public class Countdown : MonoBehaviour { public float timer = 3; void Update() { { timer -= Time.deltaTime; if(timer <= 0) Debug.Log("Timer has finished!"); } }
using Unity Engine; using System.Collections; public class Countdown : MonoBehaviour { public float timer = 3; void Update() { timer -= Time.deltaTime; if(timer <= 0) Debug.Log("Timer has finished!"); } }
還不錯,程式碼簡短實用,但問題是,如果我們需要複雜的指令碼元件(像一個角色或者敵人的類),擁有多個計時器呢?剛開始的時候,我們的程式碼也許會是這樣的:
using UnityEngine; using System.Collections; public class MultiTimer : MonoBehaviour { public float firstTimer = 3; public float secondTimer = 2; public float thirdTimer = 1; void Update() { firstTimer -= Time.deltaTime; if(firstTimer <= 0) Debug.Log("First timer has finished!"); secondTimer -= Time.deltaTime; if(secondTimer <= 0) Debug.Log("Second timer has finished!"); thirdTimer -= Time.deltaTime; if(thirdTimer <= 0) Debug.Log("Third timer has finished!"); } }
-
儘管不是太糟糕,但是我個人不是很喜歡自己的程式碼中充斥著這些計時器變數,它們看上去很亂,而且當我需要重新開始計時的時候還得記得去重置它們(這活我經常忘記做)。
如果我只用一個for迴圈來做這些,看上去是否會好很多?
for(float timer = 3; timer >= 0; timer -= Time.deltaTime) { //Just do nothing... } Debug.Log("This happens after 5 seconds!");
現在每一個計時器變數都成為for迴圈的一部分了,這看上去好多了,而且我不需要去單獨設定每一個跌倒變數。
好的,你可能現在明白我的意思:協程可以做的正是這一點!
碼入你的協程!
現在,這裡提供了上面例子運用協程的版本!我建議你從這裡開始跟著我來寫一個簡單的指令碼元件,這樣你可以在你自己的程式中看到它是如何工作的。
using UnityEngine; using System.Collections; public class CoroutineCountdown : MonoBehaviour { void Start() { StartCoroutine(Countdown()); } IEnumerator Countdown() { for(floattimer = 3; timer >= 0; timer -= Time.deltaTime) Yield return 0; Debug.Log("This message appears after 3 seconds!"); } }
這看上去有點不一樣,沒關係,接下來我會解釋這裡到底發生了什麼。
StartCoroutine(Countdown());
- 這一行用來開始我們的Countdown程式,注意,我並沒有給它傳入引數,但是這個方法呼叫了它自己(這是通過傳遞Countdown的return返回值來實現的)。
Yield
在Countdown方法中其他的都很好理解,除了兩個部分:
- l IEnumerator 的返回值
- l For迴圈中的yield return
為了能在連續的多幀中(在這個例子中,3秒鐘等同於很多幀)呼叫該方法,Unity必須通過某種方式來儲存這個方法的狀態,這是通過IEnumerator 中使用yield return語句得到的返回值,當你“yield”一個方法時,你相當於說了,“現在停止這個方法,然後在下一幀中從這裡重新開始!”。
注意:用0或者null來yield的意思是告訴協程等待下一幀,直到繼續執行為止。
當然,同樣的你可以繼續yield其他協程,我會在下一個教程中講到這些。
一些例子
協程在剛開始接觸的時候是非常難以理解的,無論是新手還是經驗豐富的程式設計師我都見過他們對於協程語句一籌莫展的時候。因此我認為通過例子來理解它是最好的方法。
多次輸出“Hello”
記住,yield return是“停止執行方法,並且在下一幀從這裡重新開始”,這意味著你可以這樣做:
//This will say hello 5 times, once each frame for 5 frames IEnumerator SayHelloFiveTimes() { Yield return 0; Debug.Log("Hello"); Yield return 0; Debug.Log("Hello"); Yield return 0; Debug.Log("Hello"); Yield return 0; Debug.Log("Hello"); Yield return 0; Debug.Log("Hello"); }
//This will do the exact same thing as the above function! IEnumerator SayHello5Times() { for(inti = 0; i < 5; i++) { Debug.Log("Hello"); Yield return 0; } }
-
每一幀輸出“Hello”,無限迴圈。。。通過在一個while迴圈中使用yield,你可以得到一個無限迴圈的協程,這幾乎就跟一個Update()迴圈等同。。。
//Once started, this will run until manually stopped or the object is destroyed IEnumerator SayHelloEveryFrame() { while(true) { //1. Say hello Debug.Log("Hello"); //2. Wait until next frame Yield return 0; }//3. This is a forever-loop, goto 1 }
-
計時
不過跟Update()不一樣的是,你可以在協程中做一些更有趣的事:
//This will do the exact same thing as the above function! IEnumerator SayHello5Times() { isUpdate = true; for (int i = 0; i < 5; i++) { Debug.Log("Hello"); yield return 0; } isUpdate = false; }
-
這個方法突出了協程一個非常酷的地方:
方法的狀態被儲存了,這使得方法中定義的這些變數都會儲存它們的值,即使是在不同的幀中。
還記得這個教程開始時那些煩人的計時器變數嗎?通過協程,我們再也不需要擔心它們了,只需要把變數直接放到方法裡面!
開始和終止協程
之前,我們已經學過了通過 StartCoroutine()方法來開始一個協程,就像這樣:
StartCoroutine(Countdown());
如果我們想要終止所有的協程,可以通過StopAllCoroutines()方法來實現,它的所要做的就跟它的名字所表達的一樣。注意,這隻會終止在呼叫該方法的物件中(應該是指呼叫這個方法的類吧)開始的協程,對於其他的MonoBehavior類中執行的協程不起作用。如果我們有以下這樣兩條協程語句:
StartCoroutine(FirstTimer()); StartCoroutine(SecondTimer());
- 那我們怎麼終止其中的一個協程呢?在這個例子裡,這是不可能的,如果你想要終止某一個特定的協程,那麼你必須得在開始協程的時候將它的方法名作為字串,就像這樣:
//If you start a Coroutine by name... StartCoroutine("FirstTimer"); StartCoroutine("SecondTimer"); //You can stop it anytime by name! StopCoroutine("FirstTimer");
更多關於協程的學習
即將為你帶來:“Scripting with Coroutines”,一個更深入的介紹,關於如何使用協程以及如何通過協程編寫物件行為。
擴充套件連結
l Coroutines – Unity Script Reference
第二部分
這個關於協程的教程共有兩部分,這是第二部分,如果您未曾看過第一部分——協程介紹,那麼在閱讀這部分內容之前建議您先了解一下。
計時器例子
在第一個教程中,我們已經瞭解了協程如何讓一個方法“暫停”下來,並且讓它yield直到某些值到達我們給定的數值;並且利用它,我們還建立了一個很棒的計時器系統。協程一個很重要的內容是,它可以讓普通的程式(比方說一個計時器)很容易地被抽象化並且被複用。
協程的引數
抽象化一個協程的第一個方法是給它傳遞引數,協程作為一個函式方法來說,它自然能夠傳遞引數。這裡有一個協程的例子,它在特定的地方輸出了特定的資訊。
Using UnityEngine; Using System.Collections; Public class TimerExample : MonoBehaviour { Void Start() { //Log "Hello!" 5 times with 1 second between each log StartCoroutine(RepeatMessage(5, 1.0f,"Hello!")); } IEnumerator RepeatMessage(int count,float frequency,string message) { for(int i = 0; i < count; i++) { Debug.Log(message); for(float timer = 0; timer < frequency; timer += Time.deltaTime) Yield return 0; } } }
-
巢狀的協程
在此之前,我們yield的時候總是用0(或者null),僅僅告訴程式在繼續執行前等待下一幀。
協程最強大的一個功能就是它們可以通過使用yield語句來相互巢狀。
眼見為實,我們先來建立一個簡單的Wait()程式,不需要它做任何事,只需要在執行的時候等待一段時間就結束。
IEnumerator Wait(float duration) { for(float timer = 0; timer < duration; timer += Time.deltaTime) Yield return 0; }
接下來我們要編寫另一個協程,如下:
Using UnityEngine; Using System.Collections; Public class TimerExample : MonoBehaviour { voidStart() { StartCoroutine(SaySomeThings()); } //Say some messages separated by time IEnumerator SaySomeThings() { Debug.Log("The routine has started"); Yield return StartCoroutine(Wait(1.0f)); Debug.Log("1 second has passed since the last message"); Yield return StartCoroutine(Wait(2.5f)); Debug.Log("2.5 seconds have passed since the last message"); } //Our wait function IEnumerator Wait(float duration) { for(float timer = 0; timer < duration; timer += Time.deltaTime) Yield return 0; } }
第二個方法用了yield,但它並沒有用0或者null,而是用了Wait()來yield,這相當於是說,“不再繼續執行本程式,直到Wait程式結束”。
現在,協程在程式設計方面的能力要開始展現了。
控制物件行為的例子
在最後一個例子中,我們就來看看協程如何像建立方便的計時器一樣來控制物件行為。協程不僅僅可以使用可計數的時間來yield,它還能很巧妙地利用任何條件。將它與巢狀結合使用,你會得到控制遊戲物件狀態的最強大工具。
運動到某一位置
對於下面這個簡單指令碼元件,我們可以在Inspector面板中給targetPosition和moveSpeed變數賦值,程式執行的時候,該物件就會在協程的作用下,以我們給定的速度運動到給定的位置。
usingUnityEngine; Using System.Collections; Public class MoveExample : MonoBehaviour { public Vector3 targetPosition; public float moveSpeed; Void Start() { StartCoroutine(MoveToPosition(targetPosition)); } IEnumerator MoveToPosition(Vector3 target) { while(transform.position != target) { transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime); Yield return 0; } } }
-
這樣,這個程式並沒有通過一個計時器或者無限迴圈,而是根據物件是否到達指定位置來yield。
按指定路徑前進
我們可以讓運動到某一位置的程式做更多,不僅僅是一個指定位置,我們還可以通過陣列來給它賦值更多的位置,通過MoveToPosition() ,我們可以讓它在這些點之間持續運動。
Using UnityEngine; Using System.Collections; Public class MoveExample : MonoBehaviour { ublic Vector3[] path; ublic float moveSpeed; Void Start() { StartCoroutine(MoveOnPath(true)); } IEnumerator MoveOnPath(bool loop) { do { foreach(var point in path) Yield return StartCoroutine(MoveToPosition(point)); } while(loop); } IEnumerator MoveToPosition(Vector3 target) { while(transform.position != target) { transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime); Yield return 0; } } }
我還加了一個布林變數,你可以控制在物件運動到最後一個點時是否要進行迴圈。
把Wait()程式加進來,這樣就能讓我們的物件在某個點就可以選擇是否暫停下來,就像一個正在巡邏的AI守衛一樣,這真是錦上添花啊!
注意:
如果你剛接觸協程,我希望這兩個教程能幫助你瞭解它們是如何工作的,以及如何來使用它們。以下是一些在使用協程時須謹記的其他注意事項:
- l 在程式中呼叫StopCoroutine()方法只能終止以字串形式啟動(開始)的協程;
- l 多個協程可以同時執行,它們會根據各自的啟動順序來更新;
- l 協程可以巢狀任意多層(在這個例子中我們只嵌套了一層);
- l 如果你想讓多個指令碼訪問一個協程,那麼你可以定義靜態的協程;
- l 協程不是多執行緒(儘管它們看上去是這樣的),它們執行在同一執行緒中,跟普通的指令碼一樣;
- l 如果你的程式需要進行大量的計算,那麼可以考慮在一個隨時間進行的協程中處理它們;
- l IEnumerator型別的方法不能帶ref或者out型的引數,但可以帶被傳遞的引用;
- l 目前在Unity中沒有簡便的方法來檢測作用於物件的協程數量以及具體是哪些協程作用在物件上。