Unity 新手入门 如何理解协程 IEnumerator yield

Unity 新手入门 如何理解协程 IEnumerator

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

协程是什么,能干什么?

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

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

public class CoroutineTest : MonoBehaviour
{public float sumtime = 3;void Update()//Update是每帧调用的{{sumtime -= Time.deltaTime;if (sumtime <= 0)Debug.Log("Done!");}}
}

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

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

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

public class CoroutineTest : MonoBehaviour
{public float sumtime1 = 3;public float sumtime2 = 2;public float sumtime3 = 1;void Update(){sumtime1 -= Time.deltaTime;if (sumtime1 <= 0)Debug.Log("timer1 Done!");sumtime2 -= Time.deltaTime;if (sumtime2 <= 0)Debug.Log("timer2 Done!");sumtime3 -= Time.deltaTime;if (sumtime3 <= 0)Debug.Log("timer3 Done!");}
}

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

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

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

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

但是

但是

但是

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

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

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

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

public class CoroutineTest : MonoBehaviour
{void Start(){StartCoroutine(Count3sec());}IEnumerator Count3sec(){for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime)yield return 0;Debug.Log("This happens after 3 seconds");}
}

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

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”一个方法时,你相当于对这个程序说:“现在停止这个方法,然后在下一帧中,从这里重新开始!”

yield return 0;

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

并不!

并不!

并不!

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

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

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

    IEnumerator count5times(){yield return 0;Debug.Log("1");yield return 0;Debug.Log("2");yield return 0;Debug.Log("3");yield return 0;Debug.Log("4");yield return 0;Debug.Log("5");}

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

    IEnumerator count5times(){for (int i = 0; i < 5; i++){Debug.Log("i+1");yield return 0;}}

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

public class CoroutineTest : MonoBehaviour
{bool isDone = false;IEnumerator count5times(){Debug.Log(isDone);for (int i = 0; i < 5; i++){Debug.Log("i+1");yield return 0;}isDone = true;Debug.Log(isDone);}void Start(){StartCoroutine(count5times());}
}

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

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

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

public class CoroutineTest : MonoBehaviour
{IEnumerator countdown(int count, float frequency){Debug.Log("countdown START!");for (int i = 0; i < count; i++){for (float timer = 0; timer < frequency; timer += Time.deltaTime)yield return 0;}Debug.Log("countdown DONE!");}void Start(){StartCoroutine(countdown(5, 1.0f));}
}

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

1536438-20190405140259723-361758790.png

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

协程计时器

public class CoroutineTest : MonoBehaviour
{IEnumerator countdown(float sec)//参数为倒计时时间{Debug.Log("countdown START!");yield return new WaitForSeconds(sec);Debug.Log("countdown DONE!");}void Start(){StartCoroutine(countdown(5.0f));}
}

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

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

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

public class CoroutineTest : MonoBehaviour
{IEnumerator SaySomeThings(){Debug.Log("The routine has started");yield return StartCoroutine(Wait(1.0f));Debug.Log("1 second has passed since the last message");yield return StartCoroutine(Wait(2.5f));Debug.Log("2.5 seconds have passed since the last message");}IEnumerator Wait(float waitsec){for (float timer = 0; timer < waitsec; timer += Time.deltaTime)yield return 0;}void Start(){StartCoroutine(SaySomeThings());}
}
yield return StartCoroutine(Wait(1.0f));

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

协程控制对象行为

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

IEnumerator MoveToPosition(Vector3 target){while (transform.position != target){transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);yield return 0;}}

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

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

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

public class CoroutineTest : MonoBehaviour
{public Vector3[] path;  public float moveSpeed;void Start(){StartCoroutine(MoveOnPath(true));}IEnumerator MoveOnPath(bool loop){do{foreach (var point in path)yield return StartCoroutine(MoveToPosition(point));}while (loop);}IEnumerator MoveToPosition(Vector3 target){while (transform.position != target){transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);yield return 0;}}IEnumerator Wait(float waitsec){for (float timer = 0; timer < waitsec; timer += Time.deltaTime)yield return 0;}
}

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循环 对指定数组或者对象 的 子元素 逐个的访问而产生的。

public interface IEnumerator
{object Current { get; }bool MoveNext();void Reset();
}

