Unity3D之协程(Coroutines & Yield )

在Unity中StartCoroutine/yield return这个模式到底是怎么应用的?

 

比如你要一个方法进行一个比较耗时的复杂运算~同时又想让脚本流畅的进行其他操作而不是卡在那里等该方法执行完毕;这个时候你就可以创建一个协同程序来调用该方法。

 

一个协同程序在执行过程中,可以在任意位置使用yield语句。yield的返回值控制何时恢复协同程序向下执行

 

源文档 <http://zhidao.baidu.com/link?url=IrW8cLCTNFWJAH6DYYuaIGm1v8kouP_cWnfOO8n1g0exLGPhM2QSI6LJm7Y051SK16UpiogoZg1HDfrVEpC3JKAaHUL15q6hrkKgpbn7uUG

 

 

 

一。什么是协同程序

       协同程序,即在主程序运行时同时开启另一段逻辑处理,来协同当前程序的执行。换句话说,开启协同程序就是开启一个线程。

 

二。协同程序的开启与终止

       在Unity3D中,使用MonoBehaviour.StartCoroutine方法即可开启一个协同程序,也就是说该方法必须在MonoBehaviour或继承于MonoBehaviour的类中调用。

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

        在Unity3D中,使用StopCoroutine(string methodName)来终止一个协同程序,使用StopAllCoroutines()来终止所有可以终止的协同程序,但这两个方法都只能终止该MonoBehaviour中的协同程序。

        还有一种方法可以终止协同程序,即将协同程序所在gameobject的active属性设置为false,当再次设置active为ture时,协同程序并不会再开启;如是将协同程序所在脚本的enabled设置为false则不会生效。这是因为协同程序被开启后作为一个线程在运行,而MonoBehaviour也是一个线程,他们成为互不干扰的模块,除非代码中用调用,他们共同作用于同一个对象,只有当对象不可见才能同时终止这两个线程。然而,为了管理我们额外开启的线程,Unity3D将协同程序的调用放在了MonoBehaviour中,这样我们在编程时就可以方便的调用指定脚本中的协同程序,而不是无法去管理,特别是对于只根据方法名来判断线程的方式在多人开发中很容易出错,这样的设计保证了对象、脚本的条理化管理,并防止了重名。

 

三。协同程序的输入、输出类型

        协同程序的返回类型为Coroutine类型。在Unity3D中,Coroutine类继承于YieldInstruction,所以,协同程序的返回类型只能为null、等待的帧数(frame)以及等待的时间。

        协同程序的参数不能指定ref、out参数。但是,我们在使用WWW类时会经常使用到协同程序,由于在协同程序中不能传递参数地址(引用),也不能输出对象,这使得每下载一个WWW对象都得重写一个协同程序,解决这个问题的方法是建立一个基于WWW的类,并实现一个下载方法。如下:

 

 1 using UnityEngine;
 2 using System.Collections;
 3 public class WWWObject : MonoBehaviour 
 4 {
 5  public WWW www;
 6  
 7  public WWWObject(string url)
 8  {
 9   if(GameVar.wwwCache)
10    www = WWW.LoadFromCacheOrDownload(url, GameVar.version);
11   else
12    www = new WWW(url);
13  }
14  
15  public IEnumerator Load()
16  {
17   Debug.Log("Start loading : " + www.url);
18   while(!www.isDone)
19   {
20    if(GameVar.gameState == GameState.Jumping || GameVar.gameState == GameState.JumpingAsync)
21     LoadScene.progress = www.progress;
22    
23    yield return 1;
24   }
25   if(www.error != null)
26    Debug.LogError("Loading error : " + www.url + "\n" + www.error);
27   else
28    Debug.Log("End loading : " + www.url);
29  }
30  
31  public IEnumerator LoadWithTip(string resourcesName)
32  {
33   Debug.Log("Start loading : " + www.url);
34   LoadScene.tipStr =  "Downloading  resources <" + resourcesName + "> . . .";
35   while(!www.isDone)
36   {
37    if(GameVar.gameState == GameState.Jumping || GameVar.gameState == GameState.JumpingAsync)
38     LoadScene.progress = www.progress;
39    
40    yield return 1;
41   }
42   if(www.error != null)
43    Debug.LogError("Loading error : " + www.url + "\n" + www.error);
44   else
45    Debug.Log("End loading : " + www.url);
46  }
47 } 

 

