尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com

记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield return ,就可以通过StartCoroutine调用了。后来也是一直稀里糊涂地用,上网google些基本都是例子,很少能帮助深入理解Unity协程的原理的。

本文只是从Unity的角度去分析理解协程的内部运行原理,而不是从C#底层的语法实现来介绍(后续有需要再进行介绍),一共分为三部分:

线程(Thread)和协程(Coroutine)

Unity中协程的执行原理

IEnumerator & Coroutine

之前写过一篇《Unity协程(Coroutine)管理类——TaskManager工具分享》主要是介绍TaskManager实现对协程的状态控制,没有Unity后台实现的协程的原理进行深究。虽然之前自己对协程还算有点了解了,但是对Unity如何执行协程的还是一片空白,在UnityGems.com上看到两篇讲解Coroutine,如数家珍,当我看到Advanced Coroutine后面的Hijack类时,顿时觉得十分精巧,眼前一亮,遂动了写文分享之。

线程(Thread)和协程(Coroutine)

D.S.Qiu觉得使用协程的作用一共有两点:1)延时(等待)一段时间执行代码;2)等某个操作完成之后再执行后面的代码。总结起来就是一句话:控制代码在特定的时机执行。

很多初学者,都会下意识地觉得协程是异步执行的,都会觉得协程是C# 线程的替代品,是Unity不使用线程的解决方案。

所以首先,请你牢记:协程不是线程,也不是异步执行的。协程和 MonoBehaviour 的 Update函数一样也是在MainThread中执行的。使用协程你不用考虑同步和锁的问题。

Unity中协程的执行原理

UnityGems.com给出了协程的定义:

A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.

即协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码。

Unity在每一帧(Frame)都会去处理对象上的协程。Unity主要是在Update后去处理协程(检查协程的条件是否满足),但也有写特例:

从上图的剖析就明白,协程跟Update()其实一样的,都是Unity每帧对会去处理的函数(如果有的话)。如果MonoBehaviour 是处于激活(active)状态的而且yield的条件满足,就会协程方法的后面代码。还可以发现:如果在一个对象的前期调用协程,协程会立即运行到第一个 yield return 语句处,如果是 yield return null ,就会在同一帧再次被唤醒。如果没有考虑这个细节就会出现一些奇怪的问题『1』。

『1』注 图和结论都是从UnityGems.com 上得来的,经过下面的验证发现与实际不符,D.S.Qiu用的是Unity 4.3.4f1 进行测试的。经过测试验证,协程至少是每帧的LateUpdate()后去运行。

下面使用 yield return new WaitForSeconds(1f); 在Start,Update 和 LateUpdate 中分别进行测试:

  1. using UnityEngine;
  2. using System.Collections;
  3. public class TestCoroutine : MonoBehaviour {
  4. private bool isStartCall = false;  //Makesure Update() and LateUpdate() Log only once
  5. private bool isUpdateCall = false;
  6. private bool isLateUpdateCall = false;
  7. // Use this for initialization
  8. void Start () {
  9. if (!isStartCall)
  10. {
  11. Debug.Log("Start Call Begin");
  12. StartCoroutine(StartCoutine());
  13. Debug.Log("Start Call End");
  14. isStartCall = true;
  15. }
  16. }
  17. IEnumerator StartCoutine()
  18. {
  19. Debug.Log("This is Start Coroutine Call Before");
  20. yield return new WaitForSeconds(1f);
  21. Debug.Log("This is Start Coroutine Call After");
  22. }
  23. // Update is called once per frame
  24. void Update () {
  25. if (!isUpdateCall)
  26. {
  27. Debug.Log("Update Call Begin");
  28. StartCoroutine(UpdateCoutine());
  29. Debug.Log("Update Call End");
  30. isUpdateCall = true;
  31. }
  32. }
  33. IEnumerator UpdateCoutine()
  34. {
  35. Debug.Log("This is Update Coroutine Call Before");
  36. yield return new WaitForSeconds(1f);
  37. Debug.Log("This is Update Coroutine Call After");
  38. }
  39. void LateUpdate()
  40. {
  41. if (!isLateUpdateCall)
  42. {
  43. Debug.Log("LateUpdate Call Begin");
  44. StartCoroutine(LateCoutine());
  45. Debug.Log("LateUpdate Call End");
  46. isLateUpdateCall = true;
  47. }
  48. }
  49. IEnumerator LateCoutine()
  50. {
  51. Debug.Log("This is Late Coroutine Call Before");
  52. yield return new WaitForSeconds(1f);
  53. Debug.Log("This is Late Coroutine Call After");
  54. }
  55. }

得到日志输入结果如下:

