本文包含两个部分,前半部分是通俗解释一下Unity中的协程,后半部分讲讲C#的IEnumerator迭代器

协程是什么,能干什么?

为了能通俗的解释,我们先用一个简单的例子来看看协程可以干什么

首先,我突发奇想,要实现一个倒计时器,我可能是这样写的:

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. public float sumtime = 3;
  4. void Update()//Update是每帧调用的
  5. {
  6. {
  7. sumtime -= Time.deltaTime;
  8. if (sumtime <= 0)
  9. Debug.Log("Done!");
  10. }
  11. }
  12. }

我们知道,写进 Update() 里的代码会被每帧调用一次

所以,让总时间sumtime在Update()中每一帧减去一个增量时间Time.deltaTime(可以理解成帧与帧的间隔时间)就能实现一个简单的倒计时器

但是,当我们需要多个独立的计时器时,用同样的思路,我们的代码可能就会写成这样:

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. public float sumtime1 = 3;
  4. public float sumtime2 = 2;
  5. public float sumtime3 = 1;
  6. void Update()
  7. {
  8. sumtime1 -= Time.deltaTime;
  9. if (sumtime1 <= 0)
  10. Debug.Log("timer1 Done!");
  11. sumtime2 -= Time.deltaTime;
  12. if (sumtime2 <= 0)
  13. Debug.Log("timer2 Done!");
  14. sumtime3 -= Time.deltaTime;
  15. if (sumtime3 <= 0)
  16. Debug.Log("timer3 Done!");
  17. }
  18. }

重复度很高,计时器越多看的越麻烦

然后有朋友可能会提到,我们是不是可以用一个循环来解决这个问题

  1. for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime)
  2. {
  3. //nothing
  4. }
  5. Debug.Log("This happens after 5 seconds");

现在每一个计时器变量都成为for循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。

但是

但是

但是

我们知道Update()每帧调用一次的,我们不能把这个循环直接写进Update() 里,更不能写一个方法在Update() 里调用,因为这相当于每帧开启一个独立的循环

那么有没有办法,再Update()这个主线程之外再开一个单独的线程,帮我们管理这个计时呢?

好了,你可能知道我想说什么了,我们正好可以用协程来干这个

先来看一段简单的协程代码

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. void Start()
  4. {
  5. StartCoroutine(Count3sec());
  6. }
  7. IEnumerator Count3sec()
  8. {
  9. for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime)
  10. yield return 0;
  11. Debug.Log("This happens after 3 seconds");
  12. }
  13. }

你很可能看不懂上面的几个关键字,但不急,我们一个个解释上面的代码干了什么

  1. StartCoroutine(Count3sec());

这一句用来开始我们的Count3sec方法

然后你可能想问的是

  1. IEnumerator 是什么?返回值是什么?
  2. For循环中的yield return是什么?

理解以下的话稍有难度,但暂时理解不了问题也不大

详细的讲:

IEnumerator 是C#的一个迭代器,你可以把它当成指向一个序列的某个节点的指针,C#提供了两个重要的接口,分别是Current(返回当前指向的元素)和 MoveNext()(将指针向前移动一个单位,如果移动成功,则返回true)

通常,如果你想实现一个接口,你可以写一个类,实现成员,等等。迭代器块(iterator block) 是一个方便的方式实现IEnumerator,你只需要遵循一些规则,并实现IEnumerator由编译器自动生成。

一个迭代器块具备如下特征:

  1. 返回IEnumerator
  2. 使用yield关键字

那么yield关键字是干嘛的?它用来声明序列中的下一个值,或者一个无意义的值。如果使用yield x(x是指一个具体的对象或数值)的话,那么movenext返回为true并且current被赋值为x,如果使用yield break使得movenext()返回false(停止整个协程)

看不太懂?问题不大

简单来说:

你现在只需要理解,上面代码中,IEnumerator类型的方法Count3sec就是一个协程,并且可以通过yield关键字控制协程的运行

一个协程的执行,可以在任何地方用yield语句来暂停,yield return的值决定了什么时候协程恢复执行。通俗点讲,当你“yield”一个方法时,你相当于对这个程序说:“现在停止这个方法,然后在下一帧中,从这里重新开始!”

  1. yield return 0;

