使用过Unity的同学一定知道,Unity提供了一套协程机制,简直不要太好用。但是这个协程依赖于Unity引擎,离开Unity就无法使用。那有没有办法实现不依赖Unity的协程呢?答案是当然阔以。 所谓实现一个协程,就是实现一个迭代器的容器!
Unity的协程
使用过Unity的同学应该都清楚Unity提供的协程,它可以使用的场景非常广泛。比如我们需要在UI打开的时候,延迟一秒钟播放一个动画。代码可以如下写:
public void OnUIOpen()
{StartCoroutine(DelyToPlayAni());
}private IEnumerator DelyToPlayAni()
{yield return new WaitForSeconds(0.5f);//PlayAniMethod
}
我们可以看到StartCoroutine方法的参数是一个返回值很奇怪的函数,这个IEnumerator 是一个什么鬼东西呢?
这要从迭代器模式说起。 迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式。简单来说,迭代器模式使得你能够获取到序列中的所有元素而不用关心是其类型是array,list,linked list或者是其他什么序列结构。这一点使得能够非常高效的构建数据处理通道(data pipeline),即数据能够进入处理通道,进行一系列的变换,或者过滤,然后得到结果。事实上,这正是LINQ的核心模式。
我们来看一下IEnumerator的接口是什么样子的。
using System.Runtime.InteropServices;namespace System.Collections
{[ComVisible(true)]public interface IEnumerator{object Current { get; }bool MoveNext();void Reset();}
}
我们发现IEnumerator属于System.Collections程序集,这说明,这个小东西根本不是Unity创造的,而是C#本身就拥有的东西。也说明,我们完全可以自己实现一个类似Unity的协程嘛!ok,摩拳擦掌,准备大展身手。
什么是IEnumerator
尴尬的问题来了,我们并不是很清楚迭代器到底是什么东西以及它的工作模式。看来开始之前,还有一些知识需要了解。
在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接口,那么就能够被迭代;调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器本身。迭代器类似数据库中的游标,他是数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。 这个只能向前移动的特性就是我们协程需要用到的特性。所谓实现一个协程,就是实现一个迭代器的容器!
早在C#1的时候,C#就提供了一个迭代器,那就是foreach。 使得能够进行比for循环语句更直接和简单的对集合的迭代,编译器会将foreach编译来调用GetEnumerator和MoveNext方法以及Current属性,如果对象实现了IDisposable接口,在迭代完成之后会释放迭代器。 我们可以再回顾下IEnumerator的接口内容,各自含义如下:
object Current{ get; } 当前正在访问的对象,只读
bool MoveNext();移动到下一个元素,我们的协程通过此函数来控制迭代器的到底要不要移动下一个代码段.
如果迭代器返回false,则证明迭代器已经执行完毕,此时协程也应该执行完毕
void Reset();重置函数
开始设计协程
根据以上分析,来我们可以设计我们的协程:需要一个迭代器对象,并且处理其接口相关的函数。
///协程类定义
public sealed class Coroutine
{private IEnumerator _routine;public Coroutine(IEnumerator routine){_routine = routine;}public bool MoveNext(){if (_routine == null)return false;return _routine.MoveNext();}public void Stop(){_routine = null;}
}
如何驱动这个协程呢?我们可以定义一个协程管理器类。
using System;
using System.Collections;
using System.Collections.Generic;namespace Coroutine
{///使用单例模式的协程管理器,用于驱动所有协程MoveNextpublic class CoroutineManager{private static CoroutineManager _instance = null;public static CoroutineManager Instance{get{if (_instance == null)_instance = new CoroutineManager();return _instance;}}///链表存储所有协程对象private LinkedList
}
开始测试协程1.0
我们的协程1.0版本写好了,如何测试呢?简单,这里以C#控制台程序驱动我们的协程为例。不过在正式测试前,还有一些工作要做。
我们需要定义一些辅助的接口和类来模拟Unity的协程控制,如WaitForSeconds等。相关实现代码如下:
using System;
namespace Coroutine
{public class Time{public const float deltaTime &#61; 0.02f;public const int deltaMilTime &#61; 20;}public interface IWait{bool Tick();}public class WaitForSeconds : IWait{public float waitTime &#61; 0;public WaitForSeconds(float time){waitTime &#61; time;}bool IWait.Tick(){waitTime -&#61; Time.deltaTime;Console.WriteLine("[WaitForSeconds] now left:" &#43; waitTime);return waitTime <&#61; 0;}}public class WaitForFrame : IWait{public int waitFrame &#61; 0;public WaitForFrame(int frame){waitFrame &#61; frame;}bool IWait.Tick(){waitFrame--;Console.WriteLine("[WaitForFrame] now left:" &#43; waitFrame);return waitFrame <&#61; 0;}}
}
为了使得WaitForSeconds等可以参与我们协程的流程控制&#xff0c;还需要对Coroutine进行改造&#xff0c;主要改造的是MoveNext函数&#xff0c;改造完后的Coroutine完整代码如下&#xff1a;
using System;
using System.Collections;namespace Coroutine
{public class Coroutine{private IEnumerator _routine;public Coroutine(IEnumerator routine){_routine &#61; routine;}public bool MoveNext(){if (_routine &#61;&#61; null)return false;//看迭代器当前的流程控制&#xff08;即yield return 后边的对象&#xff09;//是否是我们当前IWait对象&#xff0c;如果是&#xff0c;看是否满足moveNext的条件IWait wait &#61; _routine.Current as IWait;bool moveNext &#61; true;if (wait !&#61; null)moveNext &#61; wait.Tick();if (!moveNext){//此处有些不好理解。当时间没有到时&#xff0c;我们应该返回true//告诉管理器我们后边还有对象需要下一次继续迭代Console.WriteLine("[Coroutine] not movenext");return true;}else{//此时所有等待时间或者帧都已经迭代完毕&#xff0c;看IEnumerator对象后续是否还有yield return对象//将此结果通知给管理器&#xff0c;管理器会在下一次迭代时决定是否继续迭代该Coroutine对象Console.WriteLine("[Coroutine] movenext");return _routine.MoveNext();}}public void Stop(){_routine &#61; null;}}
}
一切就绪&#xff0c;code我们控制台代码&#xff0c;在main韩素中模拟unity的update函数&#xff0c;不断update我们的协程管理器即可。测试代码如下&#xff1a;
using System;
using System.Collections;
using System.Threading;namespace Coroutine
{class MainClass{public static void Main(string[] args){Console.WriteLine("[Main] coroutine test begin");CoroutineManager.Instance.Start(CoroutineTest());int framecount &#61; 0;//模拟当前帧数,当前time下&#xff0c;1秒50帧while (true){framecount&#43;&#43;;Console.WriteLine("[Main] cur framecount:<" &#43; framecount &#43; "> CoroutineManager.Instance.UpdateCoroutine");CoroutineManager.Instance.UpdateCoroutine();//模拟unity的updateThread.Sleep(Time.deltaMilTime);if (framecount >&#61; 35)break;}}private static IEnumerator CoroutineTest(){Console.WriteLine("[CoroutineTest] enter coroutine. begin return waitforseconds:0.5f");yield return new WaitForSeconds(0.5f);Console.WriteLine("[CoroutineTest] wait for seconds <0.5f> over. begin wait for frame:3");yield return new WaitForFrame(3);Console.WriteLine("[CoroutineTest] wait for frame <3> over. now exit corutine");}}
}
由我们的协程内容可知&#xff1a;第一个yield return延迟0.5秒&#xff0c;即25帧&#xff0c;应该在第26帧执行完毕。第2个yield return延迟3帧&#xff0c;应该在第29帧执行完毕。因此35帧的时候while循环退出。我们看下控制台打印结果&#xff0c;如下&#xff1a;
Last login: Wed Feb 12 17:29:04 on ttys000[Main] coroutine test begin
[Main] cur framecount:<1> CoroutineManager.Instance.UpdateCoroutine
[Coroutine] movenext
[CoroutineTest] enter coroutine. begin return waitforseconds:0.5f
[Main] cur framecount:<2> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.48
[Coroutine] not movenext
[Main] cur framecount:<3> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.46
[Coroutine] not movenext
[Main] cur framecount:<4> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.44
[Coroutine] not movenext
[Main] cur framecount:<5> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.42
[Coroutine] not movenext
[Main] cur framecount:<6> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.3999999
[Coroutine] not movenext
[Main] cur framecount:<7> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.3799999
[Coroutine] not movenext
[Main] cur framecount:<8> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.3599999
[Coroutine] not movenext
[Main] cur framecount:<9> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.3399999
[Coroutine] not movenext
[Main] cur framecount:<10> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.3199999
[Coroutine] not movenext
[Main] cur framecount:<11> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.2999999
[Coroutine] not movenext
[Main] cur framecount:<12> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.2799999
[Coroutine] not movenext
[Main] cur framecount:<13> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.2599999
[Coroutine] not movenext
[Main] cur framecount:<14> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.2399999
[Coroutine] not movenext
[Main] cur framecount:<15> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.2199999
[Coroutine] not movenext
[Main] cur framecount:<16> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.1999999
[Coroutine] not movenext
[Main] cur framecount:<17> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.1799999
[Coroutine] not movenext
[Main] cur framecount:<18> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.1599999
[Coroutine] not movenext
[Main] cur framecount:<19> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.1399999
[Coroutine] not movenext
[Main] cur framecount:<20> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.1199999
[Coroutine] not movenext
[Main] cur framecount:<21> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.0999999
[Coroutine] not movenext
[Main] cur framecount:<22> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.07999991
[Coroutine] not movenext
[Main] cur framecount:<23> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.05999991
[Coroutine] not movenext
[Main] cur framecount:<24> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.03999991
[Coroutine] not movenext
[Main] cur framecount:<25> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:0.01999991
[Coroutine] not movenext
[Main] cur framecount:<26> CoroutineManager.Instance.UpdateCoroutine
[WaitForSeconds] now left:-8.940697E-08
[Coroutine] movenext
[CoroutineTest] wait for seconds <0.5f> over. begin wait for frame:3
[Main] cur framecount:<27> CoroutineManager.Instance.UpdateCoroutine
[WaitForFrame] now left:2
[Coroutine] not movenext
[Main] cur framecount:<28> CoroutineManager.Instance.UpdateCoroutine
[WaitForFrame] now left:1
[Coroutine] not movenext
[Main] cur framecount:<29> CoroutineManager.Instance.UpdateCoroutine
[WaitForFrame] now left:0
[Coroutine] movenext
[CoroutineTest] wait for frame <3> over. now exit corutine
[CoroutineManager] remove cor
[Main] cur framecount:<30> CoroutineManager.Instance.UpdateCoroutine
[Main] cur framecount:<31> CoroutineManager.Instance.UpdateCoroutine
[Main] cur framecount:<32> CoroutineManager.Instance.UpdateCoroutine
[Main] cur framecount:<33> CoroutineManager.Instance.UpdateCoroutine
[Main] cur framecount:<34> CoroutineManager.Instance.UpdateCoroutine
[Main] cur framecount:<35> CoroutineManager.Instance.UpdateCoroutinePress any key to continue...
控制台打印的结果可以佐证我们的协程执行符合预期&#xff0c;WaitForSeconds和WaitForFrame可以正常的控制我们的协程的流程&#xff01;nice&#xff01;我们的第一课内容虽然多&#xff0c;但是出师大捷&#xff0c;为我们的协程的扩展打下了坚实的基础&#xff01;