第十八章 协程
我们知道脚本都是继承自MonoBehaviour类,而其中的Update方法里面放置了大部分的游戏逻辑处理代码。Update方法是游戏循环的每一帧都去执行,这就要求我们的代码“无时无刻”不在处理所有的可能发生的情况,并做出相应的处理。如果我们想要完成“一段时间”的逻辑代码,例如在游戏中发射一颗子弹,这个过程相对于游戏无限循环的时间来说,非常的短暂,如果我们在Update中去完成这样的逻辑,需要根据时间来进行判断,显然这个完成过程非常的复杂。这个时候,Unity给我们提供了协程,从字面意义上理解就是协助程序的意思,类似于“线程”,帮助我们在主任务进行的同时,需要一些分支任务配合工作来达到最终的效果。但是,协程不是线程,协程依旧是在主线程中进行。Unity中的协程由协程函数和协程调度器两部分构成。通过关键字IEnumerator来定义一个协程(迭代方法),然后再程序中通过StartCoroutine来开启一个协程。
接下来,我们创建一个“SampleScene8”的新创景,然后创建一个“Cube”和一个“CubeCoroutine.cs”的脚本文件,并将他们附加在一起。接下来,我们就在脚本文件中定义一个协程方法,如下所示
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeCoroutine : MonoBehaviour
{
// Update is called once per frame
void Update()
{
// 按下空格键
if (Input.GetKeyDown(KeyCode.Space))
{
// 启动协程
StartCoroutine("MoveTest");
}
}
// 定义协程方法
IEnumerator MoveTest()
{
transform.Translate(new Vector3(1, 0, 0));
yield return null;
transform.Translate(new Vector3(1, 0, 0));
yield return null;
}
}
在上面的代码中,我们使用IEnumerator MoveTest来定义协程方法,方法名称为MoveTest。在协程方法中,我们让Cube沿X轴移动两次,每次移动1个单位长度。然后我们在Update方法中监听空格键的按下,使用StartCoroutine方法来启动协程,方法参数就是协程方法名称。接下来我们就Play当工程,查看运行效果
当我们按下一次空格的时候,Cube会移动两次,每次移动1个单位长度,由于代码执行太快,因此我们看不到两次移动之间的间隔。但是从Inspector检视面板中的X数值可以看到,我们每次按下空格的时候,Cube的确是移动了两个单位的长度。
当然,这样简单的代码并不能展示协程的强大之处。我们先从协程方法里面的return解释一下。在普通方法里面,我们经常使用return方法来返回方法的执行的结果值,但是在协程方法里面,这个return并不是返回结果值。上文代码中的“yield return null;”表示暂停协程等待下一帧继续执行。这样的返回设计,让我们能够控制在指定数量的帧中来执行不同的代码,而不是在所有的帧中执行复杂的条件代码。什么意思呢?
transform.Translate(new Vector3(1, 0, 0));
yield return null;
transform.Translate(new Vector3(1, 0, 0));
yield return null;
我们的协程方法执行后,会在Update方法中执行transform.Translate(new Vector3(1, 0, 0))代码,也就是让Cueb沿X轴移动1个单位的距离。然后使用yield return null停止协程方法等待下一次Update方法。下一次Update方法执行后,就会执行协程中第二个transform.Translate(new Vector3(1, 0, 0))代码,继续让Cueb沿X轴移动1个单位的距离。然后使用yield return null停止协程方法。至此,协程方法执行完毕。
我们回头思考一下发射子弹的协程该如何实现呢?当我们按下空格键代表发射动作的时候,我们可以启动一个子弹飞行协程,在这个协程方法中,我们可以根据指定的速度和射程来移动子弹,协程结束也就代表子弹的飞行逻辑结束。但是,这个需要借助精确的时间来实现。此时,我们可以借助使用“yield return new WairForSeconds(时间);”来表示等待规定时间后继续执行,这样让我们从时间角度出发来执行我们的代码,而不用考虑他们到底在那一帧中去执行。我们稍微改动我们的代码。
// 定义协程方法
IEnumerator MoveTest()
{
transform.Translate(new Vector3(1, 0, 0));
//yield return null;
yield return new WaitForSeconds(1.0f);
transform.Translate(new Vector3(1, 0, 0));
yield return null;
}
说白了,我们让两次移动间隔1秒钟。接下来,我们在来Play当前工程,查看运行效果
这次当我们按下空格键的时候,两次移动之间由于间隔了1秒钟,因此我们看的比较清楚了。从左边的Inspector检视视图中我们也能够看到X从0变到1,再变到2的过程了。接下来,我们回到子弹发射的问题上来。注意,子弹的速度我们可以固定为一个数值,然后射击的距离也可以固定为一个数值。那么,在那个时间点,子弹飞行到哪里,我们就能够计算出来了。如果觉得1秒钟不精确的话,我们可以减小这个时间,这样子弹位置的更新就比较连续了。最后,协程方法也可以定义形参,那么在使用StartCoroutine调用的时候,可以不使用字符串而直接调用方法。代码如下:
// Start is called before the first frame update
void Start()
{
StartCoroutine(ParamTest(100));
}
// 定义包含参数的协程方法
IEnumerator ParamTest(int x)
{
Debug.Log(x);
yield return null;
}
运行结果如下:
注意:可以使用 StopCoroutine 和 StopAllCoroutines 来停止协程。 当用 SetActive(false) 禁用某个协程所附加到的游戏对象时,该协程也将停止。调用 Destroy销毁游戏对象时候,也会停止自身的协程。但通过在 MonoBehaviour 实例上将 enabled 设置为 false 来禁用 MonoBehaviour 时,协程不会停止。
总结:协程只启动一次。而后Unity会在后续的update中执行协程中的循环体。有多次循环,就会在多少次update去分别执行循环体代码。说白了,协程就是将一个过程化的行为分解到了update方法中执行。Unity中的协程可以有两种用途:第一,延迟调用;第二,分解操作,把一个过程分解执行。例如,敌人死亡后淡出消失,改变材质颜色透明度即可,让透明度由1变成0,这是一个过程,可以使用协程来实现。再比如,我们可以通过协程进行寻路A点,B点,C端(巡逻)。其实现方式就是协程嵌套协程,一个协程完成从一个点A移动到另一个点B后,本协程结束后再开启下一个协程,然后从当前点B在继续移动到下一个点C。
本课程涉及的内容已经共享到百度网盘:https://pan.baidu.com/s/1e1jClK3MnN66GlxBmqoJWA?pwd=b2id