跳到主要內容
黯羽輕揚每天積累一點點

unity3d 延時調用

免費2015-10-17#Unity3D#u3d延时调用#unity3d几秒后#unity3d return new yield#u3d return new yield#StartCoroutine#WaitForSeconds#Invoke#InvokeRepeating

u3d 可以用 Coroutine(協程)實現延時調用,也可以用 Invoke,或者乾脆 Time.time 自己控制

一、協程

協程是什麼?

A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.

協程不是新線程,是 u3d 提供的一種迭代器管理機制,例如常見的:

void Start () {
    print (0);
    StartCoroutine (wait (3));
    print (1);
}

IEnumerator wait(float s) {
    print (2);
    yield return new WaitForSeconds (s);
    print (3);
}
// => 0 2 1 [3s later] 3

StartCoroutine 傳入 wait 返回的迭代器,協程內部就可以控制迭代器內部的執行,類似於:

IEnumerator iter;

void Start () {
    print (0);
    iter = wait ();
    print (1);
}

IEnumerator wait() {
    print (2);
    yield return 1;
    print (3);
}
// => 0 1

為什麼沒有輸出 2 和 3?

因為 wait 只是創建了一個迭代器,我們知道迭代器不調用 next() 方法(C# 是 MoveNext,js 是 next)就不會執行。嗯,接著嘗試:

IEnumerator iter;

void Start () {
    print (0);
    iter = wait ();
    print (1);
    iter.MoveNext ();
    iter.MoveNext ();
}

IEnumerator wait() {
    print (2);
    yield return 1;
    print (3);
}
// => 0 1 2 3

手動調用迭代器的 MoveNext() 方法,結果和我們想的一樣,這就是 StartCoroutine 的參數,StartCoroutine 內部拿到迭代器的引用就可以控制什麼時候 MoveNext,什麼時候停下來。所以我們看到的效果是:代碼在分段執行(由 yield return 語句分開)這個表象

還不太明白?沒關係,我們接著看:

IEnumerator iter;

void Start () {
    print (0);
    iter = wait (3);
    print (1);
    iter.MoveNext ();
    iter.MoveNext ();
}

IEnumerator wait(float s) {
    print (2);
    yield return new WaitForSeconds (s);
    print (3);
}
// => 0 1 2 3

等等,為什麼輸出 2 後沒有等待 3 秒?

因為我們現在是在手動管理迭代器,第一次 MoveNext() 之後 yield return 返回的 WaitForSeconds 類型對象我們並沒有做相應處理,所以不會等待 10s。

StartCoroutine 內部可能做了這樣的處理:

  1. 拿到參數(迭代器)後,在某個時機(不是立即,至於是什麼時候,我們待會兒再議)調用迭代器的 MoveNext()

  2. 如果發現 MoveNext() 的返回值(也就是 yield return 後面的東西)是一個 WaitForSeconds 對象,就 s 秒後再執行 MoveNext()

最終我們看��的直接結果就是延時 s 秒執行 yield return 下面的東西。好了,到這裡就差不多弄清楚了,最後一個例子:

IEnumerator iter;

void Start () {
    print (0);
    StartCoroutine (iter = wait (3));
    print (1);
    iter.MoveNext ();
}

IEnumerator wait(float s) {
    print (2);
    yield return new WaitForSeconds (s);
    print (3);
}
// => 0 2 1 3

猜猜輸出 2 之後有沒有等待 3 秒?

沒有,因為我們手動 MoveNext () 了,協程第一次調用 MoveNext () 的時候迭代器已經走了一步了,協程根本沒有看到 WaitForSeconds,所以協程拿到迭代器後不是立即執行,而是等待某個屬於協程的時間段(據說是在 Update 之後,有興趣的話可以找找 u3d 函數執行順序圖,當然,這不重要)

協程是什麼?答案是開篇提到的:

協程是 u3d 提供的一種迭代器管理機制

P.S. 前輩博文 說,「協程其實就是一個 IEnumerator(迭代器)」,二者哪個更對,不用爭辯了吧

二、協程實現的延時調動

看到一種很靈活的延時調用,如下:

using UnityEngine;
using System.Collections;
using System;

public class Delay {
    public static IEnumerator run(Action action, float delaySeconds) {
        yield return new WaitForSeconds(delaySeconds);
        action();
    }
}

代碼修改自 Unity 延遲執行一段代碼的實現比較好的方式

靜態方法,方便調用,可以作為工具函數,不需要與任何物體綁定,調用方式比較醜,如下:

void Start() {
    print (1);
    StartCoroutine(Delay.run (() => {
        print (2);
    }, 3));
}
// => 1 [3s later] 3

特別注意:有 2 種情況會導致延時調用失敗

  • StartCoroutine 後面有切換場景(Application.LoadLevel

因為切換場景後協程停止執行,延時調用就失敗了

  • StartCoroutine 後面有 Destroy 當前物體或者當前物體的祖先物體

因為物體被銷毀後,該物體身上的所有腳本通過 StartCoroutine 添加的協程任務都會停止執行,所以延時調用失敗

三、Invoke 的延時調用

Invoke 類似於 js 的 setTimeout,還有類似於 setIntervalInvokeRepeating,但遠不如 js 強大,Invoke 系列只能接受字符串形式的方法名,按名延時調用,例如:

int arg;

void Start() {
    arg = 1;
    Invoke ("doSth", 3);
}

void doSth() {
    print (arg);
}
// => [3s later] 1

因為字符串形式不能傳參,所以用了全局變量來傳,語法很簡潔,也沒有讓人迷惑的地方

特別注意:上面提到的 2 種延時調用失敗的情況仍然存在

四、Time.time 實現延時調用

比較笨的方法,但能夠避免切換場景和銷毀物體導致延時調用失效的問題,代碼如下:

using UnityEngine;
using System.Collections;
using System;

public class Wait : MonoBehaviour {
    static Action _action;
    static float time;
    static float delayTime;

    // Use this for initialization
    void Start () {
        // 切換場景時不銷毀該物體
        DontDestroyOnLoad (gameObject);

        reset ();
    }
    
    // Update is called once per frame
    void Update () {
        if (Time.time > time + delayTime) {
            _action();
            reset();
        }
    }

    void reset() {
        time = 0;
        delayTime = int.MaxValue;
    }

    public static void runAfterSec(Action action, float s) {
        _action = action;
        time = Time.time;
        delayTime = s;
    }
}

用法有講究:

  1. 創建以上腳本

  2. 創建空物體,並綁定以上腳本

然後就可以在任意地方調用了,例如:

void Start() {
    int arg = 1;
    Wait.runAfterSec (() => {
        print (arg);
    }, 3);
    Application.LoadLevel("scene1");
}
// => 場景跳轉 [3s later] 1

嫌功能弱的話,可以改成用隊列存放 action 等等,自行擴展

五、總結

推薦使用第一種方式,即用協程實現延時調用,要求可控性比較高的話,可以採用第三種方式,情景比較簡單的話可以用第二種方式,畢竟要傳參、定義函數,不很方便

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論