当前位置:   article > 正文

C# 学习笔记:迭代器:协程_yield return new waitforsecond

yield return new waitforsecond

协程是Unity一个刚开始看起来很诡异但是实际用起来很方便的一种用法。

在游戏运行的时候,总是需要读秒,我之前写过一个游戏,就是炮塔看见人物就等三秒然后开枪,最开始用Time类去读秒,效果勉强能用,但是炮塔多了之后整个脚本逻辑看起来痛苦不已。但是协程就非常好的实现了读秒、读帧、读逻辑的功能。

我们在脚本里实现一遍协程是很简单的,但是协程背后是不容易整明白的。协程的语法对于曾经的我来说特别晦涩。什么IEnumerator、IEnumerable这种东西又不会读又不会写,还不知道干嘛的,再加上yield return,整个人虽然会用,提起协程来也是蒙圈的。

我上一篇文章就专程讲了迭代器,IEnumerator和IEnumerable都是在枚举接口,它们在C# 2.0以后支持了迭代器的功能,即yield return的逻辑休克。这一篇文章我们来讲讲协程在Unity中对迭代器的实现。

协程

协程在Unity里的C#中,主要就是暂停一段逻辑等待某一个功能完成后再执行剩下的逻辑,由于我们Unity脚本中调用得最多的是Update函数,这个函数是没有任何理由条件来暂停它的。所以Unity引入了协程的概念。

1.协程由StartCoroutine()来开启协程。使用StopCoroutine()来关闭协程。

2.StartCoroutine和StopCoroutine方法参数类型为IEnumerator。但也可以支持字符串类型。以下是StartCoroutine的重载版本:

3.StartCoroutine是没有返回值的,当碰到迭代器里的yield return后,它会等待我们的返回类型完成,自动为我们执行MoveNext方法,来让协程可以在yield return后执行下去。

StartCoroutine虽然参数只能是IEnumerator,但是我们可以创建IEnumerable的迭代器给它,两种迭代器完成同样的功能,但是IEnumerable的迭代器在给协程的时候需要一些转换的。我把两种迭代器放在一起看看用法,例子如下:

  1. public class CSharpTest : MonoBehaviour
  2. {
  3. public int value = 11;
  4. void Start()
  5. {
  6. IEnumerable enumerable = Countable(value);
  7. IEnumerator enumerator = enumerable.GetEnumerator();
  8. StartCoroutine(enumerator);
  9. StartCoroutine(Countor(value));
  10. }
  11. IEnumerable Countable(int value)
  12. {
  13. yield return new WaitForSeconds(3f);
  14. Debug.Log("IEnumerable" + value);
  15. }
  16. IEnumerator Countor(int value)
  17. {
  18. yield return new WaitForSeconds(3f);
  19. Debug.Log("IEnumerator" + value);
  20. }
  21. }

两个的效果是一模一样的的 我们执行看看:

两个都是同样可以执行的。

迭代器实现协程功能

协程背后就是迭代器,但是我们要自己使用迭代器实现协程的功能却是不容易,整了老半天,我自己整了个和官方功能相似的延时:

  1. public class CSharpTest : MonoBehaviour
  2. {
  3. private IEnumerator enumerator;
  4. void Start()
  5. {
  6. StartCoroutine(CountTime());
  7. enumerator = HandEnum();
  8. enumerator.MoveNext();
  9. }
  10. void FixedUpdate()
  11. {
  12. if(enumerator.Current is myWaitForSecond)
  13. {
  14. var Current = enumerator.Current as myWaitForSecond;
  15. if(Time.time>Current.waitTime)
  16. {
  17. enumerator.MoveNext();
  18. }
  19. }
  20. }
  21. IEnumerator CountTime()
  22. {
  23. Debug.Log("官方协程:开始计时!");
  24. for (int i = 1; i <= 12;i++)
  25. {
  26. Debug.Log("官方协程:过了" + i + "秒");
  27. yield return new WaitForSeconds(1f);
  28. }
  29. }
  30. IEnumerator HandEnum()
  31. {
  32. Debug.Log("手动开启协程,等待三秒");
  33. yield return new myWaitForSecond(3f);
  34. Debug.Log("手动协程三秒等待完毕!");
  35. Debug.Log("手动又让协程等待五秒");
  36. yield return new myWaitForSecond(5f);
  37. Debug.Log("手动协程五秒等待完毕");
  38. }
  39. }
  40. class myWaitForSecond
  41. {
  42. public float waitTime;
  43. public myWaitForSecond(float t)
  44. {
  45. waitTime = Time.time + t;
  46. }
  47. }

