一.协程
协程是什么?
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();
}
}
静态方法,方便调用,可以作为工具函数,不需要与任何物体绑定,调用方式比较丑,如下:
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
等等,自行扩展
五.总结
推荐使用第一种方式,即用协程实现延时调用,要求可控性比较高的话,可以采用第三种方式,情景比较简单的话可以用第二种方式,毕竟要传参、定义函数,不很方便