Unity 协程探究

一、官方手册中的描述

1、Manual/Coroutines

函数在调用时, “从调用到返回” 都发生在一帧之内,想要处理 “随时间推移进行的事务”, 相比Update,使用协程来执行此类任务会更方便。

协程在创建时,通常是一个 “返回值类型 为 IEnumerator”、“函数体中包含 yield return 语句 ” 的函数。

yiled return 可以暂停协程的执行,并在恰当时候恢复。具体在何时恢复,由 yield 的返回值决定。

启动协程,必须使用 MonoBehaviour 的 StartCoroutine 方法。

停止协程,可以使用 MonoBehaviour 的 StopCoroutine 方法 或 StopAllCoroutine 方法。

注意:以下情况也可能使协程停止
   1)、销毁启动协程的组件(GameObject.Destory(component);) ==> 协程停止
   2)、禁用启动协程的组件(component.enabled = false;)==> 协程不停止
   3)、销毁启动协程的组件所在的物体(GameObject.Destory(gameobject);) ==> 协程停止
   4)、隐藏启动协程的组件所在的物体(gameobject.SetActive(false);) ==> 协程停止

2、MonoBehaviour.StartCoroutine

StartCoroutine 方法总是立刻返回一个 Coroutine 对象(同步返回)。

无法保证协同程序按其启动顺序结束,即使他们在同一帧中完成也是如此(异步无序完成)。

可以在一个协程中启动另一个协程(支持协程嵌套)。

二、Unity中的 yield 语句类型

1、yield break;    //打断协程运行

2、yield return null;    //挂起协程,并从下一帧继续

3、yield return + “任意数字”;    //挂起协程,并从下一帧继续

4、yield return + “bool值”;    //挂起协程,并从下一帧继续

5、yield return + “任意字符串”;    //挂起协程,并从下一帧继续

6、yield return + “普通Object”;    //挂起协程,并从下一帧继续

7、yield return + “任意实现了 IEnumerator 接口的对象”。重要!(可嵌套)

    Unity 中,常见的、直接或间接实现了 IEnumerator 接口的类有:
        ------------------------------------------------------------------------------------------------
        CustomYieldInstruction (abstarct)   ——|>   IEnumerator (interface)
       
------------------------------------------------------------------------------------------------
        WaitUnitil (sealed)   ——|>   CustomYieldInstruction
        WaitWhile (sealed)
  ——|>   CustomYieldInstruction
        WaitForSecondsRealtime (非sealed,但未发现子类) 
——|>   CustomYieldInstruction
        WWW (非sealed,但未发现子类) 
——|>   CustomYieldInstruction
       
------------------------------------------------------------------------------------------------
        随着Unity更新或在一些可选的Package中,可能有更多。。。
        ------------------------------------------------------------------------------------------------

8、yield return + “任意继承了 YieldInstruction 类 ([UsedByNativeCode],源码C#层中无具体实现) 的对象”。重要!(可嵌套)

    Unity 中,常见的、直接或间接继承了 YieldInstruction 类的类有:
        ------------------------------------------------------------------------------------------------
        WaitForSeconds (sealed)   ——|>   YieldInstruction
        Coroutine (sealed)  ——|>   YieldInstruction (Coroutine 是 StartCoroutine方法的返回值,意味着协程中可嵌套协程)
        WaitForEndOfFrame (sealed) ——|>   YieldInstruction
        WaitForFixedUpdate (sealed)  ——|>   YieldInstruction
        AsyncOperation ——|>   YieldInstruction
       
------------------------------------------------------------------------------------------------
         AssetBundleCreateRequest (非sealed,但未发现子类) ——|>   AsyncOperation
        
AssetBundleRecompressOperation (非sealed,但未发现子类) ——|>   AsyncOperation
        
AssetBundleRequest (非sealed,但未发现子类) ——|>   AsyncOperation
        
ResourceRequest (非sealed,但未发现子类) ——|>   AsyncOperation
        
UnityEngine.Networking.UnityWebRequestAsyncOperation (非sealed,但未发现子类) ——|>   AsyncOperation
        
UnityEngine.iOS.OnDemandResourcesRequest (sealed) ——|>   AsyncOperation
        ------------------------------------------------------------------------------------------------
        随着Unity更新或在一些可选的Package中,可能有更多。。。
        ------------------------------------------------------------------------------------------------

***测试验证 第2、3、4、5、6条 如下:

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Func1());
    }

    IEnumerator Func1()
    {
        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return null;

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return 0;

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return 1;

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return 99; //其他整数

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return 0.5f; //浮点数值

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return false; //bool值

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return "Hi NRatel!";  //字符串

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return new Object();  //任意对象

        Debug.Log("Time.frameCount: " + Time.frameCount);
    }
}

