协程是Unity一个刚开始看起来很诡异但是实际用起来很方便的一种用法。
在游戏运行的时候,总是需要读秒,我之前写过一个游戏,就是炮塔看见人物就等三秒然后开枪,最开始用Time类去读秒,效果勉强能用,但是炮塔多了之后整个脚本逻辑看起来痛苦不已。但是协程就非常好的实现了读秒、读帧、读逻辑的功能。
我们在脚本里实现一遍协程是很简单的,但是协程背后是不容易整明白的。协程的语法对于曾经的我来说特别晦涩。什么IEnumerator、IEnumerable这种东西又不会读又不会写,还不知道干嘛的,再加上yield return,整个人虽然会用,提起协程来也是蒙圈的。
我上一篇文章就专程讲了迭代器,IEnumerator和IEnumerable都是在枚举接口,它们在C# 2.0以后支持了迭代器的功能,即yield return的逻辑休克。这一篇文章我们来讲讲协程在Unity中对迭代器的实现。
协程在Unity里的C#中,主要就是暂停一段逻辑等待某一个功能完成后再执行剩下的逻辑,由于我们Unity脚本中调用得最多的是Update函数,这个函数是没有任何理由条件来暂停它的。所以Unity引入了协程的概念。
1.协程由StartCoroutine()来开启协程。使用StopCoroutine()来关闭协程。
2.StartCoroutine和StopCoroutine方法参数类型为IEnumerator。但也可以支持字符串类型。以下是StartCoroutine的重载版本:
3.StartCoroutine是没有返回值的,当碰到迭代器里的yield return后,它会等待我们的返回类型完成,自动为我们执行MoveNext方法,来让协程可以在yield return后执行下去。
StartCoroutine虽然参数只能是IEnumerator,但是我们可以创建IEnumerable的迭代器给它,两种迭代器完成同样的功能,但是IEnumerable的迭代器在给协程的时候需要一些转换的。我把两种迭代器放在一起看看用法,例子如下:
public class CSharpTest : MonoBehaviour
{
public int value = 11;
void Start()
{
IEnumerable enumerable = Countable(value);
IEnumerator enumerator = enumerable.GetEnumerator();
StartCoroutine(enumerator);
StartCoroutine(Countor(value));
}
IEnumerable Countable(int value)
{
yield return new WaitForSeconds(3f);
Debug.Log("IEnumerable" + value);
}
IEnumerator Countor(int value)
{
yield return new WaitForSeconds(3f);
Debug.Log("IEnumerator" + value);
}
}
两个的效果是一模一样的的 我们执行看看:
两个都是同样可以执行的。
协程背后就是迭代器,但是我们要自己使用迭代器实现协程的功能却是不容易,整了老半天,我自己整了个和官方功能相似的延时:
public class CSharpTest : MonoBehaviour
{
private IEnumerator enumerator;
void Start()
{
StartCoroutine(CountTime());
enumerator = HandEnum();
enumerator.MoveNext();
}
void FixedUpdate()
{
if(enumerator.Current is myWaitForSecond)
{
var Current = enumerator.Current as myWaitForSecond;
if(Time.time>Current.waitTime)
{
enumerator.MoveNext();
}
}
}
IEnumerator CountTime()
{
Debug.Log("官方协程:开始计时!");
for (int i = 1; i <= 12;i++)
{
Debug.Log("官方协程:过了" + i + "秒");
yield return new WaitForSeconds(1f);
}
}
IEnumerator HandEnum()
{
Debug.Log("手动开启协程,等待三秒");
yield return new myWaitForSecond(3f);
Debug.Log("手动协程三秒等待完毕!");
Debug.Log("手动又让协程等待五秒");
yield return new myWaitForSecond(5f);
Debug.Log("手动协程五秒等待完毕");
}
}
class myWaitForSecond
{
public float waitTime;
public myWaitForSecond(float t)
{
waitTime = Time.time + t;
}
}
二者效果是一样的,看看效果:
我们可以看到,在协程读秒的时候,我们手动协程一样也完成了它的工作,在过了三秒和五秒的时候准时输出了。
但是,我们定义自己的myWaitForSecond的时候,里面并没有太多的内容,除了一个字段和一个构造函数外别无他物,我们在官方的waitForSecond里面也可以看到,里面实际上非常简单:
那么实际上的那些时延操作是谁来完成的呢,留给我们想象空间的只剩下一个,就是一开始的StartCoroutine。
一般而言,StartCoroutine就是简单的对某个IEnumerator 进行MoveNext()操作,但如果他发现IEnumerator其实是一个WaitForSeconds类型的话,那么他就会进行特殊等待,一直等到WaitForSeconds延时结束了,才进行正常的MoveNext调用,而至于WWW或者WaitForFixedUpdate等类型,StartCoroutine也是同样的特殊处理。
那么,我们没有使用StartCoroutine的IEnumerator就只能自己写逻辑决定什么情况来MoveNext了。
Unity在LateUpdate结束后才会检测协程的yield return是否满足然后再确定是否执行MoveNext。
对于协程来说,按理说我们设定了某个条件,那么协程会在该条件完成后进行MoveNext。
但是我们游戏是一帧一帧来算的。在协程中,会在LateUpdate后对条件是否满足进行判断,我们可以写个例子验证一下:
public class CSharpTest : MonoBehaviour
{
private bool isUpdateCor = false;
private bool isLateUpdateCor = false;
void Start()
{
StartCoroutine(Startfunction());
}
IEnumerator Startfunction()
{
Debug.Log("Start协程开始");
yield return null;
Debug.Log("Start协程结束");
}
void Update()
{
if(!isUpdateCor)
{
StartCoroutine(UpdateFunction());
isUpdateCor = true;
}
}
IEnumerator UpdateFunction()
{
Debug.Log("Update协程开始");
yield return null;
Debug.Log("Update协程结束");
}
void LateUpdate()
{
if(!isLateUpdateCor)
{
StartCoroutine(LateUpdateFunction());
isLateUpdateCor = true;
}
}
IEnumerator LateUpdateFunction()
{
Debug.Log("LateUpdate协程开始");
yield return null;
Debug.Log("LateUpdate协程结束");
}
}
按照设想,协程一旦在yield return后的条件满足就MoveNext,而且我们这里是返回null,按理说应该执行完yield return后立马输出后面那个debug语句,但是事实上:
如上图,直到LateUpdate函数结束后,我们的StarFunction的yield return后的语句才开始返回它的声明。
这个例子表明:Unity在LateUpdate函数结束后才会检测协程的yield return是否满足然后再确定是否执行MoveNext。
以下这张图效果很好:
2.
StopCoroutine是协程里的一个大坑,我们通常以为只要StopCoroutine里的参数表达形式和StartCoroutine对应就好了。。。
关闭一个协程有三个方法,这三个方法必须一一对应:
例如我们有个迭代器CountTime,那么协程必须是这样声明才能关闭:
1.创建IEnumerator实例开启协程,StopCoroutine里的参数必须是IEnumerator实例。
IEnumerator x = CountTime();
StartCoroutine(x);
......//此处省略逻辑
StopCoroutine(x);
2.创建Coroutine实例,StopCoroutine里的参数必须是Coroutine实例。
Coroutine x;
x = StartCoroutine(CountTime());
......//此处省略逻辑
StopCoroutine(x);
3.使用字符串开关协程。
StartCoroutine("CountTime");
......//此处省略逻辑
StopCoroutine("CountTime");
这个是真的坑,坑炸了。
3.
除了StopCoroutine以外,可以停止协程脚本所在的游戏对象来停止协程(gameObject.SetActive(false)),
只停止协程所在脚本不能停止协程。(MonoBehaviour.enabled = false)
有:
使用MonoBehaviour.enabled = false 协程会照常运行,但 gameObject.SetActive(false) 后协程却全部停止,即使在Inspector把gameObject 激活还是没有继续执行。并且当我们使用gameObject.SetActive(false) 后,协程不会立即暂停,而是会等待协程执行到下一次yield return后才会关闭协程。
我们看个例子:
public class CSharpTest : MonoBehaviour
{
private int i;
void Start()
{
StartCoroutine(CountTime());
}
IEnumerator CountTime()
{
Debug.Log("官方协程:开始计时!");
for (i = 0; i <= 12;i++)
{
Debug.Log("这是第" + i + "次哈哈哈");
yield return new WaitForSeconds(1f);
}
}
void Update()
{
if(i>6)
{
this.gameObject.SetActive(false);
}
}
}
输出为:
我们可以看到,我们i>6的情况是在for循环的i++后就等于7了,此时游戏对象关闭,但是还是输出了“这是第7次哈哈哈”。
我们在协程yield return new以后可以看到,有以下这些类可以接在后面
我们写一下最后一个功能的情况:
public class CSharpTest : MonoBehaviour
{
void Start()
{
StartCoroutine(Function());
}
IEnumerator Function()
{
yield return StartCoroutine(CountTime());
Debug.Log("主委托等待另一个委托读秒完成");
}
IEnumerator CountTime()
{
Debug.Log("我是另一个委托,读一秒");
yield return new WaitForSeconds(1f);
Debug.Log("另一个委托读秒结束");
}
}
如图,主委托Function直到另一个委托CountTime逻辑全部执行完毕后才接着执行自己的逻辑。
参考文章:https://www.iteye.com/blog/dsqiu-2029701