二者效果是一样的,看看效果:

我们可以看到,在协程读秒的时候,我们手动协程一样也完成了它的工作,在过了三秒和五秒的时候准时输出了。

但是,我们定义自己的myWaitForSecond的时候,里面并没有太多的内容,除了一个字段和一个构造函数外别无他物,我们在官方的waitForSecond里面也可以看到,里面实际上非常简单:

那么实际上的那些时延操作是谁来完成的呢,留给我们想象空间的只剩下一个,就是一开始的StartCoroutine。

一般而言,StartCoroutine就是简单的对某个IEnumerator 进行MoveNext()操作,但如果他发现IEnumerator其实是一个WaitForSeconds类型的话,那么他就会进行特殊等待,一直等到WaitForSeconds延时结束了,才进行正常的MoveNext调用,而至于WWW或者WaitForFixedUpdate等类型,StartCoroutine也是同样的特殊处理。

那么,我们没有使用StartCoroutine的IEnumerator就只能自己写逻辑决定什么情况来MoveNext了。

协程注意事项

1.

Unity在LateUpdate结束后才会检测协程的yield return是否满足然后再确定是否执行MoveNext。

对于协程来说,按理说我们设定了某个条件,那么协程会在该条件完成后进行MoveNext。

但是我们游戏是一帧一帧来算的。在协程中,会在LateUpdate后对条件是否满足进行判断,我们可以写个例子验证一下:

  1. public class CSharpTest : MonoBehaviour
  2. {
  3. private bool isUpdateCor = false;
  4. private bool isLateUpdateCor = false;
  5. void Start()
  6. {
  7. StartCoroutine(Startfunction());
  8. }
  9. IEnumerator Startfunction()
  10. {
  11. Debug.Log("Start协程开始");
  12. yield return null;
  13. Debug.Log("Start协程结束");
  14. }
  15. void Update()
  16. {
  17. if(!isUpdateCor)
  18. {
  19. StartCoroutine(UpdateFunction());
  20. isUpdateCor = true;
  21. }
  22. }
  23. IEnumerator UpdateFunction()
  24. {
  25. Debug.Log("Update协程开始");
  26. yield return null;
  27. Debug.Log("Update协程结束");
  28. }
  29. void LateUpdate()
  30. {
  31. if(!isLateUpdateCor)
  32. {
  33. StartCoroutine(LateUpdateFunction());
  34. isLateUpdateCor = true;
  35. }
  36. }
  37. IEnumerator LateUpdateFunction()
  38. {
  39. Debug.Log("LateUpdate协程开始");
  40. yield return null;
  41. Debug.Log("LateUpdate协程结束");
  42. }
  43. }

按照设想,协程一旦在yield return后的条件满足就MoveNext,而且我们这里是返回null,按理说应该执行完yield return后立马输出后面那个debug语句,但是事实上:

如上图,直到LateUpdate函数结束后,我们的StarFunction的yield return后的语句才开始返回它的声明。

这个例子表明:Unity在LateUpdate函数结束后才会检测协程的yield return是否满足然后再确定是否执行MoveNext。

以下这张图效果很好:

2.

关于StopCoroutine的大坑

StopCoroutine是协程里的一个大坑,我们通常以为只要StopCoroutine里的参数表达形式和StartCoroutine对应就好了。。。

但不是这样!!!!

关闭一个协程有三个方法,这三个方法必须一一对应