以上是IEnumerator的定义

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

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

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

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

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

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

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

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

public class StringPrintEnumerator : IEnumerator
{private int m_CurPt = -1;private string[] m_StrArray;public StringPrintEnumerator(string[] StrArray){m_StrArray = StrArray;}///实现public object Current{get{return m_StrArray[m_CurPt];}}public bool MoveNext(){m_CurPt++;if (m_CurPt == m_StrArray.Length)return false;return true;}public void Reset(){m_CurPt = -1;}///实现ENDpublic static void Run(){string[] StrArray = new string[4];StrArray[0] = "A";StrArray[1] = "B";StrArray[2] = "C";StrArray[3] = "D";StringPrintEnumerator StrEnum = new StringPrintEnumerator(StrArray);while (StrEnum.MoveNext()){(string)ObjI = (string)StrEnum.Current;Debug.Log(ObjI);}}
}

运行会依次输出A B C D

但是如果:

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

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

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

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

迭代器扩展应用foreach,IEnumerable

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

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

public interface IEnumerable
{IEnumerator GetEnumerator();
}

IEnumerable和IEnumerator的区别

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

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

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

那么我们回到foreach

foreach

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

我们继续写上文的StringPrintEnumerator类

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

转载于:https://www.cnblogs.com/zhxmdefj/p/10655033.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/249243.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

百万级数据库优化方案

一、百万级数据库优化方案 1.对查询进行优化&#xff0c;要尽量避免全表扫描&#xff0c;首先应考虑在 where 及 order by 涉及的列上建立索引。 2.应尽量避免在 where 子句中对字段进行 null 值判断&#xff0c;否则将导致引擎放弃使用索引而进行全表扫描&#xff0c;如&#…

EM算法 小结

猴子吃果冻 博客园首页新随笔联系管理订阅随笔- 35 文章- 0 评论- 3 4-EM算法原理及利用EM求解GMM参数过程 1.极大似然估计 原理&#xff1a;假设在一个罐子中放着许多白球和黑球&#xff0c;并假定已经知道两种球的数目之比为1:3但是不知道那种颜色的球多。如果用放回抽样方…

SPSS-回归分析

回归分析&#xff08;一元线性回归分析、多元线性回归分析、非线性回归分析、曲线估计、时间序列的曲线估计、含虚拟自变量的回归分析以及逻辑回归分析&#xff09; 回归分析中&#xff0c;一般首先绘制自变量和因变量间的散点图&#xff0c;然后通过数据在散点图中的分布特点选…

HTML5概要与新增标签

一、HTML5概要 1.1、为什么需要HTML5 HTML4陈旧不能满足日益发展的互联网需要&#xff0c;特别是移动互联网。为了增强浏览器功能Flash被广泛使用&#xff0c;但安全与稳定堪忧&#xff0c;不适合在移动端使用&#xff08;耗电、触摸、不开放&#xff09;。 HTML5增强了浏览器的…

Tomcat启动失败错误解决Could not publish server configuration for Tomcat v8.0 Server at localhost....

这个问题本质是我们有多个重名项目&#xff0c;为什么我们会有多个重名项目&#xff0c;其实一般都是我们删除以前的项目&#xff0c;然后再把它重新导进eclipse时以前的项目删除不彻底造成的&#xff0c;以前的项目在"Servers"里面的"server.xml"文件下的…

Mock.js 和Node.js详细讲解

​​​​原文地址&#xff1a;http://www.manongjc.com/article/10503.html 《一统江湖的大前端》系列是自己的前端学习笔记&#xff0c;旨在介绍javascript在非网页开发领域的应用案例和发现各类好玩的js库&#xff0c;不定期更新。如果你对前端的理解还是写写页面绑绑事件&am…

架构图

负载均衡 分布式 转载于:https://www.cnblogs.com/jiqing9006/p/10672280.html

文本聊天室(TCP-中)

开始我们今天的代码实现&#xff0c;我们接着上一回&#xff0c;上回实现了服务器的代码这次实现客户端的UI(界面)层, 我们界面层采用javafx来进行绘制,首先有个登录服务器的界面然后切换到聊天界面运行结果如下.源代码如下: 1 package jffx.blogs.net;2 3 import javafx.appli…

爱好-摩托车:铃木

