Unity中的協程(一)


這篇文章很不錯的問題,推薦閱讀英文原版:

Introduction to Coroutines

Scripting with Coroutines

 

這篇文章轉自: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());
  1. 這一行用來開始我們的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
}
  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());
  1. 那我們怎么終止其中的一個協程呢?在這個例子里,這是不可能的,如果你想要終止某一個特定的協程,那么你必須得在開始協程的時候將它的方法名作為字符串,就像這樣:
//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;  

        }  
    }  
}
  1. 嵌套的協程

在此之前,我們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中沒有簡便的方法來檢測作用於對象的協程數量以及具體是哪些協程作用在對象上。

 

目前來看,這篇文章是我在網絡上找到的最好的協程入門文章,不過要完全理解協程還是要多思考一下。

 

細雨標記: Unity


免責聲明!

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



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