***测试验证 第7条 如下:

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Func1());
    }

    IEnumerator Func1()
    {
        Debug.Log("Func1");
        yield return Func2();
    }

    IEnumerator Func2()
    {
        Debug.Log("Func2");
        yield return Func3();
    }

    IEnumerator Func3()
    {
        Debug.Log("Func3");
        yield return null;
    }
}

三、Unity协程实现原理

1、C# 的迭代器。

现在已经知道:协程肯定与IEnumerator有关,因为启动协程时需要一个 IEnumerator 对象。
而 IEnumerator 是C#实现的 迭代器模式 中的 枚举器(用于迭代的游标)。

迭代器相关接口定义如下:

namespace System.Collections
{
    //可枚举(可迭代)对象接口
    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }
    
    //迭代游标接口
    public interface IEnumerator
    {
        object Current { get; }
        bool MoveNext();
        void Reset();
    }
}

参考 MSDN C#文档中对于 IEnumeratorIEnumerable迭代器 的描述。

利用 IEnumerator 对象,可以对与之关联的 IEnumerable 集合 进行迭代:

    1)、通过 IEnumerator 的 Current 方法,可以获取集合中位于枚举数当前位置的元素。

    2)、通过 IEnumerator 的 MoveNext 方法,可以将枚举数推进到集合的下一个元素。如果 MoveNext 越过集合的末尾, 则枚举器将定位在集合中最后一个元素之后, 同时 MoveNext 返回 false。 当枚举器位于此位置时, 对 MoveNext 的后续调用也将返回 false 。如果最后一次调用 MoveNext 时返回 false,则 Current 未定义(结果为null)。

    3)、通过 IEnumerator 的 Reset 方法,可以将“迭代游标” 设置为其初始位置,该位置位于集合中第一个元素之前。

2、C# 的 yield 关键字。

C#编译器在生成IL代码时,会将一个返回值类型为 IEnumerator 的方法(其中包含一系列的 yield return 语句),构建为一个实现了 IEnumerator 接口的对象。

注意,yield 是C#的关键字,而非Unity定义!IEnumerator 对象 也可以直接用于迭代,并非只能被Unity的 StartCoroutine 使用!

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator e = Func();
        while (e.MoveNext())
        {
            Debug.Log(e.Current);
        }
    }

    IEnumerator Func()
    {
        yield return 1;
        yield return "Hi NRatel!";
        yield return 3;
    }
}

对上边C#代码生成的Dll进行反编译,查看IL代码:

3、Unity 的协程。

  Unity 协程是在逐帧迭代的,这点可以从 Unity 脚本生命周期 中看出。