ylbtech-爱好-摩托车&#xff1a;铃木1.返回顶部 2.返回顶部3.返回顶部4.返回顶部5.返回顶部 1、http://www.suzuki-china.com/motor/2、6.返回顶部作者&#xff1a;ylbtech出处&#xff1a;http://ylbtech.cnblogs.com/本文版权归作者和博客园共有&#xff0c;欢迎转载&#x…

vue面试题,知识点汇总(有答案)

一. Vue核心小知识点 1、vue中 key 值的作用 key 的特殊属性主要用在 Vue的虚拟DOM算法&#xff0c;在新旧nodes对比时辨识VNodes。如果不使用key&#xff0c;Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用key&#xff0c;它会基于…

EF中Take和Skip的区别

以例子来说明&#xff1a; 数据库中Orders表如下&#xff1a; 代码部分&#xff1a; 运行结果&#xff1a; 可以看出&#xff1a;Take()方法的作用是从查询结果中提取前n个结果&#xff1b;而Skip()方法则是跳过前n个结果&#xff0c;返回剩余的结果。转载于:https://www.cnblo…

详解Vuex常见问题、深入理解Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。 状态&#xff1f;我把它理解为在data中的属性需要共享给其他vue组件使用的部分&#xff0c;就叫做状态。简单的…

Mac OS Git 安装

一、Git是一个分布式的代码版本管理工具。类似的常用工具还有SVN,CVS。最大的特点也是优点在于提供分布式的代码管理 1、分支代码只有一份&#xff01; 使用过svn的童鞋想必都知道&#xff0c;当我们要开发一个新功能或者增加一个新版本或者修改一个复杂bug的时候&#xff0c…

JS对象与jQuery对象

JS对象大致可以分为三种&#xff0c;如下图&#xff1a; JS常用内置对象&#xff08;JS自身所持有的对象&#xff0c;不需要创建&#xff0c;直接可用&#xff09;&#xff1a; String&#xff1a;API跟java的字符串API大致相同 两种创建对象的方式&#xff1a;String s1 “…

Nginx Slab内存管理

L38 Slub内存管理适用 ngx_http_limit_conn_module、ngx_http_limit_req_module 模块场景 我们可以用阿里第三方模块Slab_Stat模块 并且用add-module 方式编译进openresty中 转载于:https://www.cnblogs.com/jackey2015/p/10684151.html

Node.js异步库async

async的使用需要安装第三方包 1.串行无关联 async.series 函数依次执行,后面不需要调前面步骤的结果 程序执行时间为所有步骤之和 2.并行无关联 async.paraller 某步出错不影响其他步骤执行 程序执行时间为最长的那个时间 3.串行有关联 async.waterfall 函数依次执行,后面需要…

vue的双向绑定原理及实现

前言 使用vue也好有一段时间了&#xff0c;虽然对其双向绑定原理也有了解个大概&#xff0c;但也没好好探究下其原理实现&#xff0c;所以这次特意花了几晚时间查阅资料和阅读相关源码&#xff0c;自己也实现一个简单版vue的双向绑定版本&#xff0c;先上个成果图来吸引各位&a…

作业——8

这个作业属于哪个课程C语言程序设计Ⅱ这个作业的要求在哪里C语言作业评价标准我在这个课程的目标是指针与字符串这个作业在哪个具体方面帮助我实现目标使用指针与字符串参考文献指针和字符串&#xff08;基础知识&#xff09;第七周作业 一 1 、使用函数删除字符串中的字符 输入…

Vue实现组件props双向绑定解决方案

注意&#xff1a; 子组件不能直接修改prop过来的数据&#xff0c;会报错 方案一&#xff1a; 用data对象中创建一个props属性的副本 watch props属性 赋予data副本 来同步组件外对props的修改 watch data副本&#xff0c;emit一个函数 通知到组件外 HelloWorld组件代码如下…

Flask系列06--(中间件)Flask的特殊装饰器 before_request,after_request, errorhandler

一.使用 Flask中的特殊装饰器(中间件)方法常用的有三个 app.before_request # 在请求进入视图函数之前app.after_request # 在请求结束视图函数之后 响应返回客户端之前app.errorhandler(404) # 重定义错误信息before_request def func():passafter_request def func(ret): # …