最近一直被迭代器,还有协程内部的实现原理所困扰,那么今天我们就自己来手写一下,自己实现一波Unity的协程。

那么首先先来看下Unity原生的协程是怎么写的

1
2
3
4
5
6
IEnumerator Test()
{
Debug.Log("123");
yield return new WaitForSeconds(5);
Debug.Log("456");
}

这样一个简单的协程就写好了,可以看到他的本质其实就是一个迭代器,返回值是IEnumerator,那么调用它也是很简单

1
StartCoroutine(Test());

参数可以是方法,也可以是字符串,Unity内部都会去帮我们调用。

到这里Unity原生的协程就已经实现了,那么现在我们一步一步来,我们可以先从迭代器开始一步一步往下拆,先来自己实现个迭代器,首先我们先写一个类,,继承IEnumerator,并实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TinyIEnumerator : IEnumerator
{
public object Current => throw new System.NotImplementedException();

public bool MoveNext()
{
throw new System.NotImplementedException();
}

public void Reset()
{
throw new System.NotImplementedException();
}
}

很简单有Current,MoveNext,Rest这几个方法,如果大家了解过迭代器就知道,其中最重要的就是 MoveNext 这个方法他的内部维护了一个状态机,那我们就先来实现它

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
public class TinyIEnumerator :IEnumerator
{
private int index;
private float time;
private int curState;

public TinyIEnumerator(int index, float time)
{
this.index = index;
this.time = time;
this.curState = 0;
}

public object Current => throw new System.NotImplementedException();

public bool MoveNext()
{
switch (curState)
{
case 0:
//如果是按照刚刚我们写的简易携程写这里应该是“123”
Debug.Log("Star" + index);
curState++;
return true;
case 1:
//而这里应该是“456”
Debug.Log("End" + index);
curState++;
return true;
default:
return false;
}
}
}

每当我们调用一次MoveNext,我们当前维护的状态+1,然后返回true,直到没有了就会返回false,我们可以理解为yield return 这个东西,会把我们刚刚写的代码拓展出来变成现在这个,而拓展出来有多少个case取决于我们写了几个yield return。

那么到现在我么已经写了一个简易的迭代器了,我们再从Unity开始我们需要理解 StartCoroutine() 这个方法到底做了什么。

这是我们F12跟踪过去可以看到的代码,可以看到他的参数类型为IEnumerator返回了一个类型为Coroutine 的东西,我们暂且可以不用去理解Coroutine 是个啥我们可以思考下,他是怎么用迭代器实现协程的。

首先我们刚刚已经实现了一个属于我们自己的迭代器里有一个很重要的方法叫MoveNext,他的返回类型为bool,我们现在就利用他来做点文章,首先我们可以先做一个容器把所有的迭代器都收集到一起方便管理。

1
2
3
4
5
public class TinyCoroutine
{
public static List coroutines = new List();
public static List removeCoroutines = new List();
}

至于为什么要两个容器我们后面再说,然后我们下面来实现下添加,并且我们需要刷新他们数据去调用他们的 MoveNext 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void CoroutineStart(TinyIEnumerator enumerator)
{
coroutines.Add(enumerator);
}
public static void UpdateCoroutines()
{
removeCoroutines.Clear();
if (coroutines.Count > 0)
{
foreach (TinyIEnumerator item in coroutines)
{
item.MoveNext();
}
}

到现在为止我们就可以调用我们自己创建的迭代器,并使用他的MoveNext方法了,但我们还是缺少了一点东西,我们怎么知道应该什么时候去调用呢。

现在这个情况我们是每一帧都会去调用 MoveNext ,但我们不可能每一帧调用一次啊,那这就乱套了啊,我们设定是5秒,他5帧就完成了,我们也不可能去算帧啊,那这时候就需要再写一个类来维护他,在该调用的时候调用 MoveNext 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Delay
{
private float time;
private float delayTime;

public Delay(float delayTime)
{
time = 0;
this.delayTime = delayTime;
}

public bool CallFun()
{
if (time < delayTime)
{
time += Time.deltaTime;
return true;
}
else
{
return false;
}
}
}

这个类我们做一个计时器时间到了返回true,还没到就返回false,这样返回Ture的时候我们就去调用一次MoveNext就完成啦。

最后贴上完整版的代码。

迭代器+计时器+协程:

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
public class TinyIEnumerator :IEnumerator
{
private Delay curDelay;
private int index;
private float time;
private int curState;

public TinyIEnumerator(int index, float time)
{
this.index = index;
this.time = time;
this.curState = 0;
}

public object Current => curDelay;

public bool MoveNext()
{
switch (curState)
{
case 0:
Debug.Log("Star" + index);
curDelay = new Delay(time);
curState++;
return true;
case 1:
Debug.Log("End" + index);
curState++;
return true;
default:
return false;
}
}

public void Reset()
{
curState = 0;
}
}
public class TinyCoroutine
{
public static List coroutines = new List();
public static List removeCoroutines = new List();
public static void CoroutineStart(TinyIEnumerator enumerator)
{
coroutines.Add(enumerator);
}
public static void UpdateCoroutines()
{
removeCoroutines.Clear();
if (coroutines.Count > 0)
{
foreach (TinyIEnumerator item in coroutines)
{
if (item.Current == null)
{
item.MoveNext();
}
if (!(item.Current as Delay).CallFun())
{
if (!item.MoveNext())
{
removeCoroutines.Add(item);
}
}
}
foreach (TinyIEnumerator item in removeCoroutines)
{
coroutines.Remove(item);
}
}
}
}

public class Delay
{
private float time;
private float delayTime;

public Delay(float delayTime)
{
time = 0;
this.delayTime = delayTime;
}

public bool CallFun()
{
if (time < delayTime)
{
time += Time.deltaTime;
return true;
}
else
{
return false;
}
}

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NewBehaviourScript : MonoBehaviour
{
TinyIEnumerator tinyIEnumerator_1 = new TinyIEnumerator(1, 1);
TinyIEnumerator tinyIEnumerator_2 = new TinyIEnumerator(2, 5);
TinyIEnumerator tinyIEnumerator_3 = new TinyIEnumerator(3, 3);
void Start()
{
TinyCoroutine.CoroutineStart(tinyIEnumerator_1);
TinyCoroutine.CoroutineStart(tinyIEnumerator_2);
TinyCoroutine.CoroutineStart(tinyIEnumerator_3);
}
private void Update()
{
TinyCoroutine.UpdateCoroutines();
}
}

总结

C#: yield return只是一个语法糖,实际编译后编译器会把他们拓展开变成我们写的这个样子,yield return帮我们省下了很多的代码。

Unity:因为看不到源码所以只能做出猜测,但Unity底层应该也差不多(肯定会比我的更加精简,性能更高),Unity的协程开启后每一帧都会有一个总的管理器被调用,去判断容器内的迭代器们是否满足条件,只有在条件满足的情况下才会去MoveNext,当然这个判断条件只能用Unity给我们内置好的,其他的比如说我们自己写的他应该都做了处理直接默认为True。

至于为什么要两个容器,那是因为foreach的时候容器是不允许被更改的,不然就报错了, 移除的代码大家可以实现下,因为有点复杂,这里就暂时不实现了,

第一次写文章如果有不对、不好、看不懂,或者错误的地方也欢迎大佬指正~