然后你可能会问,yield return后面的数字表示什么?比如yield return 10,是不是表示延缓10帧再处理?

并不!

并不!

并不!

yield return 0表示暂缓一帧,也就是让你的程序等待一帧,再继续运行。(不一定是一帧,下面会讲到如何控制等待时间)就算你把这个0换成任意的int类型的值,都是都是表示暂停一帧,从下一帧开始执行

它的效果类似于主线程单独出了一个子线程来处理一些问题,而且性能开销较小

现在你大致学会了怎么开启协程,怎么写协程了,来看看我们还能干点什么:

  1. IEnumerator count5times()
  2. {
  3. yield return 0;
  4. Debug.Log("1");
  5. yield return 0;
  6. Debug.Log("2");
  7. yield return 0;
  8. Debug.Log("3");
  9. yield return 0;
  10. Debug.Log("4");
  11. yield return 0;
  12. Debug.Log("5");
  13. }

在这个协程中,我们每隔一帧输出了一次Hello,当然你也可以改成一个循环

  1. IEnumerator count5times()
  2. {
  3. for (int i = 0; i < 5; i++)
  4. {
  5. Debug.Log("i+1");
  6. yield return 0;
  7. }
  8. }

重点来了,有意思的是,你可以在这里加一个记录始末状态的变量:

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. bool isDone = false;
  4. IEnumerator count5times()
  5. {
  6. Debug.Log(isDone);
  7. for (int i = 0; i < 5; i++)
  8. {
  9. Debug.Log("i+1");
  10. yield return 0;
  11. }
  12. isDone = true;
  13. Debug.Log(isDone);
  14. }
  15. void Start()
  16. {
  17. StartCoroutine(count5times());
  18. }
  19. }

很容易看得出上面的代码实现了什么,也就就是我们一开始的需求,计时器

这个协程方法突出了协程一个“非常有用的,和Update()不同的地方:方法的状态能被存储,这使得方法中定义的这些变量(比如isUpdate)都会保存它们的值,即使是在不同的帧中

再修改一下,就是一个简单的协程计时器了

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. IEnumerator countdown(int count, float frequency)
  4. {
  5. Debug.Log("countdown START!");
  6. for (int i = 0; i < count; i++)
  7. {
  8. for (float timer = 0; timer < frequency; timer += Time.deltaTime)
  9. yield return 0;
  10. }
  11. Debug.Log("countdown DONE!");
  12. }
  13. void Start()
  14. {
  15. StartCoroutine(countdown(5, 1.0f));
  16. }
  17. }

在上面的例子我们也能看出,和普通方法一样,协程方法也可以带参数

你甚至可以通过yield一个WaitForSeconds()更方便简洁地实现倒计时

协程计时器

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. IEnumerator countdown(float sec)//参数为倒计时时间
  4. {
  5. Debug.Log("countdown START!");
  6. yield return new WaitForSeconds(sec);
  7. Debug.Log("countdown DONE!");
  8. }
  9. void Start()
  10. {
  11. StartCoroutine(countdown(5.0f));
  12. }
  13. }

好了,可能你已经注意到了,yield的用法还是很多的

在此之前,我们之前的代码yield的时候总是用0(或者可以用null),这仅仅告诉程序在继续执行前等待下一帧。现在你又学会了用yield return new WaitForSeconds(sec)来控制等待时间,你已经可以做更多的骚操作了!

协程另外强大的一个功能就是,你甚至可以yeild另一个协程,也就是说,你可以通过使用yield语句来相互嵌套协程

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. IEnumerator SaySomeThings()
  4. {
  5. Debug.Log("The routine has started");
  6. yield return StartCoroutine(Wait(1.0f));
  7. Debug.Log("1 second has passed since the last message");
  8. yield return StartCoroutine(Wait(2.5f));
  9. Debug.Log("2.5 seconds have passed since the last message");
  10. }
  11. IEnumerator Wait(float waitsec)
  12. {
  13. for (float timer = 0; timer < waitsec; timer += Time.deltaTime)
  14. yield return 0;
  15. }
  16. void Start()
  17. {
  18. StartCoroutine(SaySomeThings());
  19. }
  20. }
  1. yield return StartCoroutine(Wait(1.0f));

