为什么需要协程
在游戏中有许多过程(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)等某个操作完成之后再执行后面的代码。
总结起来就是一句话:控制代码在特定的时机执行。
协程运行的原理
参照上图,协程跟Update()其实一样的,都是Unity每帧对会去处理的函数(如果有的话)。如果MonoBehaviour 是处于激活(active)状态的而且yield的条件满足,就会调用协程的代码。如果是 yield return null ,就会在同一帧再次被唤醒。
协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法 Current 和 MoveNext() ,迭代器方法运行到 yield return 语句时,会返回一个expression表达式并保留当前在代码中的位置。 当下次调用迭代器函数时执行从该位置重新启动。unity3d在每帧做的工作就是:调用协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。
协程怎么灵活运用
- yield return可以返回任意YieldInstruction,所以我们可以在这里加上一些条件判断:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
- 由于一个协程只是一个迭代器块而已,所以你也可以自己遍历它,这在一些场景下很有用,例如在对协程是否执行加上条件判断的时候:
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的用法再做详解