可以大胆猜测一下,实现出自己的协程(功能相似,能够说明逐帧迭代的原理,不是Unity源码):

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour
{
    private Dictionary<IEnumerator, IEnumerator> recoverDict;   //key:当前迭代器 value:子迭代器完成后需要恢复的父迭代器
    private IEnumerator enumerator;

    private void Start()
    {
        //Unity自身的协程
        //StartCoroutine(Func1());

        //自己实现的协程
        StarMyCoroutine(Func1());
    }

    private void StarMyCoroutine(IEnumerator e)
    {
        recoverDict = new Dictionary<IEnumerator, IEnumerator>();
        enumerator = e;
        recoverDict.Add(enumerator, null);  //完成后不需要恢复任何迭代器
    }

    private void LateUpdate()
    {
        if (enumerator != null)
        {
            DoEnumerate(enumerator);
        }
    }

    private void DoEnumerate(IEnumerator e)
    {
        object current;
        if (e.MoveNext())
        {
            current = e.Current;
        }
        else
        {
            //迭代结束
            IEnumerator recoverE = recoverDict[e];
            if (recoverE != null)
            {
                recoverDict.Remove(e);
            }
            //恢复至父迭代器, 若没有则会至为null
            enumerator = recoverE;
            return;
        }

        //null,什么也不做,下一帧继续
        if (current == null) { return; }

        Type type = current.GetType();

        //基础类型,什么也不做,下一帧继续
        if (current is System.Int32) { return; }
        if (current is System.Boolean) { return; }
        if (current is System.String) { return; }

        //IEnumerator 类型, 等待内部嵌套的IEnumerator迭代完成再继续
        if (current is IEnumerator)
        {
            //切换至子迭代器
            enumerator = current as IEnumerator;
            recoverDict.Add(enumerator, e);
            return;
        }
        

        //YieldInstruction 类型, 猜测也是类似IEnumerator的实现
        if (current is YieldInstruction)
        {
            //省略实现
            return;
        }
    }

    IEnumerator Func1()
    {
        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return null;

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return "Hi NRatel!";

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return 3;

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return new WaitUntil(() =>
        {
            return Time.frameCount == 20;
        });

        Debug.Log("Time.frameCount: " + Time.frameCount);
        yield return Func2();

        Debug.Log("Time.frameCount: " + Time.frameCount);
    }

    IEnumerator Func2()
    {
        Debug.Log("XXXXXXXXX");
        yield return null;
        Debug.Log("YYYYYYYYY");
        yield return Func3();   //嵌套 IEnumerator
    }

    IEnumerator Func3()
    {
        Debug.Log("AAAAAAAA");
        yield return null;
        Debug.Log("BBBBBBBB");
        yield return null;
    }
}

对比结果,基本可以达成协程作用,包括 IEnumerator 嵌套。
但是 Time.frameCount 的结果不同,想来实现细节必然是有差别的。

四、部分Unity源码分析

1、CustomYieldInstruction 类

可以继承该类,并实现自己的、需要异步等待的类

原理:
    当协程中 yield return “一个CustomYieldInstruction的子类”; 其实就相当于在原来的 迭代器A 中,插入了一个 新的迭代器B。
    当迭代程序进入 B ,如果 keepWaiting 为 true,MoveNext() 就总是返回 true。
    上面已经说过,迭代器在迭代时,MoveNext() 返回false 才标志着迭代完成!
    那么,B 就总是完不成,直到 keepWaiting 变为 false。
    这样 A 运行至 B处就 处于了 等待B完成的状态,相当于A挂起了。

 猜测 YieldInstruction 也是类似的实现。

// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License

using System.Collections;

namespace UnityEngine
{
    public abstract class CustomYieldInstruction : IEnumerator
    {
        public abstract bool keepWaiting
        {
            get;
        }

        public object Current
        {
            get
            {
                return null;
            }
        }
        public bool   MoveNext() { return keepWaiting; }   
        public void   Reset() {}
    }
}

2、WaitUntil 类

    语义为 “等待...直到满足...”
    继承自 CustomYieldInstruction,需要等待时让 m_Predicate 返回 false (keepWating为true)。

// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License

using System;

namespace UnityEngine
{
    public sealed class WaitUntil : CustomYieldInstruction
    {
        Func<bool> m_Predicate;

        public override bool keepWaiting { get { return !m_Predicate(); } }

        public WaitUntil(Func<bool> predicate) { m_Predicate = predicate; }
    }
}

3、WaitWhile 类

    语义为 “等待...如果满足...”
    继承自 CustomYieldInstruction,需要等待时让 m_Predicate 返回 true (keepWating为true)。
    与 WaitUntil 的实现恰好相反。

// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License

using System;

namespace UnityEngine
{
    public sealed class WaitWhile : CustomYieldInstruction
    {
        Func<bool> m_Predicate;

        public override bool keepWaiting { get { return m_Predicate(); } }

        public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
    }
}

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NRatel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值