这里的Wait指的是另一个协程,这相当于是说,“暂停执行本程序,等到直到Wait协程结束”

协程控制对象行为

根据我们上面讲的特性,协程还能像创建计时器一样方便的控制对象行为,比如物体运动到某一个位置

  1. IEnumerator MoveToPosition(Vector3 target)
  2. {
  3. while (transform.position != target)
  4. {
  5. transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
  6. yield return 0;
  7. }
  8. }

我们还可以让上面的程序做更多,不仅仅是一个指定位置,还可以通过数组来给它指定更多的位置,然后通过MoveToPosition() ,可以让它在这些点之间持续运动。

我们还可以再加入一个bool变量,控制在对象运动到最后一个点时是否要进行循环

再把上文的Wait()方法加进来,这样就能让我们的对象在某个点就可以选择是否暂停下来,停多久,就像一个正在巡逻的守卫一样 (这里没有实现,各位读者可以尝试自己写一个)

  1. public class CoroutineTest : MonoBehaviour
  2. {
  3. public Vector3[] path;
  4. public float moveSpeed;
  5. void Start()
  6. {
  7. StartCoroutine(MoveOnPath(true));
  8. }
  9. IEnumerator MoveOnPath(bool loop)
  10. {
  11. do
  12. {
  13. foreach (var point in path)
  14. yield return StartCoroutine(MoveToPosition(point));
  15. }
  16. while (loop);
  17. }
  18. IEnumerator MoveToPosition(Vector3 target)
  19. {
  20. while (transform.position != target)
  21. {
  22. transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
  23. yield return 0;
  24. }
  25. }
  26. IEnumerator Wait(float waitsec)
  27. {
  28. for (float timer = 0; timer < waitsec; timer += Time.deltaTime)
  29. yield return 0;
  30. }
  31. }

yield其他

这里列举了yield后面可以有的表达式

  1. null,0,1,...... 暂缓一帧,下一帧继续执行

  2. WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete 等待帧结束

  3. WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated 等待一个固定帧

  4. WaitForSeconds - causes the coroutine not to execute for a given game time period

  5. WWW - waits for a web request to complete (resumes as if WaitForSeconds or null)

  6. StartCoroutine(Another coroutine) - in which case the new coroutine will run to completion before the yielder is resumed 等待另一个协程暂停

值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足

停止协程

  1. StopCoroutine(string methodName);
  2. StopAllCoroutine();
  3. 设置gameobject的active为false时可以终止协同程序,但是再次设置为true后协程不会再启动。

总结一下

协程就是:你可以写一段顺序代码,然后标明哪里需要暂停,然后在指定在下一帧或者任意间后,系统会继续执行这段代码

当然,协程不是真多线程,而是在一个线程中实现的

通过协程我们可以方便的做出一个计时器,甚至利用协程控制游戏物体平滑运动

如果你刚接触协程,我希望这篇博客能帮助你了解它们是如何工作的,以及如何来使用它们

深入讲讲IEnumerator

基础迭代器IEnumerator

迭代器是C#中一个普通的接口类,类似于C++ iterator的概念,基础迭代器是为了实现类似for循环 对指定数组或者对象 的 子元素 逐个的访问而产生的。

  1. public interface IEnumerator
  2. {
  3. object Current { get; }
  4. bool MoveNext();
  5. void Reset();
  6. }

以上是IEnumerator的定义

Current() 的实现应该是返回调用者需要的指定类型的指定对象。

MoveNext() 的实现应该是让迭代器前进。

Reset() 的实现应该是让迭代器重置未开始位置

就像上文提到的,C#提供了两个重要的接口,分别是Current(返回当前指向的元素)和 MoveNext()(将指针向前移动一个单位,如果移动成功,则返回true)当然IEnumerator是一个interface接口,你不用担心的具体实现

