Coroutine,你究竟干了什么?

  随着自己对C#有了进一步的了解,我才慢慢发现,上面所言的那两个奇怪的IEnumeratoryield return,其实并不是Unity的什么独创,相反,他们却是C#中到处可见的迭代器的构造方式(之一),你也许对于迭代器这个东西没什么印象,但实际上,我们可能天天都在使用它!让我们马上来看一个最普遍的迭代器运用:

  int[] array = new int[] {1, 2, 3, 4, 5};
            
  foreach (int val in array) {
      // do something
  }

  代码非常简单,不过是使用foreach来遍历一个整型数组,而代码中我们早已习以为常的foreach其实就是迭代器的语法糖,在真正的运行代码中,C#的编译器会将上面的代码改头换面成这个样子:

  int[] array = new int[] {1, 2, 3, 4, 5};
            
  IEnumerator e = array.GetEnumerator();
  while (e.MoveNext()) {
      // do something
  }

  上述代码首先通过arrayGetEnumerator方法来获取array的一个“迭代器”,然后通过“迭代器”的MoveNext方法进行依次遍历,而这“迭代器”实际上就是之前那个稍显奇怪的IEnumerator类型!而至于yield return,其实是C# 2.0新引进的一种实现迭代器模式的简便语法,在之前的C# 1.0中,如果要实现一个完整的迭代器,我们必须要分别实现IEnumerableIEnumerator这两个接口,过程略显枯燥繁琐,而借助yield return,这两个步骤我们都可以省略!譬如我们写下了如下的代码:

IEnumerator Test() {
    yield return 1;
    yield return 2;
    yield return 3;
}

那么C#编译器就会帮你自动生成类似下面的这些代码(不准确,仅作示意):

public class InnerEnumerable : IEnumerable {
    public class InnerEnumerator : IEnumerator {
        int[] array = new int[] {1, 2, 3};
        int currentIndex = -1;
                
        public bool MoveNext() {
            ++currentIndex;
            return currentIndex < array.Length;
        }
                
        public Object Current {
            get { return array[currentIndex]; }
        }
                    
        public void Reset() {
            throw new Exception("unsurport");
        }
    }
            

public IEnumerator GetEnumerator() {
        return new InnerEnumerator();
    }
}        
        

IEnumerator Test() {
     InnerEnumerable e = new InnerEnumerable();
     return e.GetEnumerator();
 }


  当然,实际的迭代器代码实现远非如此简单,但原理上基本可以看做是一个有限状态机,有兴趣的朋友可以看看更深入的一些介绍,譬如这里这里

  OK,让我们继续回到Unity,通过上面的这些分析,我们大概就肯定了这么一点:Unity其实是使用了迭代器来实现延时的,像IEnumerator、yield return等的使用皆是为了配合C#中迭代器的语法,其与什么多线程之类的概念并没有多少关系,但是目前我仍然还是不能理解之前的那个最大疑问:虽然迭代器可以保留运行状态以便下次继续往下运行,但是他本身并没有提供什么机制来达到延时之类的效果,像foreach这种语句,虽然使用了迭代器,但实际上也是一股脑儿运行完毕的,并不存在延时一说,那么在Unity中,为什么简单的返回一个WaitForSeconds就可以呢?

  三 Coroutine原来如此 :)

  看来答案应该是在WaitForSeconds这个类型身上了~经过简单的一些搜索,我找到了这么一篇帖子,内容便是如何自己实现一个简单的WaitForSeconds,大体上的思路便是使用循环yield return null这种方法来达到延时的目的,直接抄一段帖子中的示例代码:

using UnityEngine; 

using System.Collections; 

 

public class TimerTest : MonoBehaviour { 

    IEnumerator Start () {

        yield return StartCoroutine(MyWaitFunction (1.0f));

        print ("1");

        yield return StartCoroutine(MyWaitFunction (2.0f));

        print ("2");

    }

 

    IEnumerator MyWaitFunction (float delay) {

        float timer = Time.time + delay;

        while (Time.time < timer) {

            yield return null;

        }

    }

}


  也就是说,如果我们在代码中写下了如下的延时语句:

yield return WaitForSeconds(1.0f);

  那么在逻辑上,其大概等价于下面的这些语句:

   float timer = Time.time + 1.0f;

   while (Time.time < timer) {

       yield return null;

   }


  而完成这些操作的,很可能便是WaitForSeconds的构造函数,因为每次延时我们都就地生成(new)了一个WaitForSeconds实例。

  然而使用ILSpy查看WaitForSeconds实现源码的结果却又让我迷惑:WaitForSeconds的构造函数非常简单,似乎仅是记录一个时间变量罢了,根本就不存在什么Whileyield之类的东西,而其父类YieldInstruction则更简单,就是单纯的一个空类……另外的,WWW这个Unity内建类型的使用方式也同样让我不解:

using UnityEngine;

using System.Collections;

public class Example : MonoBehaviour {

    public string url = "http://images.earthcam.com/ec_metros/ourcams/fridays.jpg";

    IEnumerator Start() {

        WWW www = new WWW(url);

        yield return www;

        renderer.material.mainTexture = www.texture;

    }

}

  在上面的示例代码中,yield return www;这条语句可以做到直到url对应资源下载完毕才继续往下运行(迭代),效果上类似于WaitForSeconds,但是WWW本身却又不像WaitForSeconds那样是个YieldInstruction,而且在使用上也是首先创建实例,然后直接yield 返回引用,按照这种做法,即便WWW的构造函数使用了上面的那种循环yield return null的方法,实际上也达不到我们想要的等待效果;再者便是语法上的一些细节,首先如果我们需要使用yield return的话,返回类型就必须是IEnumerable(<T>)或者IEnumerator(<T>)之一,而C#中的构造函数是没有返回值的,显然不符合这个原则,所以实际上在构造函数中我们无法使用什么yield return,另外的一点,虽然上述帖子中的方法可以实现自己的延时操作,但每次都必须进行StartCoroutine操作(如果没有也起不到延时效果),这一点也与一般的WaitForSeconds使用存在差异……

  后来看到了这篇文章,才大抵让我有所释怀:之前自己的种种猜测都聚焦在类似WaitForSeconds这些个特殊类型之上,一直以为这些类型肯定存在某些个猫腻,但实际上,这些类型(WaitForSecondsWWW之类)都是“非常正常”的类型,并没有什么与众不同之处,而让他们显得与众不同的,其实是StartCoroutine这个我过去一直忽略的家伙!

  原理其实很简单,WaitForSeconds本身是一个普通的类型,但是在StartCoroutine中,其被特殊对待了,一般而言,StartCoroutine就是简单的对某个IEnumerator 进行MoveNext()操作,但如果他发现IEnumerator其实是一个WaitForSeconds类型的话,那么他就会进行特殊等待,一直等到WaitForSeconds延时结束了,才进行正常的MoveNext调用,而至于WWW或者WaitForFixedUpdate等类型,StartCoroutine也是同样的特殊处理,如果用代码表示一下的话,大概是这个样子:

foreach(IEnumerator coroutine in coroutines)

{

    if(!coroutine.MoveNext())

        // This coroutine has finished

        continue;

 

    if(!coroutine.Current is YieldInstruction)

    {

        // This coroutine yielded null, or some other value we don't understand; run it next frame.

        continue;

    }

 

    if(coroutine.Current is WaitForSeconds)

    {

        // update WaitForSeconds time value

    }

    else if(coroutine.Current is WaitForEndOfFrame)

    {

        // this iterator will MoveNext() at the end of the frame

    }

    else /* similar stuff for other YieldInstruction subtypes or WWW etc. */

}

基于上述理论,我们就可以来实现自己的WaitForSeconds了:

首先是CoroutineManager,我们通过他来实现类似于StartCoroutine的功能:

//

//    <maintainer>Hugo</maintainer>

//    <summary>simple coroutine manager class</summary>

//

using UnityEngine;

using System.Collections.Generic;

