当前位置:   article > 正文

Unity 协程探究_yield return null

yield return null

一、官方手册中的描述

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条 如下:

  1. using System.Collections;
  2. using UnityEngine;
  3. public class Test : MonoBehaviour
  4. {
  5. void Start()
  6. {
  7. StartCoroutine(Func1());
  8. }
  9. IEnumerator Func1()
  10. {
  11. Debug.Log("Time.frameCount: " + Time.frameCount);
  12. yield return null;
  13. Debug.Log("Time.frameCount: " + Time.frameCount);
  14. yield return 0;
  15. Debug.Log("Time.frameCount: " + Time.frameCount);
  16. yield return 1;
  17. Debug.Log("Time.frameCount: " + Time.frameCount);
  18. yield return 99; //其他整数
  19. Debug.Log("Time.frameCount: " + Time.frameCount);
  20. yield return 0.5f; //浮点数值
  21. Debug.Log("Time.frameCount: " + Time.frameCount);
  22. yield return false; //bool值
  23. Debug.Log("Time.frameCount: " + Time.frameCount);
  24. yield return "Hi NRatel!"; //字符串
  25. Debug.Log("Time.frameCount: " + Time.frameCount);
  26. yield return new Object(); //任意对象
  27. Debug.Log("Time.frameCount: " + Time.frameCount);
  28. }
  29. }

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

  1. using System.Collections;
  2. using UnityEngine;
  3. public class Test : MonoBehaviour
  4. {
  5. void Start()
  6. {
  7. StartCoroutine(Func1());
  8. }
  9. IEnumerator Func1()
  10. {
  11. Debug.Log("Func1");
  12. yield return Func2();
  13. }
  14. IEnumerator Func2()
  15. {
  16. Debug.Log("Func2");
  17. yield return Func3();
  18. }
  19. IEnumerator Func3()
  20. {
  21. Debug.Log("Func3");
  22. yield return null;
  23. }
  24. }

三、Unity协程实现原理

1、C# 的迭代器。

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

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

  1. namespace System.Collections
  2. {
  3. //可枚举(可迭代)对象接口
  4. public interface IEnumerable
  5. {
  6. IEnumerator GetEnumerator();
  7. }
  8. //迭代游标接口
  9. public interface IEnumerator
  10. {
  11. object Current { get; }
  12. bool MoveNext();
  13. void Reset();
  14. }
  15. }

参考 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 使用!

  1. using System.Collections;
  2. using UnityEngine;
  3. public class Test : MonoBehaviour
  4. {
  5. void Start()
  6. {
  7. IEnumerator e = Func();
  8. while (e.MoveNext())
  9. {
  10. Debug.Log(e.Current);
  11. }
  12. }
  13. IEnumerator Func()
  14. {
  15. yield return 1;
  16. yield return "Hi NRatel!";
  17. yield return 3;
  18. }
  19. }

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