注意以上用的都是“应该是”,也就是说我们可以任意实现一个派生自” IEnumerator”类的3个函数的功能,但是如果不按设定的功能去写,可能会造成被调用过程出错,无限循环

一个简单的例子,遍历并打印一个字符串数组:

  1. public string[] m_StrArray = new string[4];

就可以派生一个迭代器接口的子类

  1. public class StringPrintEnumerator : IEnumerator
  2. {
  3. private int m_CurPt = -1;
  4. private string[] m_StrArray;
  5. public StringPrintEnumerator(string[] StrArray)
  6. {
  7. m_StrArray = StrArray;
  8. }
  9. ///实现
  10. public object Current
  11. {
  12. get
  13. {
  14. return m_StrArray[m_CurPt];
  15. }
  16. }
  17. public bool MoveNext()
  18. {
  19. m_CurPt++;
  20. if (m_CurPt == m_StrArray.Length)
  21. return false;
  22. return true;
  23. }
  24. public void Reset()
  25. {
  26. m_CurPt = -1;
  27. }
  28. ///实现END
  29. public static void Run()
  30. {
  31. string[] StrArray = new string[4];
  32. StrArray[0] = "A";
  33. StrArray[1] = "B";
  34. StrArray[2] = "C";
  35. StrArray[3] = "D";
  36. StringPrintEnumerator StrEnum = new StringPrintEnumerator(StrArray);
  37. while (StrEnum.MoveNext())
  38. {
  39. (string)ObjI = (string)StrEnum.Current;
  40. Debug.Log(ObjI);
  41. }
  42. }
  43. }

运行会依次输出A B C D

但是如果:

不正确的实现Current(返回null,数组下表越界)执行到Debug.Log时候会报错。

不正确地MoveNext(),可能会出现无限循环(当然如果逻辑上正需要这样,也是正确的)

不正确地Reset(),下次再用同一个迭代器时候不能正确工作

所以这三个方法如何才是正确的实现,完全要根据由上层的调用者约定来写

迭代器扩展应用foreach,IEnumerable

C#使用foreach语句取代了每次手写while(StrEnum.MoveNext())进行遍历

同时新定了一个接口类来包装迭代器IEnumerator,也就是IEnumerable,定义为:

  1. public interface IEnumerable
  2. {
  3. IEnumerator GetEnumerator();
  4. }

IEnumerable和IEnumerator的区别

可以看到IEnumerable接口非常的简单,只包含一个抽象的方法GetEnumerator(),它返回一个可用于循环访问集合的IEnumerator对象

IEnumerable的作用仅仅是需要派生类写一个返回指定迭代器的实现方法,也就是说IEnumerable仅仅是IEnumerator的一个包装而已。

那么返回的IEnumerator对象呢?它是一个真正的集合访问器,没有它,就不能使用foreach语句遍历集合或数组,因为只有IEnumerator对象才能访问集合中的项,才能进行集合的循环遍历。

那么我们回到foreach

foreach

就像上面提到的,foreach需要的是一个定义了IEnumerator GetEnumerator()方法的对象,当然如果他是派生自IEnumerable对象那就更好了。

我们继续写上文的StringPrintEnumerator类

这里新定义他的IEnumerable派生类MyEnumerable


