Unity提供了协程来实现一定程度上的“伪并发”。
众所周知,UnityAPI只能在主线程中调用,如果我们希望通过多线程来实现多个过程的并发处理,那么如果涉及对UnityAPI的操作,则必须转到主线程去执行,比较繁琐。
MonoBehaviour.StartCoroutine可以创建一个协程(Coroutine),
在协程中可以通过yield
语句暂停程序执行或者退出协程,从游戏运行的宏观角度看,它们似乎是在并行一样。
注意
协程中运行的代码仍然是在主线程中运行,所以我们仍然可以使用UnityAPI。
IEnumerator接口
要了解协程的执行,首先要了解StartCoroutine
方法需要的参数IEnumerator
。它是.Net层面提供的接口类型(https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.ienumerator?view=net-6.0)而非Unity层面,它提供了一种简单的枚举非泛型集合的方法(也许这也是为什么没有泛型协程)。
要实现IEnumerator接口,需要实现两个方法MoveNext
和Reset
,以及一个属性Current
的get
访问器。其中MoveNext
和Reset
用于移动和重置指向的元素,Current
用于获取当前元素。
按照约定,初始时,Current
应该指向不可用元素,需要调用一次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
|
class NatualNumberEnumrator : IEnumerator
{
private int _number = -1;
public object Current => _number;
public bool MoveNext()
{
++_number;
return _number < 100;//返回值表示Current是否合法,这里假设只数到99;
}
public void Reset()
{
_number = -1;
}
}
void Start()
{
NatualNumberEnumrator natualNumberEnumrator = new NatualNumberEnumrator();
while (natualNumberEnumrator.MoveNext())
Debug.Log(natualNumberEnumrator.Current);//打印0到99
}
|
可以看到[0, 99]这些数字并不是一开始就有,而是在我们不断执行MoveNext
过程中产生的,有点像生成器(Generator)的概念。
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
|
class FibonacciEnumrator : IEnumerator
{
private int _pre = 1;
private int _cur = 0;
public object Current => _cur;
public bool MoveNext()
{
int temp = _cur;
_cur += _pre;
_pre = temp;
return true;
}
public void Reset()
{
_pre = 1;
_cur = 0;
}
}
void Start()
{
FibonacciEnumrator fibonacciEnumrator = new FibonacciEnumrator();
int count = 0;
while (fibonacciEnumrator.MoveNext())
{
Debug.Log(fibonacciEnumrator.Current);
count++;
if (count == 10)
break;
}
}
|
yield语句
yield
是C#提供的关键字(https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield),
使用yield
后,方法只能返回一个IEnumerator
接口,且方法中的代码只能通过对IEnumerator
接口执行MoveNext
的方式来执行。
yield
语句由两种形式:
yield return <expression>;
用于更新Current
值。
yield break;
用于中途停止迭代。
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
|
IEnumerator Fibonacci()
{
Debug.Log("Fibonacci start");
int pre = 1;
int cur = 0;
while (true)
{
int temp = cur;
cur += pre;
pre = temp;
yield return cur;
}
}
void Start()
{
IEnumerator fibonacciEnumrator = Fibonacci();
Debug.Log("Create fibonacc");
for (int idx = 0; idx < 10; ++idx)
{
if (fibonacciEnumrator.MoveNext())
Debug.Log(fibonacciEnumrator.Current);
}
//Create fibonacc
//Fibonacci start
//1
//1
//2
//...
}
|
可以看到Fibonacci
方法在调用时并没有执行,而是在执行MoveNext
才被执行,直到遇到yield
语句。使用yield
语句,一个方法的执行过程
可以被“分段执行”。
注意
IEnumrator接口和yield语句是Unity协程的基石。
StartCoroutine
把Fibonacci的例子稍作修改,使用StartCoroutine
执行:
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
|
IEnumerator Fibonacci()
{
Debug.LogFormat("Fibonacci start, Frame {0}", Time.frameCount);
int pre = 1;
int cur = 0;
int cnt = 0;
while (true)
{
int temp = cur;
cur += pre;
pre = temp;
Debug.LogFormat("{0} Frame:{1}", cur, Time.frameCount);
yield return null;
++cnt;
if (cnt == 10)
yield break;
}
}
void Start()
{
IEnumerator fibonacciEnumrator = Fibonacci();
Debug.LogFormat("Create fibonacc, Frame:{0}", Time.frameCount);
StartCoroutine(fibonacciEnumrator);
//Create fibonacc, Frame:1
//Fibonacci start, Frame 1
//1 Frame:1
//1 Frame:2
//2 Frame:3
//...
}
|
注意观察Fibonacci第一项被输出的时候是第一帧,也就是说当StartCoroutine
被执行的时候,会在当前帧执行一次MoveNext
而不是隔一帧再处理。
另外我们把输出Fibonacci项的工作放在了Fibonacci
方法内,因为使用StartCoroutine
后Fibonacci
产生的IEnumrator
接口由Unity去处理,
yield
语句对于我们使用者来说已经没有什么意义了。
不过这对于Unity来说有意义吖!
Unity会通过协程yield return
返回的内容(本质上就是每次MoveNext
之后读取一下Current
)来决定暂停多久,分为以下几种情况:
YieldInstruction
对象,如WaitForSeconds
、WaitForEndOfFrame
等,由Unity决定暂停到何时。
CustomYieldInstruction
子类对象,通过重写keepWaiting
的get
访问器决定暂停到什么时候。
Coroutine
或者IEnumrator
,也就是协程嵌套,这种情况程序会执行新协程,直到执行完再继续原来的协程。
- 其他情况,暂停到下一帧。
遇到协程嵌套的情况,协程至少会等待一帧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
IEnumerator outer()
{
Debug.LogFormat("outer start, Frame:{0}", Time.frameCount);
for (int idx = 0; idx < 3; ++idx)
{
//yield return inner();
yield return StartCoroutine(inner());
}
}
IEnumerator inner()
{
Debug.LogFormat("inner Frame:{0}", Time.frameCount);
yield break;
}
void Start()
{
StartCoroutine(outer());
//outer start, Frame:1
//inner Frame:1
//inner Frame:2
//inner Frame:3
}
|
如果不希望这样,可以手动执行被嵌套的协程:
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
|
IEnumerator outer()
{
Debug.LogFormat("outer start, Frame:{0}", Time.frameCount);
for (int idx = 0; idx < 3; ++idx)
{
IEnumerator innerCoroutine = inner();
while (innerCoroutine.MoveNext())
yield return innerCoroutine.Current;
}
}
IEnumerator inner()
{
Debug.LogFormat("inner Frame:{0}", Time.frameCount);
yield break;
}
void Start()
{
StartCoroutine(outer());
//outer start, Frame:1
//inner Frame:1
//inner Frame:1
//inner Frame:1
}
|
BGM