本文主要分为三个部分:
1)Yield返回,IEnumerator和Unity StartCoroutine的关系和理解
2)协程扩展——扩展协程:返回值和错误处理
3)协程锁
简而言之,引自 ③:协程——比你想知道的还要多。
1)Yield返回,IEnumerator和Unity StartCoroutine的关系和理解
Yield 和 IEnumerator 都是 C# 的东西。 前者是关键字,后者是枚举类的接口。 对于IEnumerator,仅引用②对IEnumerable和IEnumerator区别的讨论:
先贴出IEnumerable和IEnumerator的定义:
public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { bool MoveNext(); void Reset(); Object Current { get; } }
IEnumerable 和 IEnumerator 之间有什么区别? 这是一个非常令人困惑的问题(我在很多论坛上看到有人问这个问题)。 经过长时间的学习,我有以下几点体会:
1. 对于一个支持foreach遍历的Collection,它必须实现IEnumerable接口(即它必须以某种方式返回一个IEnumerator对象)。
2. IEnumerator对象具体实现了迭代器(通过MoveNext()、Reset()、Current)。
3、从这两个接口的用词上我们也可以看出区别:IEnumerable是一个声明式接口。 它声明实现该接口的类是“可枚举的”,但没有解释如何实现。 实现一个枚举器(迭代器); IEnumerator 是一个实现接口,IEnumerator 对象是一个迭代器。
4. IEnumerable 和 IEnumerator 通过 IEnumerable 的 GetEnumerator() 方法连接。 客户端可以通过IEnumerable的GetEnumerator()获取IEnumerator对象。 从这个意义上来说,将 GetEnumerator() 视为 IEnumerator 对象的工厂方法并不是一个坏主意。
IEnumerator 是所有枚举器的基本接口。
枚举器仅允许从集合中读取数据。 枚举数不能用于修改底层集合。
最初,枚举数位于集合中第一个元素之前。 重置还将枚举器返回到此位置。 此时,调用Current会抛出异常。 因此,在读取 Current 的值之前unity 死循环协程,必须调用 MoveNext 将枚举数前进到集合的第一个元素。
Current 在调用 MoveNext 或 Reset 之前返回相同的对象。 MoveNext 将 Current 设置为下一个元素。
传递到集合末尾后,枚举数将放置在集合中最后一个元素之后,并且调用 MoveNext 返回 false。 如果对 MoveNext 的最后一次调用返回 false,则调用 Current 会引发异常。 要再次将 Current 设置为集合的第一个元素,请调用 Reset,然后调用 MoveNext。
只要集合保持不变,枚举器就保持有效。 如果对集合进行更改(例如添加、修改或删除元素),则枚举数将变得无效且不可恢复,并且下次调用 MoveNext 或 Reset 时将引发 InvalidOperationException。 如果集合在 MoveNext 和 Current 之间被修改,Current 将返回它所设置的元素,即使枚举器不再有效。
枚举器没有对集合的独占访问权; 因此,枚举集合本质上并不是一个线程安全的过程。 即使集合已同步,其他线程也可以修改该集合,从而导致枚举器引发异常。 为了确保枚举期间的线程安全,您可以在整个枚举过程中锁定集合或捕获由其他线程所做的更改引起的异常。
产量关键词
在迭代器块中使用,为枚举器对象提供值或表示迭代结束。 其形式为下列⑥之一:
收益率回报;
产量突破;
评论:
计算表达式并将其作为枚举器对象值返回; expression_r 必须可隐式转换为迭代器的yield 类型。
Yield 语句只能出现在迭代器块中,迭代器块可用作方法、运算符或访问器的主体。 此类方法、运算符或访问器的主体受以下约束约束:
不允许不安全的块。
方法、运算符或访问器参数不能为 ref 或 out。
Yield 语句不能出现在匿名方法中。
与 expression_r 一起使用时,yield return 语句不能出现在 catch 块内或包含一个或多个 catch 子句的 try 块内。
Yield return 提供了迭代器的一个重要功能,就是获取数据后立即返回。 无需将所有数据加载到数组中,有效提高了遍历效率。
Unity 启动协程
Unity使用StartCoroutine(routine: IEnumerator):Coroutine来启动协程,参数必须是IEnumerator对象。 那么Unity在幕后做了哪些神奇的处理呢?
我通常通过传入一个返回值为 IEnumerator 的函数来获取 StartCoroutine 函数的参数:
IEnumerator WaitAndPrint(float waitTime) { yield return new WaitForSeconds(waitTime); print("WaitAndPrint " + Time.time); }
使用函数中前面介绍的yield 关键字返回一个IEnumerator 对象。 Unity实现YieldInstruction作为yield返回的基类,并由几个子类实现:Cortoutine、WaitForSecondes、WaitForEndOfFrame、WaitForFixedUpdate和WWW。 StartCoroutine 将传入的 IEnumerator 封装为 Coroutine 并返回。 引擎存储并检查协程 IEnumerator 的当前值。
③枚举WWW、WaitForSeconds、null和WaitForEndOfFrame来检查Current值在MonoBebaviour生命周期中的时间(没有WaitForFixedUpdate,DSQiu猜测作者写的是Unity引擎没有提供这个实现):
WWW - 所有游戏对象更新后; 检查 isDone 标志。 如果为 trueunity 死循环协程,则调用 IEnumerator 的 MoveNext() 函数;
WaitForSeconds - 所有游戏对象发生更新后; 检查时间是否已到,如果已到,则调用 MoveNext();
null 或某个未知值 - 在所有游戏对象发生更新后; 调用 MoveNext();
WaitForEndOfFrame - 在所有摄像机发生渲染之后; 调用 MoveNext()。
如果最后一个yield返回的IEnumerator已经迭代到最后一个,MoveNext将返回false。 这时,Unity就会从协程列表中删除这个IEnumerator。
所以很容易产生误解:协程不是并行的。 它们与其他代码在同一线程中运行游戏素材下载 免费,因此当在 Update 和 Coroutine 中使用相同的值时,它会变得线程安全。 这是Unity对于线程安全的解决方案——不直接使用线程。 最近有很多关于 Unity 5 发布的讨论。 我看到它具有完整的多线程支持。 我不知道它是如何实现的。 从技术角度来说,难度还是很大的。 期待它。
总结一下:在协程方法中使用yield return实际上就是返回一个IEnumerator对象。 只有当该对象的MoveNext()返回false,即IEnumertator的Current已经迭代到最后一个元素时,才会执行yield返回。 陈述。 换句话说,yield return 将被“翻译”为 IEnmerator 对象。 如果您想了解更多详情,可以点击⑤查看。
根据⑤C#深入理解——C#编译器会生成一个IEnumerator对象。 该对象实现的MoveNext()包括函数内所有yield return的处理。 这里仅举一个例子:
using System; using System.Collections; class Test { static IEnumerator GetCounter() { for (int count = 0; count < 10; count++) { yield return count; } } }
C# 编译器生成:
internal class Test { // Note how this doesn't execute any of our original code private static IEnumerator GetCounter() { return newd__0(0); } // Nested type automatically created by the compiler to implement the iterator [CompilerGenerated] private sealed class d__0 : IEnumerator
从上面的C#实现中我们可以知道:函数中有多少个yield返回,对应的MoveNext()就会有多少次返回true(不包括嵌套)。 另一个非常重要的一点是:同一函数中的其他代码(不是yield return语句)将被移动到MoveNext。 也就是说,每次MoveNext都会在yield返回之前、之后执行相同的函数。 收益率回报之间的代码。
对于Unity引擎的YieldInstruction实现,其实可以看一个函数体。 该函数体每帧都会检查 MoveNext 是否返回 false。 例如:
yield retrun new WaitForSeconds(2f);
上面这行代码的伪代码实现:
private float elapsedTime; private float time; private void MoveNext() { elapesedTime += Time.deltaTime; if(time <= elapsedTime) return false; else return true; }
添加于:2014年4月22日8:00
2)协程扩展——扩展协程:返回值和错误处理
不知道大家在调用StartCortoutine时是否注意到,StartCortoutine返回的是YieldInstruction的子类Cortoutine对象。 除了生成的重新运行 StartCortoutine 中的嵌套 StartCortoutine 之外,此返回很有用。 在其他情况下,不考虑其存在。 反正DSQiu就是这样,我一直相信东西都有“极端”的用处,所以每次调用StartCortoutine的时候我都很纠结。 嗯,有点强迫症了。
Unity引擎将StartCoroutine传入的IEnumerator参数封装成Coroutine对象,而Coroutine对象实际上是一个IEnumerator枚举对象。 Yield 返回的 IEnumerator 对象都存储在这个协程中。 只有当上一个yield return的IEnumerator迭代完成后游戏素材,才会运行下一个。 这是基于Unity底层对Cortountine的统一管理(即上面提到的检查Current值):Unity底层应该有一个正在运行的Cortoutine列表,并在每一帧的不同时间检查它。
回到主题,上面提到的yield关键字不允许出现不安全块,也就是说它不能出现在try catch块中,并且在yield return执行过程中不能进行错误检查。 ③使用StartCortoutine返回值Cortoutine获取当前Current值并进行错误捕获处理。
首先定义一个类,封装返回值和错误信息:
public class Coroutine{ public T Value { get{ if(e != null){ throw e; } return returnVal; } } private T returnVal; //当前迭代器的Current 值 private Exception e; //抛出的错误信息 public Coroutine coroutine; public IEnumerator InternalRoutine(IEnumerator coroutine){ //先省略这部分的处理 } }
InteralRoutine 返回当前值和抛出的异常信息(如果有):
public IEnumerator InternalRoutine(IEnumerator coroutine){ while(true){ try{ if(!coroutine.MoveNext()){ yield break; } } catch(Exception e){ this.e = e; yield break; } object yielded = coroutine.Current; if(yielded != null && yielded.GetType() == typeof(T)){ returnVal = (T)yielded; yield break; } else{ yield return coroutine.Current; } }
以下内容扩展了此类的 MonoBehavior:
public static class MonoBehaviorExt{ public static CoroutineStartCoroutine (this MonoBehaviour obj, IEnumerator coroutine){ Coroutine coroutineObject = new Coroutine (); coroutineObject.coroutine = obj.StartCoroutine(coroutineObject.InternalRoutine(coroutine)); return coroutineObject; } }
最后给出一个例子:
IEnumerator Start () { var routine = StartCoroutine(TestNewRoutine()); //Start our new routine yield return routine.coroutine; // wait as we normally can Debug.Log(routine.Value); // print the result now that it is finished. } IEnumerator TestNewRoutine(){ yield return null; yield return new WaitForSeconds(2f); yield return 10; yield return 5; }
最终输出为10,因为Cortoutine遇到满足条件的T类型时执行yieldbreak; 它不执行yield return 5; 陈述。
如果中期收益率突破; 语句被删除,最终输出是 5 而不是 10。
if(yielded != null && yielded.GetType() == typeof(T)){ returnVal = (T)yielded; yield break; }
事实上,Unity引擎每帧都会检查yield return后的表达式。 如果满足,就会继续执行。
下面是一个测试的例子:连续调用yield return协程两次;
private Coroutine routine1; void Start () { routine1 = StartCoroutine(TestCoroutineExtention1()); //Start our new routine StartCoroutine(TestCortoutine()); } IEnumerator TestCoroutineExtention1() { yield return new WaitForSeconds(1); yield return 10; Debug.Log("Run 10!"); yield return new WaitForSeconds(5); yield return 5; Debug.Log("Run 5!"); } IEnumerator TestCortoutine() { //wwwState = true; yield return routine1; // wait as we normally can Debug.Log(" routine1"); yield return routine1; // wait as we normally can Debug.Log(" routine2"); }
测试运行只会输出:
跑10!
跑5!
常规1
总结一下:yield return expression只有当表达式执行完毕后才会继续执行下面的代码。 yield return StartCortoutine()的返回值不会连续两次满足,说明yield return有区分开始和结束的两种状态。 。
3)协程锁定
虽然 Cortoutine 不是多线程机制,但仍然会存在“并发”问题——同时多次调用 StartCortoutine。 当然,也可以通过Unity提供的API来获取解决方案。 在每个 StartCoroutine 之前,都会调用 StopCortoutine 方法来停止,但这使用了反射。 ,显然效率并不好。 ④ 扩展了③的解决方案,提供对Cortoutine Locking的支持。 使用字符串(方法名)来标记同一个协程方法。 对于同一个方法,如果等待时间超过timeout,就会终止之前的Coroutine方法。 直接贴在下面代码:
using UnityEngine; using System; using System.Collections; using System.Collections.Generic; ////// Extending MonoBehaviour to add some extra functionality /// Exception handling from: http://twistedoakstudios.com/blog/Post83_coroutines-more-than-you-want-to-know /// /// 2013 Tim Tregubov /// public class TTMonoBehaviour : MonoBehaviour { private LockQueue LockedCoroutineQueue { get; set; } ////// Coroutine with return value AND exception handling on the return value. /// public CoroutineStartCoroutine (IEnumerator coroutine) { Coroutine coroutineObj = new Coroutine (); coroutineObj.coroutine = base.StartCoroutine(coroutineObj.InternalRoutine(coroutine)); return coroutineObj; } /// /// Lockable coroutine. Can either wait for a previous coroutine to finish or a timeout or just bail if previous one isn't done. /// Caution: the default timeout is 10 seconds. Coroutines that timeout just drop so if its essential increase this timeout. /// Set waitTime to 0 for no wait /// public CoroutineStartCoroutine (IEnumerator coroutine, string lockID, float waitTime = 10f) { if (LockedCoroutineQueue == null) LockedCoroutineQueue = new LockQueue(); Coroutine coroutineObj = new Coroutine (lockID, waitTime, LockedCoroutineQueue); coroutineObj.coroutine = base.StartCoroutine(coroutineObj.InternalRoutine(coroutine)); return coroutineObj; } /// /// Coroutine with return value AND exception handling AND lockable /// public class Coroutine{ private T returnVal; private Exception e; private string lockID; private float waitTime; private LockQueue lockedCoroutines; //reference to objects lockdict private bool lockable; public Coroutine coroutine; public T Value { get { if (e != null) { throw e; } return returnVal; } } public Coroutine() { lockable = false; } public Coroutine(string lockID, float waitTime, LockQueue lockedCoroutines) { this.lockable = true; this.lockID = lockID; this.lockedCoroutines = lockedCoroutines; this.waitTime = waitTime; } public IEnumerator InternalRoutine(IEnumerator coroutine) { if (lockable && lockedCoroutines != null) { if (lockedCoroutines.Contains(lockID)) { if (waitTime == 0f) { //Debug.Log(this.GetType().Name + ": coroutine already running and wait not requested so exiting: " + lockID); yield break; } else { //Debug.Log(this.GetType().Name + ": previous coroutine already running waiting max " + waitTime + " for my turn: " + lockID); float starttime = Time.time; float counter = 0f; lockedCoroutines.Add(lockID, coroutine); while (!lockedCoroutines.First(lockID, coroutine) && (Time.time - starttime) < waitTime) { yield return null; counter += Time.deltaTime; } if (counter >= waitTime) { string error = this.GetType().Name + ": coroutine " + lockID + " bailing! due to timeout: " + counter; Debug.LogError(error); this.e = new Exception(error); lockedCoroutines.Remove(lockID, coroutine); yield break; } } } else { lockedCoroutines.Add(lockID, coroutine); } } while (true) { try { if (!coroutine.MoveNext()) { if (lockable) lockedCoroutines.Remove(lockID, coroutine); yield break; } } catch (Exception e) { this.e = e; Debug.LogError(this.GetType().Name + ": caught Coroutine exception! " + e.Message + "\n" + e.StackTrace); if (lockable) lockedCoroutines.Remove(lockID, coroutine); yield break; } object yielded = coroutine.Current; if (yielded != null && yielded.GetType() == typeof(T)) { returnVal = (T)yielded; if (lockable) lockedCoroutines.Remove(lockID, coroutine); yield break; } else { yield return coroutine.Current; } } } } /// /// coroutine lock and queue /// public class LockQueue { private Dictionary> LockedCoroutines { get; set; } public LockQueue() { LockedCoroutines = new Dictionary >(); } /// /// check if LockID is locked /// public bool Contains(string lockID) { return LockedCoroutines.ContainsKey(lockID); } ////// check if given coroutine is first in the queue /// public bool First(string lockID, IEnumerator coroutine) { bool ret = false; if (Contains(lockID)) { if (LockedCoroutines[lockID].Count > 0) { ret = LockedCoroutines[lockID][0] == coroutine; } } return ret; } ////// Add the specified lockID and coroutine to the coroutine lockqueue /// public void Add(string lockID, IEnumerator coroutine) { if (!LockedCoroutines.ContainsKey(lockID)) { LockedCoroutines.Add(lockID, new List()); } if (!LockedCoroutines[lockID].Contains(coroutine)) { LockedCoroutines[lockID].Add(coroutine); } } /// /// Remove the specified coroutine and queue if empty /// public bool Remove(string lockID, IEnumerator coroutine) { bool ret = false; if (LockedCoroutines.ContainsKey(lockID)) { if (LockedCoroutines[lockID].Contains(coroutine)) { ret = LockedCoroutines[lockID].Remove(coroutine); } if (LockedCoroutines[lockID].Count == 0) { ret = LockedCoroutines.Remove(lockID); } } return ret; } } }
概括:
本文主要了解Unity StartCoroutine,从C#的yield和IEnumerator到Unity的StartCoroutine,最后扩展Cortoutine。 虽然感觉不是很实用(很少情况下使用),但是对于理解Coroutine还是有好处的。 理解并思考。
我感觉第三部分的代码有问题,我还没有测试过。 附件中有代码。 如果您需要的话,请您自行领取。
如果您对DSQiu有什么建议或意见,可以在文章后面留言,或者发送邮件()进行交流。 您的鼓励和支持是我前进的动力,希望有更多更好的分享。