unity 协程 详细说明
前言:
unity协程(coroutine) 其实就是一个枚举器 的封装。下面将会说明协成的实现原理。
本文档将会从c#枚举器到unity协成过程一步步去做说明,帮你深入理解unity 协成(coroutine)。
1.c#枚举器是什么?
其实你只要用过List泛型列表遍历元素(foreach),你就会用到枚举器 。
如下面脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class yeidTest : MonoBehaviour {
void Start()
{
listForeachTest();
}
List<int> listForeach = new List<int>();
private void listForeachTest()
{
for (int i = 0; i < 5; i++)
{
listForeach.Add(i);
}
foreach (var item in listForeach)
{
Debug.Log("枚举元素:"+item);
}
}
}控制台输出:
测试代码demo中的 枚举器举例子
只要能使用foreach遍历元素的类型都会用到枚举器,如Dictionary<>,ArrayList,List<>等类型。
2.枚举器到底是什么呢?
foreach 关键字在编译后将会编译成如下形式的代码:
IEnumerator ie = listForeach.GetEnumerator();
while (ie.MoveNext())
{
Debug.Log("枚举元素:" + ie.Current);
}
测试代码demo中如下:
测试代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class yeidTest : MonoBehaviour {
void Start()
{
listForeachTest();
}
List<int> listForeach = new List<int>();
private void listForeachTest()
{
for (int i = 0; i < 5; i++)
{
listForeach.Add(i);
}
IEnumerator ie = listForeach.GetEnumerator();
while (ie.MoveNext())
{
Debug.Log("枚举元素:" + ie.Current);
}
//foreach (var item in listForeach)
//{
// Debug.Log("枚举元素:"+item);
//}
}
}控制台输出:
while循环和foreach效果一样。在下面的讲解中我们枚举元素将使用while循环的方法枚举元素,不再使用foreach关键字。以便更好的说明协程。
看到IEnumerator大家就眼熟了吧。实现协程的IEnumerator就是枚举器的接口。
f12 查看IEnumerator接口的定义
using System.Runtime.InteropServices;
namespace System.Collections
{
[ComVisible(true)]
[Guid("496B0ABF-CDEE-11D3-88E8-00902754C43A")]
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
}成员说明:
Current 遍历当前类型时,存储当前元素。
MoveNext() 每调用一次,移动到下一个元素,返回下一个元素是否为空
Reset() 重置到列表最开始
枚举器就是实现IEnumerator接口,通过MoveNext()获取下一个元素来遍历每个元素的方法。
3. yield关键字
yield return返回集合(如链表List<>)的一个元素,并且移动到下一个元素。特别注意:如果一个类定义了一个IEnumerator 返回值的GetEnumerator()方法,那么这个类就可以枚举成员。
如下代码:
class IEnumeratorTest
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return "枚举器";
}
}现在就可以使用foreach迭代集合了。
测试代码demo中如下:
所有代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class yeidTest : MonoBehaviour {
void Start()
{
IEnumeratorTest enumeratorTest = new IEnumeratorTest();
foreach (var item in enumeratorTest)
{
Debug.Log(item);
}
}
}
class IEnumeratorTest
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return "枚举器";
}
}
控制台打印:
4.yeild解释说明
包含yield语句的方法或属性称为迭代块。如上面代码:
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return "枚举器";
}
这个语句块在编译时将会编译成一个yield类型,其中包含一个状态机。yield类型实现 IEnumerator和IDisposable接口的属性和方法。
如果你感到迷惑,就编译成IL中间语言看一下。这里就不做说明了,上张图,看一下大体明白就行:
上面的
class IEnumeratorTest
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return "枚举器";
}
}
类将会被编译成如下类似的代码,yield类型为IEnumeratorTest类的一个内部类Enumerator,外部类IEnumeratorTest的GetEnumerator()方法实例化并返回一个新的yield类型。
在yield类型中,变量state定义当前迭代位置,每次MoveNext()方法后,改变当前迭代位置为下一个元素位置,并且设置current为当前迭代位置的一个对象。一下代码,主要看MoveNext()就行:
public class IEnumeratorTest
{
public IEnumerator GetEnumerator()
{
return new Enumerator(0);
}
public class Enumerator : IEnumerator<object>, IEnumerator, IDisposable
{
private int state;
private object current;
public Enumerator(int state)
{
this.state = state;
}
public object Current
{
get
{
return current;
}
}
public void Dispose()
{
}
public bool MoveNext()
{
switch (state)
{
case 0:
state++; //改变当前迭代位置为下一个元素位置
current = 1; //当前迭代位置的对象
return true;
case 1:
state++; //改变当前迭代位置为下一个元素位置
current = 2; //当前迭代位置的对象
return true;
case 2:
state++; //改变当前迭代位置为下一个元素位置
current = "枚举器"; //当前迭代位置的对象
return true;
}
return false;
}
public void Reset()
{
}
}
}
测试代码demo中如下:
所有测试代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class YeildWhat : MonoBehaviour {
void Start()
{
IEnumeratorTest enumeratorTest = new IEnumeratorTest();
#region 使用foreach 和下面while循环是一样的---因为最终会编译成while循环,上面已经说明了
//foreach (var item in enumeratorTest)
//{
// Debug.Log(item);
//}
#endregion
IEnumerator ie = enumeratorTest.GetEnumerator();
while (ie.MoveNext())
{
Debug.Log("枚举元素:" + ie.Current);
}
}
}
public class IEnumeratorTest
{
public IEnumerator GetEnumerator()
{
return new Enumerator(0);
}
public class Enumerator : IEnumerator<object>, IEnumerator, IDisposable
{
private int state;
private object current;
public Enumerator(int state)
{
this.state = state;
}
public object Current
{
get
{
return current;
}
}
public void Dispose()
{
}
public bool MoveNext()
{
switch (state)
{
case 0:
state++; //改变当前迭代位置为下一个元素位置
current = 1; //当前迭代位置的对象
return true;
case 1:
state++; //改变当前迭代位置为下一个元素位置
current = 2; //当前迭代位置的对象
return true;
case 2:
state++; //改变当前迭代位置为下一个元素位置
current = "枚举器"; //当前迭代位置的对象
return true;
}
return false;
}
public void Reset()
{
}
}
}将代码copy到脚本测试:
打印如下:
枚举器其实就是通过每次调用MoveNext()方法,来改变集合中位当前元素位置,来一个个遍历元素。类似通过更改下标来获取元素。yield关键字其实是实现IEnumerator 等接口
如MoveNext()方法的编译器自动编译的关键字。当然你也可以自己实现MoveNext()等方法,如果你不怕麻烦。
上面我们对枚举器IEnumerator枚举器接口和yield关键字做出了解释说明,开始对协程(coroutine)解释说明。
通过unity 的update()模拟协程。
修改上面代码,不使用while循环或者foreach关键字枚举元素。我们将在update()时时刷新来枚举所有元素。原理就是上面所说的每次MoveNext()方法后,改变当前迭代位置为下一个元素位置。
测试代码demo中如下:
代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class update2Coroutine : MonoBehaviour {
Update2CoroutineTest update2CoroutineTest = new Update2CoroutineTest();
IEnumerator e;
public update2Coroutine()
{
e = update2CoroutineTest.GetEnumerator();
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
if (e.MoveNext())
{
}
}
}
public class Update2CoroutineTest
{
public IEnumerator GetEnumerator()
{
Debug.Log("协程:"+1);
yield return 0;
Debug.Log("协程:" + 2);
yield return 0;
Debug.Log("协程:" + "枚举器");
yield return 0; }
}
控制台打印:
5.2协程
现在我们在用startcoroutine()方法启动协程。
测试代码demo中如下:
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoroutineTest : MonoBehaviour {
// Use this for initialization
void Start () {
CoroutineJsTest coroutineJsTest = new CoroutineJsTest();
StartCoroutine(coroutineJsTest.GetEnumerator());
}
// Update is called once per frame
void Update () {
}
}
public class CoroutineJsTest
{
public IEnumerator GetEnumerator()
{
Debug.Log("协程:"+1);
yield return 0;
Debug.Log("协程:" + 2);
yield return 0;
Debug.Log("协程:" + "枚举器");
yield return 0;
}
}控制台打印:
从上面可以看出 ,在update()方法模拟协程和使用unity自带StartCoroutine()方法启动协程效果差不多。看来unity实现的StartCoroutine()启动协程和我们
update()模拟是一样的。但是也不确定到底是不是通过类似update()方法实现的。反编译 UnityEngine.dll程序集也没有找到具体实现方法。。。。。但是唯一确定的
一点就是unity也是通过枚举一步步运行程序块的。类似update()模拟的协程,每次遇到yield return ,就执行yield 类型的
MoveNext()方法,改变当前迭代位置为下一个元素位置。等待下一次MoveNext()方法调用。StartCoroutine()方法会不停的调用MoveNext()方法方法(这样就类似于foreach)。直到枚举结束。
但是注意的是,yield return 后面跟的值除了unity自带的类(如:new WaitForSeconds(0.2f)。集成自 YieldInstruction)
和协程语句块(返回值为 IEnumerator的方法 ) ,其他值 没有意义(yield return 0和yield return null其实都是一样的,只是遇到yield return就做相同处理,不会去处理后面跟的值了)。
下面测试一下update()与协程等待时间
首相打印协程执行的时间间隔和update()的时间间隔
测试代码demo中如下:
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UpDateAndCoroutine : MonoBehaviour {
/// <summary> 是否打印update的时间间隔</summary>
public bool isGetUpdateDeltaTime;
/// <summary> 是否打印协程的间隔时间</summary>
public bool isPrintCoroutineTime;
/// <summary> 设置协程执行的时间间隔</summary>
public float waitTime;
// Use this for initialization
void Start () {
updateCurrentTime= Time.realtimeSinceStartup;
cotoutineCurrentTime = Time.realtimeSinceStartup;
StartCoroutine(GetEnumerator());
}
float updateDeltaTime = 0;
float updateCurrentTime = 0;
// Update is called once per frame
void Update () {
if (isGetUpdateDeltaTime)
{
getUpdateDeltaTime();
}
}
void getUpdateDeltaTime()
{
updateDeltaTime = Time.realtimeSinceStartup - updateCurrentTime;
updateCurrentTime = Time.realtimeSinceStartup;
Debug.Log("deltaTime:" + updateDeltaTime);
}
float cotoutineDeltaTime = 0;
float cotoutineCurrentTime = 0;
public IEnumerator GetEnumerator()
{
for (; ;)
{
updateDeltaTime = Time.realtimeSinceStartup - updateCurrentTime;
updateCurrentTime = Time.realtimeSinceStartup;
Debug.Log("deltaTime:" + updateDeltaTime);
yield return new WaitForSeconds(waitTime);
}
}
}首先设置协程的时间间隔0.5秒和是否打印协程时间间隔,设置如下:
控制台打印:

现在只打印update的时间间隔,设置如下:
打印如下,update时间间隔在0.015左右。
好的现在时间间隔都有了。现在我们设置协程的时间间隔小于update的时间间隔,这里我设置为0.009f:
打印:
你会发现不管你设置的协程时间间隔多小(前提小于update的时间间隔),打印的时间和update的时间非常接近,
这说明你的协程时间间隔最小就是update的时间间隔。不可能再短了。即使你设置的比update时间间隔小。协程也只会执行update 的时间间隔了。
正好也验证了上面所说的:
在update()方法模拟协程和使用unity自带StartCoroutine()方法启动协程效果差不多。看来unity实现的StartCoroutine()启动协程和我们update()模拟是一样的。但是也不确定到底是不是通过类似update()方法实现的。
转载请标注文章来源。尊重他人的劳动成果。