本篇内容基于 https://gamedevbeginner.com/coroutines-in-unity-when-and-how-to-use-them/
以及官方教程
为什么使用协程
协程非常适合设置需要随时间发生变化的游戏逻辑。很自然我们会想到update,update里指出每一帧unity会执行什么操作。协程则可以将代码从update中解放出来,至于为什么要这样做,请看例子:
假设我们有一辆坦克,当点击地面时,我希望坦克转向我点击的位置,朝该位置移动,到达后,等待一秒再开火。像这样:
那么我们可以给坦克列一个行动清单:
- 转向特定角度
- 移动到特定点
- 等待一秒后开火
这样一个看起来简单的逻辑,如果要再update中执行,将变得非常混乱且难以理解,因为我们要想这三个步骤按照顺序(而非同时)发生,我们要设置很多额外的变量,代码如下:
// Please don't do this
bool tankMoving;
bool facingRightWay;
bool tankInPosition;
float timer;
void Update()
{
if(Input.GetMouseButtonDown(0))
{
tankMoving = true;
}
if (tankMoving)
{
MoveTank();
}
}
void MoveTank()
{
if (facingRightWay == false)
{
if (angleIsWrong)
{
TurnTank();
}
else if (angleIsCorrect)
{
facingRightWay = true;
}
}
if (facingRightWay && tankInPosition == false)
{
if (positionIsWrong)
{
MoveToPosition();
}
else if (positionIsCorrect)
{
tankInPosition = true;
}
}
if (facingRightWay && tankInPosition && timer < 1)
{
timer += Time.deltaTime;
}
else if (facingRightWay && tankInPosition && timer > 1)
{
FireTank();
facingRightWay = false;
tankInPosition = false;
tankMoving = false;
timer = 0;
}
}
可以看到,要判断哪些事情已经发生了,当前帧可以做哪些事情,非常混乱且易出错,如果用协程?
void Update()
{
if(Input.GetMouseButtonDown(0))
{
StartCoroutine(MoveTank());
}
}
IEnumerator MoveTank()
{
while(facingWrongWay)
{
TurnTank();
yield return null;
}
while (notInPosition)
{
MoveToPosition();
yield return null;
}
yield return new WaitForSeconds(1);
Fire();
}
这次,代码更像一个代办事项清单,每一个动作都在最后一个动作完成后执行。Unity处理每一个while循环时,直到其条件不为真,然后处理下一个。
实际上这样与unity执行常规函数的方式并没有不同,只是这样逻辑是在多帧而不是一帧上执行的。
因为yield关键字,它告诉unity:“这一帧就执行到这里停下!下一帧再从这儿开始!”
如果理解不了,举一个例子,把每一帧想象成你打了一天的游戏,晚上这个yield出现了,它会叫你睡觉,存下进度,明天接着这里再玩。之前呢,我们是一天从早到晚不吃不喝地玩(所有的操作都在一帧里执行)现在呢,我们是每天玩一点,分多天去玩(操作分在多帧里)人的负担是不是小了很多。
何时在unity中使用协程
当你想要创建需要暂停的动作、按顺序执行一系列步骤、或者想要运行长时间任务(这个任务所要花费的时间比一帧长,举例就是这个游戏你一天通不了关,,)这些个时候,你就要考虑用协程。举例包括:
- 将对象移动到某个位置
- 为对象提供要执行的任务列表(比如前面的坦克)
- 淡入淡出的视觉效果或音频
- 等待资源加载
那么,如何编写协程?
如何编写协程
协程的结构和常规函数基本一致,但有几个关键区别。
首先,协程中的返回类型是IEnumerator,like this:
IEnumerator MyCoroutine()
{
// Code goes here!
}
先不用管这个IE什么的,现在只要知道,这是unity将函数执行拆分到多个帧的实现方式。
就像一般函数一样,我们可以将参数传递给协程:
IEnumerator MyCoroutine(int number)
{
number++;
// Code goes here.
}
只要协程没有被终止,在协程中的变量都会保持值(即变量的值会带到下一帧去,没有人每天都会从头开始打游戏吧?)
此外,和常规函数不同的是,协程允许我们在代码执行当中,用yield语句暂停代码。
如何在unity中暂停协程(yield)
在我们希望函数终端的时候,使用关键字yield return。
yield表面方法是一个迭代器,并且它将执行超过一帧;而return与常规函数一样,会在该点终止执行,并将控制权交给调用它的方法。
yield return之后的内容,将指定unity在继续执行钱,等待多久,我们有哪些选择呢?
yield return null (等到下一帧再执行)
yield return null告诉unity等到下一帧再执行,将它与while结合起来:
IEnumerator MyCoroutine()
{
int i = 0;
while (i < 10)
{
// Count to Ten
i++;
yield return null;
}
while (i > 0)
{
// Count back to Zero
i--;
yield return null;
}
// All done!
}
unity将完成第一个循环,每一帧累加一个数字,然后是第二个循环,每帧减少一个数字,直到代码块结束。
如果没有 yield return null,所有代码将立即执行,和常规函数一样。
wait for seconds(等待一段时间)
Wait For Seconds或Wait For Seconds Real Time(使用未缩放的时间)允许我们指定确切的等待时间。它只能在 Coroutine 中使用(即它在 Update 中不起作用)。
就像之前一样,我们需要将 Wait for Seconds 与yield return语句一起使用,在这种情况下,需要使用new关键字才能使其工作。
IEnumerator WaitFiveSeconds()
{
print("Start waiting");
yield return new WaitForSeconds(5);
print("5 seconds has passed");
}
这样的写法适合一次性等待。如果要重复延迟,先缓存一下WaitForSeconds对象好一些(不用每次都new)
WaitForSeconds delay = new WaitForSeconds(1);
Coroutine coroutine;
void Start()
{
StartCoroutine("MyCoroutine");
}
IEnumerator MyCoroutine()
{
int i= 100;
while (i>0)
{
// Do something 100 times
i--;
yield return delay;
}
// All Done!
}
Wait for Seconds Real Time执行完全相同的功能,但使用未缩放的时间。这意味着即使更改时间刻度,它仍然可以工作,例如暂停游戏时。
IEnumerator WaitFiveSeconds()
{
// Pause the game
Time.timeScale = 0;
yield return new WaitForSecondsRealtime(5);
print("You can't stop me");
}
Yield Return Wait Until / Wait While (等待委托)
Wait Until暂停执行,直到委托评估为真,而Wait While等待它为假后再继续。
以下是它在脚本中的使用:
int fuel=500;
void Start()
{
StartCoroutine(CheckFuel());
}
private void Update()
{
fuel--;
}
IEnumerator CheckFuel()
{
yield return new WaitUntil(IsEmpty);
print("tank is empty");
}
bool IsEmpty()
{
if (fuel > 0)
{
return false;
}
else
{
return true;
}
}
等待帧结束
此特定指令会等到 Unity 渲染完每个 Camera 和 UI 元素,然后才实际显示帧。一个典型的用途是截屏。
IEnumerator TakeScreeshot()
{
// Waits until the frame is ready
yield return new WaitForEndOfFrame();
CaptureScreen();
}
等待另一个协程
最后,可以让 yield 直到另一个由 yield 语句触发的协程完成执行。
只需在 yield return 之后使用 Start Coroutine 方法,如下所示:
void Start()
{
StartCoroutine(MyCoroutine());
}
IEnumerator MyCoroutine()
{
print("Coroutine has started");
yield return StartCoroutine(MyOtherCoroutine());
print("Coroutine has ended");
}
IEnumerator MyOtherCoroutine()
{
int i = 3;
while (i>0)
{
// Do something 3 times
i--;
yield return new WaitForSeconds(1);
}
print("All Done!");
}
在启动的协程完成后,代码将继续执行
如何启动协程
启动协程有两种方法,可以用函数名的字符串启动:
void Start()
{
StartCoroutine("MyCoroutine");
}
IEnumerator MyCoroutine()
{
// Coroutine business...
}
也可以捅过引用方法名称来启动协程(和常规函数一样)
void Start()
{
StartCoroutine(MyCoroutine());
}
IEnumerator MyCoroutine()
{
// Do things...
}
这两种技术都是 Start Coroutine 的重载方法。多数情况下,它们非常相似,但有几个关键区别。
首先,使用字符串方法而不是名称方法会对性能造成轻微影响。
另外,使用字符串启动协程时,只能传入一个参数,如下所示:
void Start()
{
StartCoroutine("MyCoroutine", 1);
}
IEnumerator MyCoroutine(int value)
{
// Code goes here...
}
然而,当停止协程时,会注意到使用一种方法与另一种方法之间的最大区别。
如何结束协程
协程在其代码执行后自动结束。您不需要显式结束协程。然而,我们可能希望在协程完成之前手动结束它。这可以通过几种不同的方式来完成。
从协程内部(使用yield break)
添加 yield break 语句将在协程完成之前结束它。这对于作为条件语句的结果退出协程很有用。
IEnumerator MyCoroutine(float value)
{
if (value > 10)
{
// Do one thing
yield break;
}
// Do another
}
这允许您创建可以退出协程的条件代码路径。
但是如果你想意外地停止一个协程怎么办。例如,想完全取消协程正在执行的操作。幸运的是,协程也可以在外部停止。
从协程外部结束(使用停止协程)
使用其字符串停止协程
如果是使用其字符串启动协程,则可以使用相同的字符串再次停止它。像这样
StopCoroutine("MyCoroutine");
但是,如果使用相同的字符串启动了多个协程,则在使用此方法时所有协程都将停止。
那么,如果想停止一个特定的协程实例怎么办?
通过引用停止协程
如果您在启动时存储对该 Coroutine 的引用,则可以停止特定的 Coroutine 实例。
bool stopCoroutine;
Coroutine runningCoroutine;
void Start()
{
runningCoroutine = StartCoroutine(MyCoroutine());
}
void Update()
{
if (stopCoroutine == true)
{
StopCoroutine(runningCoroutine);
stopCoroutine = false;
}
}
IEnumerator MyCoroutine()
{
// Coroutine stuff...
}
停止 MonoBehaviour 上的所有协程
停止协程最简单、最可靠的方法是调用Stop All Coroutines。
像这样:
StopAllCoroutines();
这将停止由调用它的脚本启动的所有协程,因此它不会影响在其他地方运行的其他协程。