原文: Introduction to Coroutines
Unity的系统程序系统的能力由C#的IEnumerator提供,IEnumerator是一个简单但是强大的接口,这个接口允许你写自己的可数集合类型。但是你不必在意这个,让我们直接跳到一个简单的例子,这个例子展示了协同程序可以做的事情。首先,让我们看一个简单的一小块代码:
The Countdown Timer
这里有一个简单的组件,组件仅仅减少它的timer字段,输出一个消息——timer到达了0。
using UnityEngine;
using System.Collections;
public class Countdown : MonoBehaviour
{
public float timer = 3;
void Update()
{
timer -= Time.deltaTime;
if (timer <= 0)
Debug.Log("Timer has finished!");
}
}
不是太坏,非常少的代码达到了它的目标。但是有一个问题,如果我们有更多的组件(像一个Player或者Enemy类)它们有多个计时器,怎么办?我们的代码看起来就像这样:
using UnityEngine;
using System.Collections;
public class MultiTimer : MonoBehaviour
{
public float firstTimer = 3;
public float secondTimer = 2;
public float thirdTimer = 1;
void Update()
{
firstTimer -= Time.deltaTime;
if (firstTimer <= 0)
Debug.Log("First timer has finished!");
secondTimer -= Time.deltaTime;
if (secondTimer <= 0)
Debug.Log("Second timer has finished!");
thirdTimer -= Time.deltaTime;
if (thirdTimer <= 0)
Debug.Log("Third timer has finished!");
}
}
这也不算太坏,但是就个人而言,我不喜欢这些计时的变量让我的代码乱作一团。感觉很脏,我总是不得不确保在重新计时的时候重置它们(但是我经常忘记)。
如果我用for循环是否会变得好些?
for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
{
//Just do nothing...
}
Debug.Log("This happens after 5 seconds!");
这看起来很整齐,因为现在每一个计时的变量仅仅作为循环体的一部分,我不需要重复设置这些变量了。
好的,你可能了解了我大概的想法:协同程序可以将这件事做的很漂亮!
进入协同程序
现在,这里有一个和上边完全相同的例子,但是使用了一个协同程序!我建议你写一个简单的组件并且从现在开始跟着我的步骤,你自己再看看发生的事情。
using UnityEngine;
using System.Collections;
public class CoroutineCountdown : MonoBehaviour
{
void Start()
{
StartCoroutine(Countdown());
}
IEnumerator Countdown()
{
for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
yield return 0;
Debug.Log("This message appears after 3 seconds!");
}
}
这里看起来有一点不同,我会解释这里发生了什么。
StartCoroutine(Countdown());
这一行开始了我们的Countdown方法。注意我没有给Countdown传入一个引用,而是调用这个函数自身(有效的传递一个倒计时的值)
Yielding
这个Countdown函数能很好的自行解释他自己,除了两个部分:
*IEnumerator返回值
*for循环中的yield返回值
为了在多个帧中运行这个方法(这里指的是3秒内的帧),Unity通过某些方式已经存储了这个方法的状态。它使用IEnumerator类型代表yield return返回调用的值。当你yield一个方法的时候,就会说:“现在停止这个方法,在下一帧重新从这里开始执行!”
注意:yield 0或者null会告诉协同程序进入等待,直到下一帧继续之前。但是你也可以yield其他的协同程序,这个我会在一个课程中讲解。
一些例子
协同程序在一开始是相当让人困惑的,我看到新的和有经验的程序员在协同语法上都瞪大了他们的眼镜。所以,我会尽可能的通过例子来说明,这里有几个简单的协同程序的例子:
在一段时间里说“Hello”
记住,yield return 说:“停止这个函数,下一帧在继续执行”,这意味着可以做这些:
//This will say hello 5 times, once each frame for 5 frames
IEnumerator SayHelloFiveTimes()
{
yield return 0;
Debug.Log("Hello");
yield return 0;
Debug.Log("Hello");
yield return 0;
Debug.Log("Hello");
yield return 0;
Debug.Log("Hello");
yield return 0;
Debug.Log("Hello");
}
//This will do the exact same thing as the above function!
IEnumerator SayHello5Times()
{
for (int i = 0; i < 5; i++)
{
Debug.Log("Hello");
yield return 0;
}
}
永远的....在每一帧说“Hello”
在while循环里使用yield,你可以有一个持续运行的协同程序!这让你模拟在Update()循环中。
//Once started, this will run until manually stopped or the object is destroyed
IEnumerator SayHelloEveryFrame()
{
while (true)
{
//1. Say hello
Debug.Log("Hello");
//2. Wait until next frame
yield return 0;
} //3. This is a forever-loop, goto 1
}
记录 秒
...但是不像在Update()里,你可以协同程序里做一些花哨的事情,像这样:
IEnumerator CountSeconds()
{
int seconds = 0;
while (true)
{
for (float timer = 0; timer < 1; timer += Time.deltaTime)
yield return 0;
seconds++;
Debug.Log(seconds + " seconds have passed since the Coroutine started.");
}
}
这个函数的让协同程序亮了:这个函数的状态被存了起来,所以在函数体中任何定义的变量都会保持他们的值,甚至在两个帧之间。还记得在教程一开始的令人讨厌的计时变量吗?使用协同函数,我们不再需要他们,我们只需要在函数体中放置这些变量!
开始&&停止&&协同程序
之前,我们学习了通过调用StartCoroutine()方法启动一个协同程序,像这样:
StartCoroutine(Countdown());
如果我们想要停止所有的协同程序,我们可以使用StopAllCoroutines()函数,它做的就像它的名字中承诺的一样。注意它只会停止这些协同程序——被该物体启动的协同程序,而不是其他MonoBehaviours运行的协同函数。
但是,如果我们有两个协同程序在运行会怎样,像这样:
StartCoroutine(FirstTimer());
StartCoroutine(SecondTimer());
...我们想要停止他们中的一个,怎么办?这种情况是不行的。如果我们想要停止一个特定的协同程序,你必须使用一个string为参数的函数,像这样:
//If you start a Coroutine by name...
StartCoroutine("FirstTimer");
StartCoroutine("SecondTimer");
//You can stop it anytime by name!
StopCoroutine("FirstTimer");
额外的链接:
- Coroutines – Unity Script Referencee