调用:
 1 using UnityEngine;
 2 using System.Collections;
 3 using System.Collections.Generic;
 4 public class LoadResources : MonoBehaviour 
 5 {
 6  static string url = "http://61.149.211.88/Package/test.unity3d";
 7  public static WWW www = null;
 8  IEnumerator Start()
 9  {
10   if(!GameVar.resourcesLoaded)
11   {  
12    GameVar.gameState = GameState.Jumping;
13    
14    WWWObject obj = new WWWObject(url);
15    www = obj.www;
16    yield return StartCoroutine(obj.LoadWithTip("Textures"));
17    
18    GameVar.resourcesLoaded = true;
19    GameVar.gameState = GameState.Run;
20   }
21  }
22 } 

 

 

 

 

 

另一个协同的说明

 

说到Coroutine,我们必须提到两个更远的东西。在操作系统(os)级别,有进程(process)和线程(thread)两个(仅从我们常见的讲)实际的“东西”(不说概念是因为这两个家伙的确不仅仅是概念,而是实际存在的,os的代码管理的资源)。这两个东西都是用来模拟“并行”的,写操作系统的程序员通过用一定的策略给不同的进程和线程分配CPU计算资源,来让用户“以为”几个不同的事情在“同时”进行“。在单CPU上,是os代码强制把一个进程或者线程挂起,换成另外一个来计算,所以,实际上是串行的,只是“概念上的并行”。在现在的多核的cpu上,线程可能是“真正并行的”。

Coroutine,翻译成”协程“,初始碰到的人马上就会跟上面两个概念联系起来。直接先说区别,Coroutine是编译器级的,Process和Thread是操作系统级的。Coroutine的实现,通常是对某个语言做相应的提议,然后通过后成编译器标准,然后编译器厂商来实现该机制。Process和Thread看起来也在语言层次,但是内生原理却是操作系统先有这个东西,然后通过一定的API暴露给用户使用,两者在这里有不同。Process和Thread是os通过调度算法,保存当前的上下文,然后从上次暂停的地方再次开始计算,重新开始的地方不可预期,每次CPU计算的指令数量和代码跑过的CPU时间是相关的,跑到os分配的cpu时间到达后就会被os强制挂起。Coroutine是编译器的魔术,通过插入相关的代码使得代码段能够实现分段式的执行,重新开始的地方是yield关键字指定的,一次一定会跑到一个yield对应的地方。

对于Coroutine,下面是一个实现的function,里面的片段被yield关键字分成2段:

IEnumerator YieldSomeStuff()
{
    yield "hello";
    Console.WriteLine("foo!");
    yield "world";
}

推进的代码(模拟,非实际):

IEnumerator e = YieldSomeStuff();
while(e.MoveNext())
{
    Console.WriteLine(e.Current);
}

以此来推进整个代码片段的分段执行。更详细的分析如 @邓凯的文章里提到。这里只要说明的是,对于Coroutine,是编译器帮助做了很多的事情,来让代码不是一次性的跑到底,而不是操作系统强制的挂起。代码每次跑多少,是可预期的。但是,Process和Thread,在这个层面上完全不同,这两个东西是操作系统管理的。在unity中,StartCoroutine这个方法是个推进器。StartCoroutine会发起类似上面的while循环。因为是while循环,因此,Coroutine本身其实不是“异步的”。

Coroutine在整个Unity系统的位置,下面一张图可以说明:

注:图片来自Coroutines++

Unity官方文档里也写到"Normal Coroutine在Update之后"的字眼,如下内容第一行:

Normal coroutine updates are run after the Update function returns. A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes. Different uses of Coroutines:

yield; The coroutine will continue after all Update functions have been called on the next frame.
yield WaitForSeconds(2); Continue after a specified time delay, after all Update functions have been called for the frame
yield WaitForFixedUpdate(); Continue after all FixedUpdate has been called on all scripts
yield WWW Continue after a WWW download has completed.
yield StartCoroutine(MyFunc); Chains the coroutine, and will wait for the MyFunc coroutine to complete first.

由上面的图和文字可以大致猜测,.net虚拟机在每一帧循环中,会依次进入每个编译器预定义好的入口。对于Coroutine,编译器需要产生一些代码,在每次的大循环中,Unity的Update()返回后,保证是yield后的代码被正确调用,这样就形成了我们看到的一个function能分段执行的机制。


作者:周华
链接:http://www.zhihu.com/question/23895384/answer/26066323
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

posted @ 2016-03-31 08:48  MATU  阅读(693)  评论(0)    收藏  举报