分享

Unity3D中协程Coroutine&yield

 3dC 2016-07-20

       百度百科中,协程相关概念:与子例程(执行过程没有返回值)一样,协程(coroutine)也是一种程序组件,更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自 Simula 和 Modula-2 语言,但也有其他语言支持。协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。 协程最初在1963年被提出。协程不是进程或线程,一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。


协程

       当我们调用一个函数的时候,必须在返回之前完成运行。这样的话,游戏中单独一帧的更新内必须完成这个函数的所有步骤。这样的话函数调用就不能应用于这种情况:在一段时间内实现一段动画或一系列事件。
举个小例子:考虑这种任务——逐渐降低对象的alpha值,直到对象完全看不见为止。

  1. <span style="font-size:18px;">void Fade() {  
  2.     for (float f = 1f; f >= 0; f -= 0.1f) {  
  3.         Color c = renderer.material.color;  
  4.         c.a = f;  
  5.         renderer.material.color = c;  
  6.     }  
  7. }</span>  

        Fade函数不会产生我们期望的效果,它会在一帧更新中完成对象的alpha值的逐渐降低至0,中间结果值我们看不到,只能看到对象立马消失了。而我们预期的效果是,alpha值每减少一点就呈现出alpha对该帧渲染的效果,因而需要很多帧来呈现这个循序渐进变化的过程。当然啦,如果在Update函数中添加相应的代码使得一帧接一帧地来处理这种情况是可能的,但是使用协程来处理这种情况会更加方便。


       协程就像一个函数,它可以暂停执行,将控制权转交给Unity,当下一帧更新又被调用时又会从上次暂停的地方接着继续执行。
C#中,协程的声明是这个样子的: 

  1. IEnumerator Fade() {  
  2.     for (float f = 1f; f >= 0; f -= 0.1f) {  
  3.         Color c = renderer.material.color;  
  4.         c.a = f;  
  5.         renderer.material.color = c;  
  6.         yield return null;  
  7.     }  
  8. }  
       可以看出,协程本质上就是一个有着IEnumerator返回值的函数,并且函数体中包含yield返回语句。yield返回语句就是该帧暂停下一帧恢复的点。而且该函数中的f变量的值不会因为每次调用而重置,它都会保存上一次暂停时的值,协程中所有的参数和变量都会完好保存下来,感觉有点像static变量了。为了让协程运行起来,我们需要使用StartCoroutine函数来启动它: 
  1. void Update() {  
  2.     if (Input.GetKeyDown("f")) {  
  3.         StartCoroutine("Fade");  
  4.     }  
  5. }  
在UnityScript中,很简单,包含yield语句即为协程,返回类型IEnumerator不需要显示声明:
[javascript] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. function Fade() {  
  2.     for (var f = 1.0; f >= 0; f -= 0.1) {  
  3.         var c = renderer.material.color;  
  4.         c.a = f;  
  5.         renderer.material.color = c;  
  6.         yield;  
  7.     }  
  8. }  
在UnityScript中,协程就像个正常的函数一样被调用
[javascript] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. function Update() {  
  2.     if (Input.GetKeyDown("f")) {  
  3.         Fade();  
  4.     }  
  5. }  

       默认情况下,协程都是根据帧的切换来暂停恢复的,该帧暂停,下一帧接着暂停的地方继续执行。但是我们也可以通过指定暂停时间来实现相同的效果,使用WaitForSeconds函数;另外还可以使用WaitForFixedUpdate函数实现以固定帧率实现暂停恢复协程。
  1. IEnumerator Fade() {  
  2.     for (float f = 1f; f >= 0; f -= 0.1f) {  
  3.         Color c = renderer.material.color;  
  4.         c.a = f;  
  5.         renderer.material.color = c;  
  6.         yield return new WaitForSeconds(.1f);  
  7.     }  
  8. }  