Unity 新手入门 如何理解协程 IEnumerator yield的更多相关文章

  1. C#中的yield return与Unity中的Coroutine(协程)(下)

    Unity中的Coroutine(协程) 估计熟悉Unity的人看过或者用过StartCoroutine() 假设我们在场景中有一个UGUI组件, Image: 将以下代码绑定到Image using ...

  2. 深入理解协程(二):yield from实现异步协程

    原创不易,转载请联系作者 深入理解协程分为三部分进行讲解: 协程的引入 yield from实现异步协程 async/await实现异步协程 本篇为深入理解协程系列文章的第二篇. yield from ...

  3. 深入理解协程(三):async/await实现异步协程

    原创不易,转载请联系作者 深入理解协程分为三部分进行讲解: 协程的引入 yield from实现异步协程 async/await实现异步协程 本篇为深入理解协程系列文章的最后一篇. 从本篇你将了解到: ...

  4. 深入理解协程(四):async/await异步爬虫实战

    本文目录: 同步方式爬取博客标题 async/await异步爬取博客标题 本片为深入理解协程系列文章的补充. 你将会在从本文中了解到:async/await如何运用的实际的爬虫中. 案例 从CSDN上 ...

  5. Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)

    Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...

  6. 终结python协程----从yield到actor模型的实现

    把应用程序的代码分为多个代码块,正常情况代码自上而下顺序执行.如果代码块A运行过程中,能够切换执行代码块B,又能够从代码块B再切换回去继续执行代码块A,这就实现了协程 我们知道线程的调度(线程上下文切 ...

  7. Unity学习疑问记录之协程

    http://blog.csdn.net/huang9012/article/details/38492937 总结:1.协程相当于多线程但不是,(尽管它们看上去是这样的),它们运行在同一线程中,跟普 ...

  8. 理解Python协程:从yield/send到yield from再到async/await

    Python中的协程大概经历了如下三个阶段:1. 最初的生成器变形yield/send2. 引入@asyncio.coroutine和yield from3. 在最近的Python3.5版本中引入as ...

  9. Unity3D之协程(Coroutines & Yield )

    在Unity中StartCoroutine/yield return这个模式到底是怎么应用的? 比如你要一个方法进行一个比较耗时的复杂运算~同时又想让脚本流畅的进行其他操作而不是卡在那里等该方法执行完 ...

随机推荐

  1. 中文乱码总结之JSP乱码

    一.JSP中文乱码问题 JSP页面的汉字显示为乱码,而英文和阿拉伯数字正常. 二.原因 汉字编码时用的字符集 与解码用的字符集不一致:所有的字符集都兼容ASCII码,所以英文.数字不存在乱码. 编号 ...

  2. PAT甲级1103 Integer Factorization【dfs】【剪枝】

    题目:https://pintia.cn/problem-sets/994805342720868352/problems/994805364711604224 题意: 给定一个数n,要求从1~n中找 ...

  3. Java+Selenium操作日期时间选择框插件

    在自动化测试的时候我们经常会碰到下面的时间日期插件(这个时候这个文本框是不运行我们输入时间的), 我们可以用java获取当前日期,然后用Selenium结合JS代码就可以直接往文本框输入内容. 像这种 ...

  4. PDM:Training Models of Shape from Sets of Examples

    这篇论文介绍了一种创建柔性形状模型(Flexible Shape Models)的方法--点分布模型(Point Distribution Model).该方法使用一系列标记点来表示形状,重要的是根据 ...

  5. memory error python报错

    np.array时报错内存溢出,检查了python安装的是64位版本,通过下面dtype=np.uint8不再报错texts_vec = (np.array(texts_vec,dtype=np.ui ...

  6. paginate()出来的数据怎样循环插入数据?

    paginate()出来的数据怎样循环插入数据? paginate()分页如何转数组操作数据之后再转回对象? thinkphp5 model里面用toarray后怎么分页? 以上类似问题的出现,是因为 ...

  7. 时区切换导致quartz定时任务没有触发问题

    时区切换对Quartz的cron表达式有影响,切换的1小时内停止触发定时任务,导致sla没有定时清空内存计数,误发限流. 美国夏令时PST切换到冬令时PDT,会有时间跳变.不带时区跳变的,会出现时间重 ...

  8. 使用Apache JMeter对SQL Server、Mysql、Oracle压力测试(一)

    前段时间面试被问到了数据库方面的知识:比如选择什么样的数据库,如何优化,怎么加索引,于是想到了自己动手测试一下常用数据库的性能: 第一步,下载好JMeter之后打开运行.话说这个JMeter打开还真是 ...

  9. zookeeper的使用demo(c#/java)

    Zookeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储,但是 Zookeeper 并不是用来专门存储数据的,它 ...

  10. ESP8266清理flash学习记录

    学习来源:http://bbs.eeworld.com.cn/thread-497588-1-1.html 还稍看了电子产品世界 主要内容 1在windows 上通过 命令行 安装  Python环境 ...