Unity-协程
基本使用方法
1 2 3 4 5 6 7 8 9
| IEnumerator CoroutineFunc() { code...; yield return ...; code...; }
StartCoroutine(CoroutineFunc); StopCoroutine(CoroutineFunc);
|
函数会在yield return
处停止,并把控制权转交给Unity
,但会在下一帧中继续执行,如果想要提前结束的话则调用yield break
原理
Unity的协程是基于C#的迭代器实现的,但主要是C#2之后的迭代器
在C#1中,想要使用foreach
关键字遍历某个对象,需要先让该对象实现IEnumerable
的接口,同时还要实现一个迭代器类
在C#2中,引入了迭代块语句yield return
来简化上述过程,程序员只需要实现1个返回参数类型为IEnumerator
的函数即可获得C#1中用两个类实现的相同的效果,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static IEnumerator GetEnumerator() { int arr[] = {1, 2, 3}; for(int i = 0; i < 3; i++) { yield return arr[i]; } }
foreach(int a in GetEnumerator()) { }
|
通过反编译可执行文件可以发现,我们编写的函数在编译器处理过后已经完全不再是一个函数了,特别是其中的yield return
语句,和一般函数中的return
语句完全不同,另外,函数中所有的局部变量都变成了生成迭代器类中的全局变量,可以通过反编译代码验证这一点。
假设C#源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| using System; using System.Collections.Generic;
namespace net { class HelloWorld { static void Main(string[] args) { foreach(int i in GetEnumerator()) { } }
static IEnumerable<Int32> GetEnumerator() { for(int i = 0; i < 10; i++) { yield return i; } } } }
|
经过反编译后得到如下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
| using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices;
namespace net { internal class HelloWorld { private static void Main(string[] args) { foreach (int i in HelloWorld.GetEnumerator()) { } }
private static IEnumerable<int> GetEnumerator() { return new HelloWorld.<GetEnumerator>d__1(-2); }
[CompilerGenerated] private sealed class <GetEnumerator>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable { [DebuggerHidden] public <GetEnumerator>d__1(int <>1__state) { this.<>1__state = <>1__state; this.<>l__initialThreadId = Environment.CurrentManagedThreadId; }
[DebuggerHidden] void IDisposable.Dispose() { }
bool IEnumerator.MoveNext() { int num = this.<>1__state; if (num != 0) { if (num != 1) { return false; } this.<>1__state = -1; int num2 = this.<i>5__1; this.<i>5__1 = num2 + 1; } else { this.<>1__state = -1; this.<i>5__1 = 0; } if (this.<i>5__1 >= 10) { return false; } this.<>2__current = this.<i>5__1; this.<>1__state = 1; return true; }
int IEnumerator<int>.Current { [DebuggerHidden] get { return this.<>2__current; } }
[DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); }
object IEnumerator.Current { [DebuggerHidden] get { return this.<>2__current; } }
[DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator() { HelloWorld.<GetEnumerator>d__1 result; if (this.<>1__state == -2 && this.<>l__initialThreadId == Environment.CurrentManagedThreadId) { this.<>1__state = 0; result = this; } else { result = new HelloWorld.<GetEnumerator>d__1(0); } return result; }
[DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator(); }
private int <>1__state;
private int <>2__current;
private int <>l__initialThreadId;
private int <i>5__1; } } }
|
可以发现,该函数被完全处理成了一个新的内部类,名称为<GetEnumerator>d__1
, 其中,局部变量i
变成了<i>5__1
, 又增加了3个新的变量,其中<>1__state
记录对象的当前状态,<>2__current
记录下一个要返回的值,<>1__initialThreadId
记录当前线程Id
。
同时,这个生成类变成了一个类似于状态机的东西,它将原来的局部变量作为状态机的上下文环境,将原本的代码逻辑按照复杂的规则转换成状态机中状态之间的切换,实现在MoveNext
函数中,这样,在主函数完成对MoveNext
的一次调用后,并不会丢失函数中声明的局部变量,同时也不会导致线程阻塞,有些类似于操作系统中的进程调度,这正是设计协程的良好框架!
但是,由于该方法需要将局部变量存储在堆中,如果频繁开启协程,则会申请大量空间,造成时间和空间上的巨大浪费,因此要谨慎使用协程,尽量不要在Update
函数中每帧开启协程,并且大部分情况下这种代码逻辑是有问题的。
C++实现
虽然不知道微软使用了什么样的规则将包含yield
的函数翻译成迭代器类,但是我们可以直接写一个迭代器类来实现相同的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| #include<iostream> #include<memory> #include<vector> #include<conio.h>
using namespace std;
template <typename T> using Ref = shared_ptr<T>;
static int id = 0;
int getId() { return id++; }
class Enumerator { public: Enumerator():state(-1), val(10), ID(getId()), timer(0), current(0) {
} bool MoveNext();
int state;
int ID; int val;
int timer;
int current; };
class Coroutine { public: void Update(); void Add(Ref<Enumerator> Etr);
vector< Ref<Enumerator> > Etrs; };
int main() { char ch; Coroutine Crt; while(1) { Crt.Update(); if(_kbhit()) { if((ch = _getch()) == 'w') { Crt.Add(make_shared<Enumerator>()); } } }
return 0; }
bool Enumerator::MoveNext() { switch(state) { case -1: cout << ID << " start!\n"; state = 0; case 0: while(val >= 0) { cout << ID << ": " << val << endl; val -= 1;
state = 2; timer = 0; return true; } state = 1; case 1: cout << ID << " end!\n"; return false; case 2: for(; timer < 10000;) { timer++; return true; } state = 0; return true; } return false; }
void Coroutine::Add(Ref<Enumerator> Etr) { Etrs.push_back(Etr); }
void Coroutine::Update() { for(int i = 0; i < Etrs.size(); i++) { Enumerator* Etr = Etrs[i].get(); if(!Etr->MoveNext()) { Etrs.erase(Etrs.begin() + i); i--; } } }
|
在以上程序中,Enumerator
为迭代器类,其MoveNext
输出一个整数,并随着迭代次数不断减少。 Coroutine
为管理协程的类,当按下w
键时,程序向其中添加一个迭代器对象,Coroutine
对象会在主线程中每帧更新所有迭代器对象,直到该对象迭代完成,将其弹出