然后将yield return new WaitForSeconds(1f);改为 yield return null; 发现日志输入结果和上面是一样的,没有出现上面说的情况:

  1. using UnityEngine;
  2. using System.Collections;
  3. public class TestCoroutine : MonoBehaviour {
  4. private bool isStartCall = false;  //Makesure Update() and LateUpdate() Log only once
  5. private bool isUpdateCall = false;
  6. private bool isLateUpdateCall = false;
  7. // Use this for initialization
  8. void Start () {
  9. if (!isStartCall)
  10. {
  11. Debug.Log("Start Call Begin");
  12. StartCoroutine(StartCoutine());
  13. Debug.Log("Start Call End");
  14. isStartCall = true;
  15. }
  16. }
  17. IEnumerator StartCoutine()
  18. {
  19. Debug.Log("This is Start Coroutine Call Before");
  20. yield return null;
  21. Debug.Log("This is Start Coroutine Call After");
  22. }
  23. // Update is called once per frame
  24. void Update () {
  25. if (!isUpdateCall)
  26. {
  27. Debug.Log("Update Call Begin");
  28. StartCoroutine(UpdateCoutine());
  29. Debug.Log("Update Call End");
  30. isUpdateCall = true;
  31. }
  32. }
  33. IEnumerator UpdateCoutine()
  34. {
  35. Debug.Log("This is Update Coroutine Call Before");
  36. yield return null;
  37. Debug.Log("This is Update Coroutine Call After");
  38. }
  39. void LateUpdate()
  40. {
  41. if (!isLateUpdateCall)
  42. {
  43. Debug.Log("LateUpdate Call Begin");
  44. StartCoroutine(LateCoutine());
  45. Debug.Log("LateUpdate Call End");
  46. isLateUpdateCall = true;
  47. }
  48. }
  49. IEnumerator LateCoutine()
  50. {
  51. Debug.Log("This is Late Coroutine Call Before");
  52. yield return null;
  53. Debug.Log("This is Late Coroutine Call After");
  54. }
  55. }

『今天意外发现Monobehaviour的函数执行顺序图,发现协程的运行确实是在LateUpdate之后,下面附上:』
                                                                       增补于:03/12/2014 22:14

前面在介绍TaskManager工具时,说到MonoBehaviour 没有针对特定的协程提供Stop方法,其实不然,可以通过MonoBehaviour enabled = false 或者 gameObject.active = false 就可以停止协程的执行『2』。

经过验证,『2』的结论也是错误的,正确的结论是,MonoBehaviour.enabled = false 协程会照常运行,但 gameObject.SetActive(false) 后协程却全部停止,即使在Inspector把  gameObject 激活还是没有继续执行:

  1. using UnityEngine;
  2. using System.Collections;
  3. public class TestCoroutine : MonoBehaviour {
  4. private bool isStartCall = false;  //Makesure Update() and LateUpdate() Log only once
  5. private bool isUpdateCall = false;
  6. private bool isLateUpdateCall = false;
  7. // Use this for initialization
  8. void Start () {
  9. if (!isStartCall)
  10. {
  11. Debug.Log("Start Call Begin");
  12. StartCoroutine(StartCoutine());
  13. Debug.Log("Start Call End");
  14. isStartCall = true;
  15. }
  16. }
  17. IEnumerator StartCoutine()
  18. {
  19. Debug.Log("This is Start Coroutine Call Before");
  20. yield return new WaitForSeconds(1f);
  21. Debug.Log("This is Start Coroutine Call After");
  22. }
  23. // Update is called once per frame
  24. void Update () {
  25. if (!isUpdateCall)
  26. {
  27. Debug.Log("Update Call Begin");
  28. StartCoroutine(UpdateCoutine());
  29. Debug.Log("Update Call End");
  30. isUpdateCall = true;
  31. this.enabled = false;
  32. //this.gameObject.SetActive(false);
  33. }
  34. }
  35. IEnumerator UpdateCoutine()
  36. {
  37. Debug.Log("This is Update Coroutine Call Before");
  38. yield return new WaitForSeconds(1f);
  39. Debug.Log("This is Update Coroutine Call After");
  40. yield return new WaitForSeconds(1f);
  41. Debug.Log("This is Update Coroutine Call Second");
  42. }
  43. void LateUpdate()
  44. {
  45. if (!isLateUpdateCall)
  46. {
  47. Debug.Log("LateUpdate Call Begin");
  48. StartCoroutine(LateCoutine());
  49. Debug.Log("LateUpdate Call End");
  50. isLateUpdateCall = true;
  51. }
  52. }
  53. IEnumerator LateCoutine()
  54. {
  55. Debug.Log("This is Late Coroutine Call Before");
  56. yield return null;
  57. Debug.Log("This is Late Coroutine Call After");
  58. }
  59. }

先在Update中调用 this.enabled = false; 得到的结果:

然后把 this.enabled = false; 注释掉,换成 this.gameObject.SetActive(false); 得到的结果如下:

       整理得到:通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。

yield 后面可以有的表达式:

a) null - the coroutine executes the next time that it is eligible

b) WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete

c) WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated

d) WaitForSeconds - causes the coroutine not to execute for a given game time period

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

f) 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) 将不会满足。

IEnumerator & Coroutine

协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法 Current 和 MoveNext() ,前面介绍的 TaskManager 就是利用者两个方法对协程进行了管理,只有当MoveNext()返回 true时才可以访问 Current,否则会报错。迭代器方法运行到 yield return 语句时,会返回一个expression表达式并保留当前在代码中的位置。 当下次调用迭代器函数时执行从该位置重新启动。

Unity在每帧做的工作就是:调用 协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。

Hijack

这里在介绍一个协程的交叉调用类 Hijack(参见附件):

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using System.Collections;
  6. [RequireComponent(typeof(GUIText))]
  7. public class Hijack : MonoBehaviour {
  8. //This will hold the counting up coroutine
  9. IEnumerator _countUp;
  10. //This will hold the counting down coroutine
  11. IEnumerator _countDown;
  12. //This is the coroutine we are currently
  13. //hijacking
  14. IEnumerator _current;
  15. //A value that will be updated by the coroutine
  16. //that is currently running
  17. int value = 0;
  18. void Start()
  19. {
  20. //Create our count up coroutine
  21. _countUp = CountUp();
  22. //Create our count down coroutine
  23. _countDown = CountDown();
  24. //Start our own coroutine for the hijack
  25. StartCoroutine(DoHijack());
  26. }
  27. void Update()
  28. {
  29. //Show the current value on the screen
  30. guiText.text = value.ToString();
  31. }
  32. void OnGUI()
  33. {
  34. //Switch between the different functions
  35. if(GUILayout.Button("Switch functions"))
  36. {
  37. if(_current == _countUp)
  38. _current = _countDown;
  39. else
  40. _current = _countUp;
  41. }
  42. }
  43. IEnumerator DoHijack()
  44. {
  45. while(true)
  46. {
  47. //Check if we have a current coroutine and MoveNext on it if we do
  48. if(_current != null && _current.MoveNext())
  49. {
  50. //Return whatever the coroutine yielded, so we will yield the
  51. //same thing
  52. yield return _current.Current;
  53. }
  54. else
  55. //Otherwise wait for the next frame
  56. yield return null;
  57. }
  58. }
  59. IEnumerator CountUp()
  60. {
  61. //We have a local increment so the routines
  62. //get independently faster depending on how
  63. //long they have been active
  64. float increment = 0;
  65. while(true)
  66. {
  67. //Exit if the Q button is pressed
  68. if(Input.GetKey(KeyCode.Q))
  69. break;
  70. increment+=Time.deltaTime;
  71. value += Mathf.RoundToInt(increment);
  72. yield return null;
  73. }
  74. }
  75. IEnumerator CountDown()
  76. {
  77. float increment = 0f;
  78. while(true)
  79. {
  80. if(Input.GetKey(KeyCode.Q))
  81. break;
  82. increment+=Time.deltaTime;
  83. value -= Mathf.RoundToInt(increment);
  84. //This coroutine returns a yield instruction
  85. yield return new WaitForSeconds(0.1f);
  86. }
  87. }
  88. }

上面的代码实现是两个协程交替调用,对有这种需求来说实在太精妙了。

小结:

今天仔细看了下UnityGems.com 有关Coroutine的两篇文章,虽然第一篇(参考①)现在验证的结果有很多错误,但对于理解协程还是不错的,尤其是当我发现Hijack这个脚本时,就迫不及待分享给大家。

本来没觉得会有UnityGems.com上的文章会有错误的,无意测试了发现还是有很大的出入,当然这也不是说原来作者没有经过验证就妄加揣测,D.S.Qiu觉得很有可能是Unity内部的实现机制改变了,这种东西完全可以改动,Unity虽然开发了很多年了,但是其实在实际开发中还是有很多坑,越发觉得Unity的无力,虽说容易上手,但是填坑的功夫也是必不可少的。

看来很多结论还是要通过自己的验证才行,贸然复制粘贴很难出真知,切记!

如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

转载请在文首注明出处:http://dsqiu.iteye.com/blog/2029701

