一、協程
協程是什麼?
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 內部可能做了這樣的處理:
-
拿到參數(迭代器)後,在某個時機(不是立即,至於是什麼時候,我們待會兒再議)調用迭代器的
MoveNext() -
如果發現
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,還有類似於 setInterval 的 InvokeRepeating,但遠不如 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;
}
}
用法有講究:
-
創建以上腳本
-
創建空物體,並綁定以上腳本
然後就可以在任意地方調用了,例如:
void Start() {
int arg = 1;
Wait.runAfterSec (() => {
print (arg);
}, 3);
Application.LoadLevel("scene1");
}
// => 場景跳轉 [3s later] 1
嫌功能弱的話,可以改成用隊列存放 action 等等,自行擴展
五、總結
推薦使用第一種方式,即用協程實現延時調用,要求可控性比較高的話,可以採用第三種方式,情景比較簡單的話可以用第二種方式,畢竟要傳參、定義函數,不很方便
暫無評論,快來發表你的看法吧