百度百科中,协程相关概念:与子例程(执行过程没有返回值)一样,协程(coroutine)也是一种程序组件,更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自 Simula 和 Modula-2 语言,但也有其他语言支持。协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。 协程最初在1963年被提出。协程不是进程或线程,一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
当我们调用一个函数的时候,必须在返回之前完成运行。这样的话,游戏中单独一帧的更新内必须完成这个函数的所有步骤。这样的话函数调用就不能应用于这种情况:在一段时间内实现一段动画或一系列事件。
举个小例子:考虑这种任务——逐渐降低对象的alpha值,直到对象完全看不见为止。
void Fade() {
for (float f = 1f; f >= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
}
}
Fade函数不会产生我们期望的效果,它会在一帧更新中完成对象的alpha值的逐渐降低至0,中间结果值我们看不到,只能看到对象立马消失了。而我们预期的效果是,alpha值每减少一点就呈现出alpha对该帧渲染的效果,因而需要很多帧来呈现这个循序渐进变化的过程。当然啦,如果在Update函数中添加相应的代码使得一帧接一帧地来处理这种情况是可能的,但是使用协程来处理这种情况会更加方便。
协程就像一个函数,它可以暂停执行,将控制权转交给Unity,当下一帧更新又被调用时又会从上次暂停的地方接着继续执行。
C#中,协程的声明是这个样子的:
IEnumerator Fade() {
for (float f = 1f; f >= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return null;
}
}
可以看出,协程本质上就是一个有着IEnumerator返回值的函数,并且函数体中包含yield返回语句。yield返回语句就是该帧暂停下一帧恢复的点。而且该函数中的f变量的值不会因为每次调用而重置,它都会保存上一次暂停时的值,协程中所有的参数和变量都会完好保存下来,感觉有点像static变量了。为了让协程运行起来,我们需要使用StartCoroutine函数来启动它: void Update() {
if (Input.GetKeyDown("f")) {
StartCoroutine("Fade");
}
}
在UnityScript中,很简单,包含yield语句即为协程,返回类型IEnumerator不需要显示声明:function Fade() {
for (var f = 1.0; f >= 0; f -= 0.1) {
var c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield;
}
}
在UnityScript中,协程就像个正常的函数一样被调用function Update() {
if (Input.GetKeyDown("f")) {
Fade();
}
}
IEnumerator Fade() {
for (float f = 1f; f >= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return new WaitForSeconds(.1f);
}
}
在UnityScript中function Fade() {
for (var f = 1.0; f >= 0; f -= 0.1) {
var c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield WaitForSeconds(0.1);
}
}
对于将一个效果持续一段时间这是很有用的一种方式,而且它也是一种有用的性能优化手段。例如,游戏中有许多任务需要定期执行,最显而易见的方法就是让Update函数去做。然而,Update函数会每秒执行好多次,很可能任务并不需要这么频繁地去执行,这样协程就起到作用了,任务放到协程中,可以根据设定的间隔时间去执行相应的任务,避免了每帧都去执行的浪费。举个类似例子,警报器警告玩家是否敌人在附近:
function ProximityCheck() {
for (int i = 0; i < enemies.Length; i++) {
if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
return true;
}
}
return false;
}
假如有很多敌人,那么每帧都调用该函数会带来很大的开销。假如我们利用协程每0.1s调用一次,这将会极大降低check的次数,而游戏的可玩性并没有差异:
IEnumerator DoCheck() {
for(;;) {
ProximityCheck;
yield return new WaitForSeconds(.1f);
}
}
Coroutine StartCoroutine(string methodName, object value = null);
UnityScript中StartCoroutine(routine: IEnumerator): Coroutine;
StartCoroutine(methodName: string, value: object = null): Coroutine;
使用StartCoroutine(string methodName)和StartCoroutine(IEnumerator routine)都可以开启一个线程。区别在于使用字符串作为参数可以开启线程并在线程结束前终止线程,相反使用IEnumerator 作为参数只能等待线程的结束而不能随时终止(除非使用StopAllCoroutines()方法);另外使用字符串作为参数时,开启线程时最多只能传递一个参数,并且性能消耗会更大一点,而使用IEnumerator 作为参数则没有这个限制。
另外终止协同程序的方法:将协同程序所在GameObject的active属性设置为false,当再次设置active为ture时,协同程序并不会再开启;如果将协同程序所在脚本的enabled设置为false则不会生效。这是因为协同程序被开启后作为一个线程在运行,而MonoBehaviour也是一个线程,他们成为互不干扰的模块,除非代码中用调用,他们共同作用于同一个对象,只有当对象不可见才能同时终止这两个线程。
(协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受GameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。)
协程与顺序执行不同:关键在于yield。如果顺序执行的时候进行耗费cpu时间或者一直等待某个资源的时候,程序将卡在这个地方不能前进。而协同程序可以使等待资源的线程让出资源,进行下一个协同程序的操作,yield可以在执行出错的时候挂起,下次恢复的时候再进行操作。
注意:协同程序的参数不能指定ref、out参数;Update函数与FixedUpdate函数中不能使用yield语句,但可以在它们中使用StartCoroutine函数来调用一个函数;协程最大的用处是可以实现将一段程序延迟执行或将任务的各个部分分布在一个时间段内连续执行(需要好多帧执行一次的情况);Unity在每一帧都会去处理对象上的协程,主要在渲染后去处理协程(检查协程的条件是否满足)。