unity 协程 详细说明

前言:
unity协程(coroutine) 其实就是一个枚举器 的封装。下面将会说明协成的实现原理。
本文档将会从 c#枚举器unity协成过程一步步去做说明,帮你深入理解unity 协成( coroutine)。

demo下载地址

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 IEnumerat orTest
{
    public  I E nume r ator  GetEnumerator()
    {
        yield return 1;
        yield return 2;
        yield return "枚举器";
    }
}
控制台打印:



4.yeild解释说明

包含yield语句的方法或属性称为迭代块。如上面代码:

    public  I E nume r ator  GetEnumerator()
    {
        yield return 1;
        yield return 2;
        yield return "枚举器";
    }
这个语句块在编译时将会编译成一个yield类型,其中包含一个状态机。 yield类型实现 IEnumerator和IDisposable接口的属性和方法。
如果你感到迷惑,就编译成IL中间语言看一下。这里就不做说明了,上张图,看一下大体明白就行:



上面的
class IEnumerat orTest
{
    public  I E nume r ator  GetEnumerator()
    {
        yield return 1;
        yield return 2;
        yield return "枚举器";
    }
}
类将会被编译成如下类似的代码,yield类型为IEnumeratorTest类的一个内部类Enumerator,外部类IEnumeratorTestGetEnumerator()方法实例化并返回一个新的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()等方法,如果你不怕麻烦。
5.1枚举器的封装到协程
上面我们对枚举器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()方法实现的。
转载请标注文章来源。尊重他人的劳动成果。

文章原地址   demo下载地址


猜你喜欢

转载自blog.csdn.net/fengya1/article/details/79386936
今日推荐