原文链接在这里[Unity Patterns | Introduction to Coroutines]
序言
在Unity中,Coroutine是我最喜欢的特性之一。我几乎在所有的项目中都会用它来控制角色的运动,连续的行为以及对象的行为等等。在这个教程中,我将会详细介绍Coroutine的工作原理,同时还会通过几个实际的小例子来演示Coroutine能如何被应用到实际的项目中。
Coroutine简介
注意:这是关于Coroutine的教程中的第一篇,如果你有兴趣还可以读一下这个教程中的第二篇文章,地址在这里
Unity的Coroutine系统源自于C#的IEnumerator,IEnumerator接口的功能简单又强大,我们可以很容易的为我们自己的集合类型定义自己的enumarator。不过我们不用担心自定义Enumerator的实现是不是会太复杂,我们可以直接先来看一个例子,通过这个简单的例子我们来分析一下Coroutine的工作原理。首先,我们先看一段简单的代码。
计时器
这是一个非常简单的计时器脚本,计时器时间到0的时候,就输出一个Log信息,表明计时已经结束。
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!");
}
}
这段代码乍看上去还不错,代码简短同时也很好地实现了计时的需求。但是问题来了,如果我们有一些更加复杂的脚本(例如玩家角色或者怪物),这些脚本中都含有多个计时器呢?那么我们的代码最终就有可能会变成这个样子了。
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!");
}
}
其实如果能忍的话,这段代码也还算可以啦,并没有啥硬伤。但是就我个人而言,我实在不喜欢有这么多的Timer变量,搞得整个代码毫无美感。而且我当我想重启这些Timer的时候,我一定得重置这些Timer的值(而事实上我总忘),这简直不能忍啊,有木有。
那么如果我写个循环是不是能再优化一些呢?
for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
{
//Just do nothing...
}
Debug.Log("This happens after 3 seconds!");
嗯,看起来已经好多了,这回Timer变量成了循环内部的局部变量了,我也不需要再手动地设置每个循环中Timer的值了。
你可能已经知道接下来我要说的是啥了:对的,Coroutine干的就是这个。
初尝Coroutine
下面是一个跟前面我们写的计时器脚本功能一模一样的例子,只不过这个例子是使用了Coroutine来实现计时的功能。咱们最好实际动手敲一遍这个脚本,然后在Unity中运行一下,看看效果是不是一样的。
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方法的Coroutine。请注意在这里我直接传入了一个方法调用作为参数(这样可以有效地将Countdown方法的返回值当做参数传入到StartCoroutine方法中),而没有传入Countdown函数的引用。
Yielding
这个Countdown方法除了以下两部分内容,其实已经很好地完成了自解释的过程:
- IEnumerator类型的返回值
- 循环中的yield return代码段
为了让Countdown方法能跨多帧执行(这个例子中,Countdown方法会在三分钟之内的每一帧都执行),Unity需要将整个函数执行的状态保存到内存的某个地方。而这个操作就是通过你使用yield return代码段返回的IEnumerator来实现的。当你“yield”一个方法的时候,实际上你就在告诉Unity“暂时先停止执行这个方法,等到下一帧的时候再从这儿开始执行这个方法!”。
注意:yield return代码段返回0或者null的意思就是告诉Unity等下一帧再从这里开始执行这个方法。但是你完全可以yield return其他的Coroutine,这个我们会在下一个教程中谈到。
举些栗子
在我们刚刚开始接触Corounines的时候,确实比较容易被搞得稀里糊涂的,我已经看过不少挫逼和牛逼的程序员对这个Coroutine的语法愁眉不展了。所以捏,偶觉得还是需要通过一些栗子来让我们更快更好地去理解Coroutine这货:
说几次“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循环中使用Coroutine的话,其实是可以达成死循环的效果滴。这个特性可以让我们很轻易的就模拟出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方法的效果,我们还可以在Coroutine中做更牛逼的事情,例如:
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.");
}
}
这个方法向我们展示了Coroutine的一个非常骚比的特性,那就是这个方法执行体内部的所有状态都会被自动保存,包括方法体中定义的局部变量,都会在整个程序执行的过程中被自动保存起来,直到下一帧再次被调用。还记得文章开头的那些无聊的timer字段们吗?有了Coroutine之后,我们就不再需要那些货了,只需要把每个计时器需要的字段放到自己的计时方法内定义就OK了,是不是处女座程序猿童鞋的福音捏?
启动和结束Coroutine
至此,我们已经学习了通过StartCoroutine()方法来启动一个Coroutine:
StartCoroutine(Countdown());
如果我们想结束所有正在运行的Coroutine的话,我们可以通过StopAllCoroutines()这个方法来结束。但是需要注意的是,这个方法只会结束由调用StopCoroutines()方法的同一个MonoBehaviour组件启动的Coroutine们,对于通过其他MonoBehaviour组件启动的Coroutine就没有效果了。
如果我们通过下面的这种方式启动了两个Coroutine:
StartCoroutine(FirstTimer());
StartCoroutine(SecondTimer());
然后我们又想单独结束两个Timer中的某一个,上面提到的方法就无法胜任了。如果我们想单独结束某一个指定的Timer的话,我们可以通过使用Coroutine的名称来作为StartCoroutine()和StopCoroutine()方法的参数,如下:
//If you start a Coroutine by name...
StartCoroutine("FirstTimer");
StartCoroutine("SecondTimer");
//You can stop it anytime by name!
StopCoroutine("FirstTimer");
[译者自己加上的]又或者这样,不使用字面常量:
// Define the Timers
IEnumerator firstTimer = FirstTimer();
IEnumerator secondTimer = SecondTimer();
// Start the Coroutines
StartCoroutine(firstTimer);
StartCoroutine(secondTimer);
// Stop the first timer coroutine
StopCoroutine(firstTimer);
扩展阅读
扩展阅读链接
如果你知道一些不错的关于Unity Coroutine的教程或者相关的教程的话,希望你能在评论中留一下这些教程的链接。
另外,如果你发现这个教程中有哪些不对的地方或者不可用的链接,或者有什么其他的问题,欢迎你联系我们。
欢迎打赏
网站主机租赁需要费用,写教程呢,又是个颇费时间的活,所以你懂的。欢迎通过PayPal打赏,以便Unity Patterns能持续运营下去。 Chevy(译者注:原文作者名称)是个穷小子,给多给少都是莫大的帮助哦。
以上翻译未对原文做任何删减,酌情增加了一丢丢内容,如果翻译有错误的地方请指正,更欢迎大家用美金支持原文作者的这种分享,使用上方的PayPal链接就可以给原文作者进行捐赠支持了。