浅谈Unity协程和迭代器
大家好,之前一直知道Untiy的协程是通过迭代器来实现的,但是迭代器具体干了什么一直没有了解过,所以今天决定写一篇文章来梳理一下这一部分的内容。
前言
大家好,之前一直知道Untiy的协程是通过迭代器来实现的,但是迭代器具体干了什么一直没有了解过,所以今天决定写一篇文章来梳理一下这一部分的内容。
个人水平有限,如有错误欢迎大家指正和补充。
Unity协程的基本使用
要使用Unity的协程需要定义一个返回值为IEnumerator
接口的方法,通过StartCoroutine
来启动,协程的执行可以通过yield return
语句暂停,直到下一帧或指定的时间后继续执行。
下面是一个简单的协程,这个协程在开启后每一帧执行一次,直到物体到达指定的位置。接下来我会从迭代器接口和yield
语法来解释协程。
private void Start()
{
StartCoroutine(MoveCubeToTarget());
}
private IEnumerator MoveCubeToTarget()
{
while (transform.position != Vector3.one)
{
transform.position = Vector3.MoveTowards(transform.position, Vector3.one, Time.deltaTime);
yield return null;
}
}
迭代器接口
IEnumerator
是C#给提供的一个迭代器接口,这个接口有泛型和非泛型的版本。下面我们通过foreach
和List
的源码来学习一下这个接口。
List<int> testList = new List<int> { 0, 1, 2, 3 };
foreach (var i in testList) {
Debug.Log(i);
}
上面是一个列表的foreach
遍历过程。在C#中,要使用foreach
循环遍历一个集合,该集合的类或结构体需要实现一个GetEnumerator()
方法,这个方法声明在IEnumerable
和IEnumerable<T>
接口中,其实不实现这个接口,直接实现方法也可以。这个接口也比较简单,只有一个方法,返回一个IEnumerator<T>
或IEnumerator
的对象。当然我们的重点不是IEnumerable
这个接口,而是其返回的IEnumerator
,这里就不对IEnumerable
研究了。
IEnumerator<T> GetEnumerator();
这个IEnumerator
又是个什么东西呢。C#官方文档对其的描述是:支持对泛型集合进行简单迭代。这个接口定义了一个属性和两个方法,Current
属性表示当前的元素,MoveNext()
方法表示移动到下一个元素,Reset()
方法表示重头迭代。
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
public interface IEnumerator<out T> : IEnumerator, IDisposable
{
T Current { get; }
}
下面我们看看在List
中对这两个接口的实现,以及在foreach
中是如何使用的。这里我只截取其中的关键代码,如果想要看全部源码,可以自行去查看,源码地址:https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,cf7f4095e4de7646
这里可以看到List
中通过GetEnumerator
()
返回了一个Enumerator
结构体,这个结构体中通过MoveNext()
来修改并存储current
和index
的值,直到List
的末尾。
public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
{
...
public Enumerator GetEnumerator() {
return new Enumerator(this);
}
...
}
[Serializable]
public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator {
private List<T> list;
private int index;
private int version;
private T current;
...
public bool MoveNext() {
List<T> localList = list;
if (version == localList._version && ((uint)index < (uint)localList._size)) {
current = localList._items[index];
index++;
return true;
}
return MoveNextRare();
}
private bool MoveNextRare() {
if (version != list._version) {
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
index = list._size + 1;
current = default(T);
return false;
}
public T Current {
get { return current; }
}
...
}
了解了List
的迭代器中干了什么之后,那么在foreach
中是如何使用这个迭代器的呢?我们可以通过Rider的IL View来看foreach
生成了什么东西。这里可以看到foreach
所生成的实际是一个while
循环,并且其就是使用了MoveNext()
和Current
这进行迭代。
Unity协程中如何使用迭代器
知道了IEnumerator
这个接口之后,接下来让我们回到开头的那个协程中。这时可能有小伙伴有一些疑问,同样都是返回IEnumerator
,怎么List
中是定义了一个结构体,我们写的协程中写了yield return
这个奇怪的东西。这个其实不难解释,一句话来说,yield return
是C#提供给我们的语法糖。我们接着用IL View来看一下我们写的协程会生成什么样的代码。
下面是构建生成的代码,这里我们就不去关心具体的逻辑,我们单从结构上看,我们写的MoveCubeToTarget()
在生成后实际返回了一个<MoveCubeToTarget>d__1
的类,这个类是根据我们写的逻辑所自动生成的实现了IEnumerator
的一个类,这和List
中的实现方式是相似的,所以这里我们可以知道yield return
只是可以让我们更方便使用的语法糖而已。
private void Start()
{
this.StartCoroutine(this.MoveCubeToTarget());
}
[IteratorStateMachine(typeof(Test.<MoveCubeToTarget>d__1))]
private IEnumerator MoveCubeToTarget()
{
Test.<MoveCubeToTarget>d__1 target = new Test.<MoveCubeToTarget>d__1(0);
target.<>4__this = this;
return (IEnumerator)target;
}
[CompilerGenerated]
private sealed class <MoveCubeToTarget>d__1 : IEnumerator<object> , IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public Test<>4__this;
...
bool IEnumerator.MoveNext()
{
int num = this.<>1__state;
if (num != 0)
{
if (num != 1)
return false;
this.<>1__state = -1;
}
else
this.<>1__state = -1;
if (!Vector3.op_Inequality(this. <>4__this.transform.position, Vector3.one))
return false;
this.<>4__this.transform.position = Vector3.MoveTowards(this. <>4__this.transform.position, Vector3.one, Time.deltaTime);
this.<>2__current = (object)null;
this.<>1__state = 1;
return true;
}
object IEnumerator<object>.Current
{
[DebuggerHidden]
get {
return this.<>2__current;
}
}
...
}
到这里我们提供给Unity的迭代器接口就到此结束了,Unity内部如何去使用这个接口因为其进行了封装,我们就没办法看到了,我猜测大概率是使用Time.deltaTime
进行判断,如果有知道的大佬也可以进行补充。
更多推荐
所有评论(0)