3、Unity 的协程。

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

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

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. public class Test : MonoBehaviour
  6. {
  7. private Dictionary<IEnumerator, IEnumerator> recoverDict; //key:当前迭代器 value:子迭代器完成后需要恢复的父迭代器
  8. private IEnumerator enumerator;
  9. private void Start()
  10. {
  11. //Unity自身的协程
  12. //StartCoroutine(Func1());
  13. //自己实现的协程
  14. StarMyCoroutine(Func1());
  15. }
  16. private void StarMyCoroutine(IEnumerator e)
  17. {
  18. recoverDict = new Dictionary<IEnumerator, IEnumerator>();
  19. enumerator = e;
  20. recoverDict.Add(enumerator, null); //完成后不需要恢复任何迭代器
  21. }
  22. private void LateUpdate()
  23. {
  24. if (enumerator != null)
  25. {
  26. DoEnumerate(enumerator);
  27. }
  28. }
  29. private void DoEnumerate(IEnumerator e)
  30. {
  31. object current;
  32. if (e.MoveNext())
  33. {
  34. current = e.Current;
  35. }
  36. else
  37. {
  38. //迭代结束
  39. IEnumerator recoverE = recoverDict[e];
  40. if (recoverE != null)
  41. {
  42. recoverDict.Remove(e);
  43. }
  44. //恢复至父迭代器, 若没有则会至为null
  45. enumerator = recoverE;
  46. return;
  47. }
  48. //null,什么也不做,下一帧继续
  49. if (current == null) { return; }
  50. Type type = current.GetType();
  51. //基础类型,什么也不做,下一帧继续
  52. if (current is System.Int32) { return; }
  53. if (current is System.Boolean) { return; }
  54. if (current is System.String) { return; }
  55. //IEnumerator 类型, 等待内部嵌套的IEnumerator迭代完成再继续
  56. if (current is IEnumerator)
  57. {
  58. //切换至子迭代器
  59. enumerator = current as IEnumerator;
  60. recoverDict.Add(enumerator, e);
  61. return;
  62. }
  63. //YieldInstruction 类型, 猜测也是类似IEnumerator的实现
  64. if (current is YieldInstruction)
  65. {
  66. //省略实现
  67. return;
  68. }
  69. }
  70. IEnumerator Func1()
  71. {
  72. Debug.Log("Time.frameCount: " + Time.frameCount);
  73. yield return null;
  74. Debug.Log("Time.frameCount: " + Time.frameCount);
  75. yield return "Hi NRatel!";
  76. Debug.Log("Time.frameCount: " + Time.frameCount);
  77. yield return 3;
  78. Debug.Log("Time.frameCount: " + Time.frameCount);
  79. yield return new WaitUntil(() =>
  80. {
  81. return Time.frameCount == 20;
  82. });
  83. Debug.Log("Time.frameCount: " + Time.frameCount);
  84. yield return Func2();
  85. Debug.Log("Time.frameCount: " + Time.frameCount);
  86. }
  87. IEnumerator Func2()
  88. {
  89. Debug.Log("XXXXXXXXX");
  90. yield return null;
  91. Debug.Log("YYYYYYYYY");
  92. yield return Func3(); //嵌套 IEnumerator
  93. }
  94. IEnumerator Func3()
  95. {
  96. Debug.Log("AAAAAAAA");
  97. yield return null;
  98. Debug.Log("BBBBBBBB");
  99. yield return null;
  100. }
  101. }

对比结果,基本可以达成协程作用,包括 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 也是类似的实现。

  1. // Unity C# reference source
  2. // Copyright (c) Unity Technologies. For terms of use, see
  3. // https://unity3d.com/legal/licenses/Unity_Reference_Only_License
  4. using System.Collections;
  5. namespace UnityEngine
  6. {
  7. public abstract class CustomYieldInstruction : IEnumerator
  8. {
  9. public abstract bool keepWaiting
  10. {
  11. get;
  12. }
  13. public object Current
  14. {
  15. get
  16. {
  17. return null;
  18. }
  19. }
  20. public bool MoveNext() { return keepWaiting; }
  21. public void Reset() {}
  22. }
  23. }

2、WaitUntil 类

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

  1. // Unity C# reference source
  2. // Copyright (c) Unity Technologies. For terms of use, see
  3. // https://unity3d.com/legal/licenses/Unity_Reference_Only_License
  4. using System;
  5. namespace UnityEngine
  6. {
  7. public sealed class WaitUntil : CustomYieldInstruction
  8. {
  9. Func<bool> m_Predicate;
  10. public override bool keepWaiting { get { return !m_Predicate(); } }
  11. public WaitUntil(Func<bool> predicate) { m_Predicate = predicate; }
  12. }
  13. }

3、WaitWhile 类

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

  1. // Unity C# reference source
  2. // Copyright (c) Unity Technologies. For terms of use, see
  3. // https://unity3d.com/legal/licenses/Unity_Reference_Only_License
  4. using System;
  5. namespace UnityEngine
  6. {
  7. public sealed class WaitWhile : CustomYieldInstruction
  8. {
  9. Func<bool> m_Predicate;
  10. public override bool keepWaiting { get { return m_Predicate(); } }
  11. public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
  12. }
  13. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/95598
推荐阅读
相关标签
  

闽ICP备14008679号