【前置知识】
【为什么要有协程】
游戏中逻辑更新和画面更新的时间点有确定性,必须按照帧序列严格保持同步,否则就会出现游戏中对象不同步的现象。虽然多线程也能实现对象同步等效果,但是对一个大型游戏而言,游戏中的对象非常多,用多线程来实现很容易出Bug,而且往往很难找到Bug在哪,这无疑会加大开发的难度。
为了开发便利,统一生命周期管理,避免多线程锁的问题,Unity采用单线程逻辑。
为此,UnityEngine对其API和Componet等做出了限制,不能在主线程之外的线程中使用它们,这就确保了它们始终是在单线程中运行。
但是,对于一些耗时的操作(即需要较长时间才能执行完的代码块),我们不能一直等待其执行完毕再执行接下来的操作,否则游戏会出现卡顿等现象。例如,在游戏中等待1s再去执行某个操作时,如果我们让主线程等待1s,那么游戏画面就会停顿1s,这是不行的。为了解决这类问题,就需要使用协程——在主线程运行的同时开启另一段逻辑处理,来协同当前程序的执行。
显然,协程的出现是为了解决单线程的问题,如同线程的出现是为了解决进程的问题一样。我们可以想象,如果程序越来越复杂,进程之上还会有xx程,协程之下还会有xx程。
【协程的特点及执行过程】
【测试例子】
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Coroutine : MonoBehaviour
{
private bool isStart = true;
void Start()
{
Debug.Log("开始执行Start" + Time.frameCount);
StartCoroutine(Test());
Debug.Log("结束执行Start" + Time.frameCount);
}
void Update()
{
if (isStart)
{
Debug.Log("开始执行Update"+ Time.frameCount);
isStart = false;
StartCoroutine(Test());
Debug.Log("结束执行Update" + Time.frameCount);
}
}
IEnumerator Test()//冒泡排序
{
int[] A = new int[10];
System. Random ra = new System.Random();
for (int i = 0; i < 10; i++)
{
A[i] = ra.Next(200);
}
bubblesort(A);
foreach (int a in A)
{
Debug.Log(a);
}
Debug.Log("开始执行协程" + Time.frameCount);
yield return null;
Debug.Log("结束执行协程"+Time.frameCount);
}
public void bubblesort(int[] A)
{
int n = A.Length;
for (int i = 0; i < A.Length - 1; i++)
{
for (int j = 0; j < A.Length - 1; j++)
{
if (A[j] > A[j + 1])
{
int temp = A[j];
A[j] = A[j + 1];
A[j + 1] = temp;
}
}
}
}
}
【测试结果】
【结论】
显而易见,我们在调用协程时(即使用StartCoroutine()函数时),程序会先继续执行协程里的代码,而不是立即返回,继续执行下一行代码,遇到yield 时,协程停止执行,再返回执行下一行代码。
那么yield后的代码在什么时候执行呢?
在Unity官方文档中的生命周期中可以看到yield后的代码在Update执行完之后再执行,即在所有脚本中的Update执行完后,如果yield后面还有代码就执行其后的代码,而且,yield后面的代码要在下一帧中执行。
所以,yield的作用是中断协程的执行。在执行协程的过程中,如果遇到yield,则表示当前帧执行到这里停止,不再继续执行下一行代码,所以这个协程没有执行完毕,在下一帧中会再次执行这个协程,并且从yield开始执行。
如果协程要在下一帧能够从yield开始执行,那么协程就有自己的堆栈,自己的局部变量,有自己的指令指针。总而言之,要保存在上一帧中执行协程时所得的结果。
yield后面可以跟一个表达式,函数等。当下一帧从yield开始执行时,会判断yield后面的条件是否满足。如果满足,就继续执行下一行代码;如果不满足,在当前帧中,协程执行到这里就停止,下一帧继续判断是否满足条件,直到满足条件才继续执行下一行。
【yield返回值及含义】(在C#中是yield return)
yield return null; 下一帧再执行后续代码
yield return 0; 下一帧再执行后续代码
yield return 6;(任意数字) 下一帧再执行后续代码
yield break; 直接结束该协程的后续操作
yield return asyncOperation;等异步操作结束后再执行后续代码
yield return StartCoroution(/*某个协程*/);等待某个协程执行完毕后再执行后续代码
yield return WWW();等待WWW操作完成后再执行后续代码
yield return new WaitForEndOfFrame();等待帧结束,等待直到所有的摄像机和GUI被渲染完成后,在该帧显示在屏幕之前执行
yield return new WaitForSeconds(0.3f);等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间会受到Time.timeScale的影响);
yield return new WaitForSecondsRealtime(0.3f);等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间不受到Time.timeScale的影响);
yield return WaitForFixedUpdate();等待下一次FixedUpdate开始时再执行后续代码
yield return new WaitUntil()将协同执行直到 当输入的参数(或者委托)为true的时候....如:yield return new WaitUntil(() => frame >= 10);
yield return new WaitWhile()将协同执行直到 当输入的参数(或者委托)为false的时候.... 如:yield return new WaitWhile(() => frame < 10);
yield return X>5;等待变量x大于5时再执行后续代码
yield return transform.position.y<0;等待物体的高度小于0时再执行后续代码
如果在当前帧就可以执行完协程中的最后一行代码,那么就表示这个协程已经执行完毕。在下一帧中,会继续执行所有的Update,但不会再执行这个在上一帧中已经执行完毕的协程。(Start和Update中的协程虽然都是调用Test,但它们是两个协程,而不是一个。)
所以,协程实际上是运行在Unity主线程中,有多个协程时,这些协程依次使用CPU,其他协程处于休眠状态(即CPU每次只响应一个协程的计算请求,只有完成计算请求后,才会响应下一个协程的计算请求),这就类同单核CPU响应多个线程请求的情况,也类同多个脚本中多个Update依次执行。
【线程和协程的区别】
- 在多核CPU下,同一时间可以同时执行多个线程,但只能执行一个协程
- 开启一个线程比开启一个协程的开销大
- 线程适合多个关联性不大的任务同时处理,协程适合将一个任务分时间片处理
- 线程是操作系统级别的概念,开发者无法预估线程在何时被调度执行;协程是编译器级别的概念,协程的调度需要开发者自己去实现
- 线程不能使用UnityEngine的API,协程可以
- ······各类区别很多,弄明白执行过程后,意思都差不多
【协程的使用】
【注意事项】
- 必须在MonoBehaviour或继承于MonoBehaviour的类中(类似Update)
- 协程的启动和终止只是对该 MonoBehaviour而言(类似Update)
【启动协程】
- StartCoroutine(IEnumerator routine)
- StartCoroutine(string methodName)或StartCoroutine(string routine,object value) (比上一种方法性能开销要更高,而且最多只能传递 一个参数)
【终止协程】
- StopCoroutine(string methondname)只能终止通过字符串名字启动的协程
- StopCoroutine(Coroutine coroutine)
- StopAllCoroutines()终止所有协程
- 在协程内部终止:yield break;
- 把物体的active属性设置为false(类似Update,该方式容易引发bug)
【案例】
每隔1秒输出一个信息,输出10次
【代码】
IEnumerator Test2()
{
for (int i = 0; i < 10; i++)
{
Debug.Log("永恒之星");
yield return new WaitForSeconds(1.0f);
}
}
【输出】
【参考】
[1]https://blog.csdn.net/qiaoquan3/article/details/51276485
[2]https://docs.unity3d.com/Manual
[3]https://blog.csdn.net/qq_28180261/article/details/64500720
[4]https://blog.csdn.net/hany3000/article/details/10382221
[5]https://blog.csdn.net/huang9012/article/details/38492937
[6]https://blog.csdn.net/beihuanlihe130/article/details/76098844
[7]https://blog.csdn.net/u011484013/article/details/51136780
[8]https://zhuanlan.zhihu.com/p/55420579
[9]https://blog.csdn.net/qq_30585525/article/details/90769410