前言

大家好,之前一直知道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#给提供的一个迭代器接口,这个接口有泛型和非泛型的版本。下面我们通过foreachList的源码来学习一下这个接口。

List<int> testList = new List<int> { 0, 1, 2, 3 };
foreach (var i in testList) {
    Debug.Log(i);
}

上面是一个列表的foreach遍历过程。在C#中,要使用foreach循环遍历一个集合,该集合的类或结构体需要实现一个GetEnumerator()方法,这个方法声明在IEnumerableIEnumerable<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()来修改并存储currentindex的值,直到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这进行迭代。

03f1f8a16f33481d820aca4461cd3a46.png

347c4ca14ee14d8a9abc4791de0cc752.png

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进行判断,如果有知道的大佬也可以进行补充。

 

Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