[Unity][基础篇]协程

为什么需要协程

在游戏中有许多过程(Process)需要花费多个逻辑帧去计算。

  • 你会遇到“密集”的流程,比如说寻路,寻路计算量非常大,所以我们通常会把它分割到不同的逻辑帧去进行计算,以免影响游戏的帧率。

  • 你会遇到“稀疏”的流程,比如说游戏中的触发器,这种触发器大多数时候什么也不做,但是一旦被调用会做非常重要的事情(比如说游戏中自动开启的门就是在门前放了一个Empty Object作为trigger,人到门前就会触发事件)。

不管什么时候,如果你想创建一个能够历经多个逻辑帧的流程,但是却不使用多线程,那你就需要把一个任务来分割成多个任务,然后在下一帧继续执行这个任务。

比如,A算法是一个拥有主循环的算法,它拥有一个open list来记录它没有处理到的节点,那么我们为了不影响帧率,可以让A算法在每个逻辑帧中只处理open list中一部分节点,来保证帧率不被影响(这种做法叫做time slicing)。

再比如,我们在处理网络传输问题时,经常需要处理异步传输,需要等文件下载完毕之后再执行其他任务,一般我们使用回调来解决这个问题,但是Unity使用协程可以更加自然的解决这个问题如下边的程序:

private IEnumerator Test() { 
  WWW www = new WWW(ASSEST_URL);
  yield return www; 
  AssetBundle bundle = www.assetBundle;
}

协程是什么

协程不是线程,也不是异步执行的。协程和 MonoBehaviour 的 Update函数一样也是在MainThread中执行的。使用协程你不用考虑同步和锁的问题。

从程序结构的角度来讲,协程是一个有限状态机,这样说可能并不是很明白,说到协程(Coroutine),我们还要提到另一样东西,那就是子例程(Subroutine),子例程一般可以指函数,函数是没有状态的,等到它return之后,它的所有局部变量就消失了,但是在协程中我们可以在一个函数里多次返回,局部变量被当作状态保存在协程函数中,知道最后一次return,协程的状态才别清除。

简单来说,协程就是:你可以写一段顺序的代码,然后标明哪里需要暂停,然后在下一帧或者一段时间后,系统会继续执行这段代码。

协程的作用一共有两点:
1)延时(等待)一段时间执行代码;
2)等某个操作完成之后再执行后面的代码。
总结起来就是一句话:控制代码在特定的时机执行。

协程运行的原理

Paste_Image.png

参照上图,协程跟Update()其实一样的,都是Unity每帧对会去处理的函数(如果有的话)。如果MonoBehaviour 是处于激活(active)状态的而且yield的条件满足,就会调用协程的代码。如果是 yield return null ,就会在同一帧再次被唤醒。
  协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法 Current 和 MoveNext() ,迭代器方法运行到 yield return 语句时,会返回一个expression表达式并保留当前在代码中的位置。 当下次调用迭代器函数时执行从该位置重新启动。unity3d在每帧做的工作就是:调用协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。

协程怎么灵活运用

    1. yield return可以返回任意YieldInstruction,所以我们可以在这里加上一些条件判断:
YieldInstruction y; 
if(something) 
    y = null;
else if(somethingElse) 
    y = new WaitForEndOfFrame();
else 
    y = new WaitForSeconds(1.0f);

 yield return y;
    1. 由于一个协程只是一个迭代器块而已,所以你也可以自己遍历它,这在一些场景下很有用,例如在对协程是否执行加上条件判断的时候:
IEnumerator DoSomething(){ /* ... */} 
IEnumerator DoSomethingUnlessInterrupted() { 
    IEnumerator e = DoSomething(); 
    bool interrupted = false; 
    while(!interrupted) { 
        e.MoveNext(); 
        yield return e.Current; 
        interrupted = HasBeenInterrupted(); 
    }
}
  • 3)由于协程可以yield协程,所以我们可以自己创建一个协程函数,如下:
IEnumerator UntilTrueCoroutine(Func fn) { 
    while(!fn()) yield return null;
} 
Coroutine UntilTrue(Func fn) { 
    return StartCoroutine(UntilTrueCoroutine(fn));
} 
IEnumerator SomeTask() { 
    /* ... */ 
    yield return UntilTrue(() => _lives < 3); 
    /* ... */
}

总结

  • 协程和Update()一样更新,其也可以使用Time.deltaTime
  • 协程并不是多线程,它和Update()一样是在主线程中执行的,所以不需要处理线程的同步与互斥问题
  • yield return null其实没什么神奇的,只是unity3d封装以后,这个协程在下一帧就被自动调用了
    可以看到,在使用协程时yield这个关键字出现的很频繁,如果觉得陌生的话,你可以暂时理解其作用为延时一段时间后继续往下执行。之后会专门针对yield的用法再做详解
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容