public class CoroutineManager : MonoBehaviour {

public static CoroutineManager Instance {

    get;

private set;

}

List<System.Collections.IEnumerator> m_enumerators = new List<System.Collections.IEnumerator>();

List<System.Collections.IEnumerator> m_enumeratorsBuffer = new List<System.Collections.IEnumerator>();

void Awake() {

    if (Instance == null) {

    Instance = this;

}

else {

    Debug.LogError("Multi-instances of CoroutineManager");

}

}

void LateUpdate() {

    for (int i = 0; i < m_enumerators.Count; ++i) {

// handle special enumerator

if (m_enumerators[i].Current is CoroutineYieldInstruction) {

    CoroutineYieldInstruction yieldInstruction = m_enumerators[i].Current as CoroutineYieldInstruction;

if (!yieldInstruction.IsDone()) {

    continue;

}

}

// other special enumerator here ...

// do normal move next

if (!m_enumerators[i].MoveNext()) {

    m_enumeratorsBuffer.Add(m_enumerators[i]);

continue;

}

}

// remove end enumerator

for (int i = 0; i < m_enumeratorsBuffer.Count; ++i) {

    m_enumerators.Remove(m_enumeratorsBuffer[i]);

}

m_enumeratorsBuffer.Clear();

}

public void StartCoroutineSimple(System.Collections.IEnumerator enumerator) {

m_enumerators.Add(enumerator);

}

}

接着便是我们自己的WaitForSeconds了,不过在此之前我们先来实现WaitForSeconds的基类,CoroutineYieldInstruction

//

//    <maintainer>Hugo</maintainer>

//    <summary>coroutine yield instruction base class</summary>

//

using UnityEngine;

using System.Collections;

public class CoroutineYieldInstruction {

public virtual bool IsDone() {

    return true;

}

}


  很简单不是吗?类型仅有一个虚拟的IsDone方法,上面的CoroutineManager就是依据此来进行迭代器迭代的,OK,该是我们的WaitForSeconds上场了:

//

//    <maintainer>Hugo</maintainer>

//    <summary>coroutine wait for seconds class</summary>

//

using UnityEngine;

using System.Collections;

public class CoroutineWaitForSeconds : CoroutineYieldInstruction {

float m_waitTime;

float m_startTime;

public CoroutineWaitForSeconds(float waitTime) {

m_waitTime = waitTime;

m_startTime = -1;

}

public override bool IsDone() {

// NOTE: a little tricky here

if (m_startTime < 0) {

    m_startTime = Time.time;

}

// check elapsed time

return (Time.time - m_startTime) >= m_waitTime;

}

}

原理非常简单,每次IsDone调用时进行累时,直到延时结束,就这么简单 :)

写个简单的案例来测试一下:

//

//    <maintainer>Hugo</maintainer>

//    <summary>coroutine test case</summary>

//

using UnityEngine;

using System.Collections;

public class CoroutineTest: MonoBehaviour {

void Start() {

// start unity coroutine

StartCoroutine(UnityCoroutine());

    // start self coroutine

CoroutineManager.Instance.StartCoroutineSimple(SelfCoroutine());

}

IEnumerator UnityCoroutine() {

Debug.Log("Unity coroutine begin at time : " + Time.time);

yield return new WaitForSeconds(5);

Debug.Log("Unity coroutine begin at time : " + Time.time);

}

IEnumerator SelfCoroutine() {

Debug.Log("Self coroutine begin at time : " + Time.time);

yield return new CoroutineWaitForSeconds(5);

Debug.Log("Self coroutine begin at time : " + Time.time);

}

}

效果虽然不如原生的WaitForSeconds那么精确,但也基本符合期望,简单给张截图:


四 尾声

  Coroutine这个东西对于我来说确实比较陌生,其中的迭代原理也困扰了我许久,不少抵触情绪也“油然而生”(在此自我反省一下),但是经过简单的一阵子试用,我却赫然发现自己竟然离不开他了!究其原因,可能是其简洁高效的特性深深折服了我,想想以前那些个分散于代码各处的计时变量和事件逻辑,现在统统都可以做成一个个Coroutine,不仅易于理解而且十分高效,我相信不管是谁,在实际使用了Unity中的Coroutine之后,都会对他爱不释手的:)当然,这么好的东西网上自然早以有了非常优秀的介绍,有兴趣的朋友可以仔细看看 :)

  好了,就这样吧,下次再见了~

原文地址:http://blog.csdn.net/tkokof1/article/details/11842673

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值