在UnityScript中
[javascript] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. function Fade() {  
  2.     for (var f = 1.0; f >= 0; f -= 0.1) {  
  3.         var c = renderer.material.color;  
  4.         c.a = f;  
  5.         renderer.material.color = c;  
  6.         yield WaitForSeconds(0.1);  
  7.     }  
  8. }  
        对于将一个效果持续一段时间这是很有用的一种方式,而且它也是一种有用的性能优化手段。例如,游戏中有许多任务需要定期执行,最显而易见的方法就是让Update函数去做。然而,Update函数会每秒执行好多次,很可能任务并不需要这么频繁地去执行,这样协程就起到作用了,任务放到协程中,可以根据设定的间隔时间去执行相应的任务,避免了每帧都去执行的浪费。

举个类似例子,警报器警告玩家是否敌人在附近:

  1. function ProximityCheck() {  
  2.     for (int i = 0; i < enemies.Length; i++) {  
  3.         if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {  
  4.                 return true;  
  5.         }  
  6.     }  
  7.     return false;  
  8. }  

假如有很多敌人,那么每帧都调用该函数会带来很大的开销。假如我们利用协程每0.1s调用一次,这将会极大降低check的次数,而游戏的可玩性并没有差异:

  1. <pre name="code" class="csharp">IEnumerator DoCheck() {  
  2. for(;;) {  
  3. ProximityCheck;  
  4. yield return new WaitForSeconds(.1f);  
  5. }  
  6. }  


MonoBehaviour中协程相关函数:

  • StartCoroutine:启动协程
            C#中Coroutine StartCoroutine(IEnumerator routine);

                     Coroutine StartCoroutine(string methodName, object value = null);
            UnityScript中StartCoroutine(routine: IEnumerator): Coroutine;

                                 StartCoroutine(methodName: string, value: object = null): Coroutine;

            使用StartCoroutine(string methodName)和StartCoroutine(IEnumerator routine)都可以开启一个线程。区别在于使用字符串作为参数可以开启线程并在线程结束前终止线程,相反使用IEnumerator 作为参数只能等待线程的结束而不能随时终止(除非使用StopAllCoroutines()方法);另外使用字符串作为参数时,开启线程时最多只能传递一个参数,并且性能消耗会更大一点,而使用IEnumerator 作为参数则没有这个限制。

  • StopAllCoroutine:终止该MonoBehaviour上的所有协程
            C#中void StopAllCoroutines();
            UnityScript中StopAllCoroutines(): void;
  • StopCoroutine:终止该MonoBehaviour上名字为methodName的协程
             C#中void StopCoroutine(string methodName);
             UnityScript中StopCoroutine(methodName: string): void;

      另外终止协同程序的方法:将协同程序所在GameObject的active属性设置为false,当再次设置active为ture时,协同程序并不会再开启;如果将协同程序所在脚本的enabled设置为false则不会生效。这是因为协同程序被开启后作为一个线程在运行,而MonoBehaviour也是一个线程,他们成为互不干扰的模块,除非代码中用调用,他们共同作用于同一个对象,只有当对象不可见才能同时终止这两个线程。

        (协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受GameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。)


协程VS线程VS顺序执行

       协程与线程不同:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。协同有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。

       协程与顺序执行不同:关键在于yield。如果顺序执行的时候进行耗费cpu时间或者一直等待某个资源的时候,程序将卡在这个地方不能前进。而协同程序可以使等待资源的线程让出资源,进行下一个协同程序的操作,yield可以在执行出错的时候挂起,下次恢复的时候再进行操作。


注意:协同程序的参数不能指定ref、out参数;Update函数与FixedUpdate函数中不能使用yield语句,但可以在它们中使用StartCoroutine函数来调用一个函数;协程最大的用处是可以实现将一段程序延迟执行或将任务的各个部分分布在一个时间段内连续执行(需要好多帧执行一次的情况);Unity在每一帧都会去处理对象上的协程,主要在渲染后去处理协程(检查协程的条件是否满足)。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多