unity3d延时调用

一.协程

协程是什么?

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等等,自行扩展

五.总结

推荐使用第一种方式,即用协程实现延时调用,要求可控性比较高的话,可以采用第三种方式,情景比较简单的话可以用第二种方式,毕竟要传参、定义函数,不很方便

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code