unity---Courtine 协程的更多相关文章

  1. [Unity菜鸟] 协程Coroutine

    1.协程,即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态. unity中StartCoroutine()就是协程,协程实际上是在一个线程中, ...

  2. C#神器 委托 + Unity神器 协程

    作为源生的C#程序员,可能已经非常了解委托(delegate).行动(Action)以及C#的事件了,不过作为一个半道转C#的程序员而言,这些东西可能还是有些陌生的,虽然委托并非是C#独创,亦非是首创 ...

  3. Unity使用协程技术制作倒计时器

    先上效果图 图片资源来自http://www.51miz.com/ 1.素材准备 在http://www.51miz.com/搜索png格式的数字图片,用Unity自带的图集制作工具,进行分割.Con ...

  4. 关于Unity中协程、多线程、线程锁、www网络类的使用

    协程 我们要下载一张图片,加载一个资源,这个时候一定不是一下子就加载好的,或者说我们不一定要等它下载好了才进行其他操作,如果那样的话我就就卡在了下载图片那个地方,傻住了.我们希望我们只要一启动加载的命 ...

  5. Unity在协程(Coroutines)内开启线程(Threading )

    孙广东  2017.6.13 http://blog.csdn.NET/u010019717 为什么要在协程中开启线程, 因为很多时候我们是需要线程执行完成后回到主线程的.然后主线程在继续执行后续的操 ...

  6. 关于Unity的协程

    协程 认识协程 //协程不是多线程:是一段在主程序之外执行的代码 //协程不受生命周影响 //作用:能够口直代码在特定的时间执行. //1,延时操作 //2,等待某代码执行结束之后执行 /* 特点:1 ...

  7. 【Unity】协程Coroutine及Yield常见用法

    最近学习协程Coroutine,参考了别人的文章和视频教程,感觉协程用法还是相当灵活巧妙的,在此简单总结,方便自己以后回顾.Yield关键字的语意可以理解为“暂停”. 首先是yield return的 ...

  8. Unity在协程内部停止协程自身后代码执行问题

    当在协程内部停止自身后,后面的代码块还会继续执行,直到遇到yield语句才会终止. 经测试:停止协程,意味着就是停止yield,所以在停止协程后,yield之后的语句也就不会执行了. 代码如下: us ...

  9. 发现一个小坑的地方,unity的协程,想要停止,必须以字符串启动

    今天想要停止一个协成,发现调用 StopCoroutine(ShowDebug()); 竟然不管用,后来看了文档才知道,原来想要停止协成,必须用字符启动协程 StartCoroutine(" ...

  10. unity 之协程返回值

    yield return null; // 下一帧再执行后续代码yield return 6;//(任意数字) 下一帧再执行后续代码yield break; //直接结束该协程的后续操作yield r ...

随机推荐

  1. [Windows Azure] About Affinity Groups for Virtual Network

    Affinity groups are the way to group the services in your Windows Azure subscription that need to wo ...

  2. debian8.5安装sublime text3

    在官网www.sublimetext.com下载安装包 我这里用的是Ubuntu 64 bit版. 下载后使用su命令切换到root账户. 执行安装命令 dpkg -i sublime-text*.d ...

  3. 【Delphi】SPComm注意事项

    Spcomm属性设置 SPCOMM 控件的属性设置很关键的,特别是使用事件驱动时接收大数据块时尤为明显,如果设置不当,接收到的数据可能严重出错. ReadIntervalTimeout:=100 SP ...

  4. vue 的事件冒泡

    一.事件冒泡 方法一.使用event.cancelBubble = true来组织冒泡 <div @click="show2()"> <input type=&q ...

  5. 循环遍历li并获取其自定义属性的方法

    var lists = $('.list'); for (var j = 0; j < lists.length; j++) { var index = $('.list').eq(j).att ...

  6. Docker 入门(Mac环境)- part 4 swarms

    part-4 Swarms 简介 这一节主要是介绍一下如何在集群模式下部署docker应用:集群的概念很好理解了,多台机器共同完成一项任务:和Hadoop那些集群一样,docker也相当于有一个管理机 ...

  7. 关于fork()函数的精辟分析

    http://blog.csdn.net/yanh_lzu/article/details/2311644 第一贴:cu上关于fork()函数的精辟分析   声明:在别人的博客上看到这篇文章,真的很精 ...

  8. 关于RAID_1+0和RAID_0+1的比较

    RAID的概念就不多说了,说说 RAID 0 和 RAID 1 . RAID 0 是条带存储,叠加所有硬盘容量,因此不具有容错性,原理如下图所示: RAID 1 使用非常原始的方式(复制一份.镜像)进 ...

  9. Android开发(一)——全屏或者取消标题栏

    先介绍去掉标题栏的方法: 第一种:也一般入门的时候经常使用的一种方法 requestWindowFeature(Window.FEATURE_NO_TITLE);//去掉标题栏 注意这句一定要写在se ...

  10. JS自动关闭授权弹窗,并刷新父页面

    echo "<script>window.opener.location.href='index.php'; window.close();</script>&quo ...