例如我们有个迭代器CountTime,那么协程必须是这样声明才能关闭:

1.创建IEnumerator实例开启协程,StopCoroutine里的参数必须是IEnumerator实例。

  1. IEnumerator x = CountTime();
  2. StartCoroutine(x);
  3. ......//此处省略逻辑
  4. StopCoroutine(x);

2.创建Coroutine实例,StopCoroutine里的参数必须是Coroutine实例。

  1. Coroutine x;
  2. x = StartCoroutine(CountTime());
  3. ......//此处省略逻辑
  4. StopCoroutine(x);

3.使用字符串开关协程。

  1. StartCoroutine("CountTime");
  2. ......//此处省略逻辑
  3. StopCoroutine("CountTime");

这个是真的坑,坑炸了。

3.

除了StopCoroutine以外,可以停止协程脚本所在的游戏对象来停止协程(gameObject.SetActive(false)),

只停止协程所在脚本不能停止协程。(MonoBehaviour.enabled = false

有:

使用MonoBehaviour.enabled = false 协程会照常运行,但 gameObject.SetActive(false) 后协程却全部停止,即使在Inspector把gameObject 激活还是没有继续执行并且当我们使用gameObject.SetActive(false) 后,协程不会立即暂停,而是会等待协程执行到下一次yield return后才会关闭协程。

我们看个例子:

  1. public class CSharpTest : MonoBehaviour
  2. {
  3. private int i;
  4. void Start()
  5. {
  6. StartCoroutine(CountTime());
  7. }
  8. IEnumerator CountTime()
  9. {
  10. Debug.Log("官方协程:开始计时!");
  11. for (i = 0; i <= 12;i++)
  12. {
  13. Debug.Log("这是第" + i + "次哈哈哈");
  14. yield return new WaitForSeconds(1f);
  15. }
  16. }
  17. void Update()
  18. {
  19. if(i>6)
  20. {
  21. this.gameObject.SetActive(false);
  22. }
  23. }
  24. }

输出为:

我们可以看到,我们i>6的情况是在for循环的i++后就等于7了,此时游戏对象关闭,但是还是输出了“这是第7次哈哈哈”。

协程的功能

我们在协程yield return new以后可以看到,有以下这些类可以接在后面

  • null - 立马继续执行
  • WaitForEndOfFrame - 暂停该协程直到帧上所有渲染和GUI完成之后才继续执行。(可以用于截屏功能)
  • WaitForFixedUpdate - 暂停该协程在下一个FixedUpdate步骤才继续执行。
  • WaitForSeconds - 使协程等待固定的时间段后执行。它受制于我们设定的TimeScale。
  • WaitForSecondsRealtime - 使协程等待固定的时间段后执行,它里面的时间与现实世界时间一致。
  • WWW - 暂停协程等待Web请求完成后继续执行(这个有时间细细写)
  • WaitUntil - WaitUntil是一个bool返回值委托,暂停协程直到该委托包装的bool返回值的函数返回false之后才继续执行。
  • WaitWhile - WaitWhile是一个bool返回值委托,暂停协程直到该委托包装的bool返回值的函数返回true之后才继续执行。
  • 其他的协程 - 等待其他的协程完全执行完逻辑后才继续执行。

我们写一下最后一个功能的情况:

  1. public class CSharpTest : MonoBehaviour
  2. {
  3. void Start()
  4. {
  5. StartCoroutine(Function());
  6. }
  7. IEnumerator Function()
  8. {
  9. yield return StartCoroutine(CountTime());
  10. Debug.Log("主委托等待另一个委托读秒完成");
  11. }
  12. IEnumerator CountTime()
  13. {
  14. Debug.Log("我是另一个委托,读一秒");
  15. yield return new WaitForSeconds(1f);
  16. Debug.Log("另一个委托读秒结束");
  17. }
  18. }

如图,主委托Function直到另一个委托CountTime逻辑全部执行完毕后才接着执行自己的逻辑。

 

参考文章:https://www.iteye.com/blog/dsqiu-2029701

 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号