本文包含两个部分,前半部分是通俗解释一下Unity中的协程,后半部分讲讲C#的IEnumerator迭代器web
为了能通俗的解释,咱们先用一个简单的例子来看看协程能够干什么c#
首先,我突发奇想,要实现一个倒计时器,我多是这样写的:数组
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(能够理解成帧与帧的间隔时间)就能实现一个简单的倒计时器app
可是,当咱们须要多个独立的计时器时,用一样的思路,咱们的代码可能就会写成这样:函数
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!"); } }
重复度很高,计时器越多看的越麻烦oop
而后有朋友可能会提到,咱们是否是能够用一个循环来解决这个问题性能
for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime) { //nothing } Debug.Log("This happens after 5 seconds");
如今每个计时器变量都成为for循环的一部分了,这看上去好多了,并且我不须要去单独设置每个跌倒变量。this
可是线程
可是
可是
咱们知道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方法
而后你可能想问的是
理解如下的话稍有难度,但暂时理解不了问题也不大
IEnumerator 是C#的一个迭代器,你能够把它当成指向一个序列的某个节点的指针,C#提供了两个重要的接口,分别是Current(返回当前指向的元素)和 MoveNext()(将指针向前移动一个单位,若是移动成功,则返回true)
一般,若是你想实现一个接口,你能够写一个类,实现成员,等等。迭代器块(iterator block) 是一个方便的方式实现IEnumerator,你只须要遵循一些规则,并实现IEnumerator由编译器自动生成。
一个迭代器块具有以下特征:
那么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)); } }
在上面的例子咱们也能看出,和普通方法同样,协程方法也能够带参数
你甚至能够经过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后面能够有的表达式
null,0,1,...... 暂缓一帧,下一帧继续执行
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) 将不会知足
协程就是:你能够写一段顺序代码,而后标明哪里须要暂停,而后在指定在下一帧或者任意间后,系统会继续执行这段代码
固然,协程不是真多线程,而是在一个线程中实现的
经过协程咱们能够方便的作出一个计时器,甚至利用协程控制游戏物体平滑运动
若是你刚接触协程,我但愿这篇博客能帮助你了解它们是如何工做的,以及如何来使用它们
迭代器是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; } ///实现END public 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(),下次再用同一个迭代器时候不能正确工做
因此这三个方法如何才是正确的实现,彻底要根据由上层的调用者约定来写
C#使用foreach语句取代了每次手写while(StrEnum.MoveNext())进行遍历
同时新定了一个接口类来包装迭代器IEnumerator,也就是IEnumerable,定义为:
public interface IEnumerable { IEnumerator GetEnumerator(); }
能够看到IEnumerable接口很是的简单,只包含一个抽象的方法GetEnumerator(),它返回一个可用于循环访问集合的IEnumerator对象。
IEnumerable的做用仅仅是须要派生类写一个返回指定迭代器的实现方法,也就是说IEnumerable仅仅是IEnumerator的一个包装而已。
那么返回的IEnumerator对象呢?它是一个真正的集合访问器,没有它,就不能使用foreach语句遍历集合或数组,由于只有IEnumerator对象才能访问集合中的项,才能进行集合的循环遍历。
那么咱们回到foreach
就像上面提到的,foreach须要的是一个定义了IEnumerator GetEnumerator()方法的对象,固然若是他是派生自IEnumerable对象那就更好了。
咱们继续写上文的StringPrintEnumerator类
这里新定义他的IEnumerable派生类MyEnumerable