Coroutine

来源:互联网 发布:淘宝如何退货 编辑:程序博客网 时间:2024/04/30 15:26
Unity中使用Coroutine需要注意的问题:
1.使用的地方和不能使用的地方:
必须在MonoBehaviour或继承于MonoBehaviour的类中调用 yield coroutine。yield不可以在Update或者FixedUpdate里使用。
2.开启协程:
StartCoroutine(string methodName)和StartCoroutine(IEnumeratorroutine)都可以开启一个协程,
区别:使用字符串作为参数时,开启协程时最多只能传递一个参数,并且性能消耗会更大一点; 而使用IEnumerator 作为参数则没有这个限制。
3.删除协程:
1).在Unity3D中,使用StopCoroutine(stringmethodName)来终止该MonoBehaviour指定方法名的一个协同程序,使用StopAllCoroutines()来终止所有该MonoBehaviour可以终止的协同程序。
包括StartCoroutine(IEnumerator routine)的。
2).还有一种方法可以终止协同程序,即将协同程序所在gameobject的active属性设置为false,当再次设置active为ture时,协同程序并不会再开启;
如是将协同程序所在脚本的enabled设置为false则不会生效。
4.js和C#中使用区别:
在C#中要使用 yield return而不是yield。
C#中yield(中断)语句必须要在IEnumerator类型里,C#方法的返回类型为IEnumerator,返回值如(eg:yield return new WaitForSeconds(2); 或者 yield returnnull);
5.协程函数返回值和参数类型,组合的设计模式:
协同程序的返回类型为Coroutine类型。在Unity3D中,Coroutine类继承于YieldInstruction,所以,协同程序的返回类型只能为null、等待的帧数(frame)以及等待的时间。
协同程序的参数不能指定ref、out参数。但是,我们在使用WWW类时会经常使用到协同程序,由于在协同程序中不能传递参数地址(引用),也不能输出对象,
这使得每下载一个WWW对象都得重写一个协同程序,解决这个问题的方法是建立一个基于WWW的类(用组合模式来解决-其实就是不通过函数传参全局关联一个对象了),并实现一个下载方法。如下:
using UnityEngine;
using System.Collections;
public class WWWObject : MonoBehaviour
{
public WWW www;
public WWWObject(string url)
{
if(GameVar.wwwCache)
www = WWW.LoadFromCacheOrDownload(url, GameVar.version);
else
www = new WWW(url);
}
public IEnumerator Load()
{
Debug.Log("Start loading : " + www.url);
while(!www.isDone)
{
if(GameVar.gameState == GameState.Jumping || GameVar.gameState ==GameState.JumpingAsync)
LoadScene.progress = www.progress;
yield return 1;
}

if(www.error != null)
Debug.LogError("Loading error : " + www.url + "\n" +www.error);
else
Debug.Log("End loading : " + www.url);
}


public IEnumerator LoadWithTip(string resourcesName)
{
Debug.Log("Start loading : " + www.url);
LoadScene.tipStr = "Downloading  resources<" + resourcesName + "> . . .";
while(!www.isDone)
{
if(GameVar.gameState == GameState.Jumping || GameVar.gameState ==GameState.JumpingAsync)
LoadScene.progress= www.progress;
yield return 1;
}
if(www.error != null)
Debug.LogError("Loading error : " + www.url + "\n" +www.error);
else
Debug.Log("End loading : " + www.url);
}
}


//用:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class LoadResources : MonoBehaviour
{
static string url ="http://61.149.211.88/Package/test.unity3d";
public static WWW www = null;
IEnumerator Start()
{
if(!GameVar.resourcesLoaded)

GameVar.gameState = GameState.Jumping;
WWWObject obj = new WWWObject(url);
www = obj.www;
yield return StartCoroutine(obj.LoadWithTip("Textures"));
GameVar.resourcesLoaded = true;
GameVar.gameState = GameState.Run;
}
}
}


yield使用


 yield能干的事情远远不止这种简单的特定时间的延时,例如可以在下一帧继续执行这段代码(yield return null),可以在下一次执行FixedUpdate的时候继续执行这段代码(yield new WaitForFixedUpdate ();),可以让异步操作(如LoadLevelAsync)在完成以后继续执行,可以……可以让你看到头晕。


  unity3d官方对于协程的解释是:一个协同程序在执行过程中,可以在任意位置使用yield语句。yield的返回值控制何时恢复协同程序向下执行。协同程序在对象自有帧执行过程中堪称优秀。协同程序在性能上没有更多的开销。StartCoroutine函数是立刻返回的,但是yield可以延迟结果。直到协同程序执行完毕。


  如果只是认为yield用于延时,那么可以用的很顺畅;但是若看到yield还有这么多功能,目测瞬间就凌乱了,更不要说活学活用了。不过,如果从原理上进行理解,就很容易理清yield的各种功能了。


C#中的yield


复制代码
 1 public static IEnumerable<int> GenerateFibonacci()
 2 {
 3     yield return 0;
 4     yield return 1;
 5 
 6     int last0 = 0, last1 = 1, current;
 7 
 8     while (true)
 9     {
10         current = last0 + last1;
11         yield return current;
12 
13         last0 = last1;
14         last1 = current;
15     }
16 }
复制代码
  yield return的作用是在执行到这行代码之后,将控制权立即交还给外部。yield return之后的代码会在外部代码再次调用MoveNext时才会执行,直到下一个yield return——或是迭代结束。虽然上面的代码看似有个死循环,但事实上在循环内部我们始终会把控制权交还给外部,这就由外部来决定何时中止这次迭代。有了yield之后,我们便可以利用“死循环”,我们可以写出含义明确的“无限的”斐波那契数列。转自老赵的博客。


  IEnumerable与IEnumerator的区别比较小,在unity3d中只用到IEnumerator,功能和IEnumerable类似。至于他们的区别是什么,网上搜了半天,还是模糊不清,有童鞋能解释清楚的请留言。不过对于这段代码对于unity3d中yield的理解已经足够了。


游戏中需要使用yield的场景


  既然要使用yield,就得给个理由吧,不能为了使用yield而使用yield。那么先来看看游戏中可以用得到yield的场景:


游戏结算分数时,分数从0逐渐上涨,而不是直接显示最终分数


人物对话时,文字一个一个很快的出现,而不是一下突然出现
10、9、8……0的倒计时


某些游戏(如拳皇)掉血时血条UI逐渐减少,而不是突然降低到当前血量


…………………………


unity3d中yield应用举例


  首先是官网的一段代码:


复制代码
 1 using UnityEngine;
 2 using System.Collections;
 3 
 4 public class yield1 : MonoBehaviour {
 5 
 6     IEnumerator Do() {
 7         print("Do now");
 8         yield return new WaitForSeconds(2);
 9         print("Do 2 seconds later");
10     }
11     void Awake() {
12         StartCoroutine(Do());
13         print("This is printed immediately");
14     }
15 
16     // Use this for initialization
17     void Start () {
18     
19     }
20     
21     // Update is called once per frame
22     void Update () {
23     
24     }
25 }
复制代码
  这个例子将执行Do,但是Do函数之后的print指令会立刻执行。这个例子没有什么实际意义,只是为了验证一下yield确实是有延时的。


  下面来看看两段显示人物对话的代码(对话随便复制了一段内容),功能是一样的,但是方法不一样:


复制代码
 1 using UnityEngine;
 2 using System.Collections;
 3 
 4 public class dialog_easy : MonoBehaviour {
 5     public string dialogStr = "yield return的作用是在执行到这行代码之后,将控制权立即交还给外部。yield return之后的代码会在外部代码再次调用MoveNext时才会执行,直到下一个yield return——或是迭代结束。虽然上面的代码看似有个死循环,但事实上在循环内部我们始终会把控制权交还给外部,这就由外部来决定何时中止这次迭代。有了yield之后,我们便可以利用“死循环”,我们可以写出含义明确的“无限的”斐波那契数列。";
 6     public float speed = 5.0f;
 7 
 8     private float timeSum = 0.0f;
 9     private bool isShowing = false;
10     // Use this for initialization
11     void Start () {
12         ShowDialog();
13     }
14     
15     // Update is called once per frame
16     void Update () {
17         if(isShowing){
18             timeSum += speed * Time.deltaTime;
19             guiText.text = dialogStr.Substring(0, System.Convert.ToInt32(timeSum));
20 
21             if(guiText.text.Length == dialogStr.Length)
22                 isShowing = false;
23         }
24     }
25 
26     void ShowDialog(){
27         isShowing = true;
28         timeSum = 0.0f;
29     }
30 }
复制代码
  这段代码实现了在GUIText中逐渐显示一个字符串的功能,速度为每秒5个字,这也是新手常用的方式。如果只是简单的在GUIText中显示一段文字,ShowDialog()函数可以做的很好;但是如果要让字一个一个蹦出来,就需要借助游戏的循环了,最简单的方式就是在Update()中更新GUIText。


  从功能角度看,这段代码完全没有问题;但是从代码封装性的角度来看,这是一段很恶心的代码,因为本应由ShowDialog()完成的功能放到了Update()中,并且在类中还有两个private变量为这个功能服务。如果将来要修改或者删除这个功能,需要在ShowDialog()和Update()中修改,并且还可能修改那两个private变量。现在代码比较简单,感觉还不算太坏,一旦Update()中再来两个类似的的功能,估计写完代码一段时间之后自己修改都费劲。


  如果通过yield return null实现帧与帧之间的同步,则代码优雅了很多:


复制代码
 1 using UnityEngine;
 2 using System.Collections;
 3 
 4 public class dialog_yield : MonoBehaviour {
 5     public string dialogStr = "yield return的作用是在执行到这行代码之后,将控制权立即交还给外部。yield return之后的代码会在外部代码再次调用MoveNext时才会执行,直到下一个yield return——或是迭代结束。虽然上面的代码看似有个死循环,但事实上在循环内部我们始终会把控制权交还给外部,这就由外部来决定何时中止这次迭代。有了yield之后,我们便可以利用“死循环”,我们可以写出含义明确的“无限的”斐波那契数列。";
 6     public float speed = 5.0f;
 7 
 8     // Use this for initialization
 9     void Start () {
10         StartCoroutine(ShowDialog());
11     }
12     
13     // Update is called once per frame
14     void Update () {
15     }
16     
17     IEnumerator ShowDialog(){
18         float timeSum = 0.0f;
19         while(guiText.text.Length < dialogStr.Length){
20             timeSum += speed * Time.deltaTime;
21             guiText.text = dialogStr.Substring(0, System.Convert.ToInt32(timeSum));
22             yield return null;
23         }
24     }
25 }
复制代码
  相关代码都被封装到了ShowDialog()中,这么一来,不论是要增加、修改或删除功能,都变得容易了很多。


  根据官网手册的描述,yield return null可以让这段代码在下一帧继续执行。在ShowDialog()中,每次更新文字以后yield return null,直到这段文字被完整显示。看到这里,可能有童鞋不解:


为什么在协程中也可以用Time.deltaTime?
协程中的Time.deltaTime和Update()中的一样吗?
这样使用协程,会不会出现与主线程访问共享资源冲突的问题?(线程的同步与互斥问题)
yield return null太神奇了,为什么会在下一帧继续执行这个函数?
这段代码是不是相当于为ShowDialog()构造了一个自己的Update()?
  要解释这些问题,先看看unity3d中的协程是怎么运行的吧。


协程原理分析


  本段内容转自这篇博客,想看的童鞋自己点击。


 


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




  协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码。Unity在每一帧(Frame)都会去处理对象上的协程。Unity主要是在Update后去处理协程(检查协程的条件是否满足):
  从上图的剖析就明白,协程跟Update()其实一样的,都是Unity每帧对会去处理的函数(如果有的话)。如果MonoBehaviour 是处于激活(active)状态的而且yield的条件满足,就会协程方法的后面代码。还可以发现:如果在一个对象的前期调用协程,协程会立即运行到第一个 yield return 语句处,如果是 yield return null ,就会在同一帧再次被唤醒。如果没有考虑这个细节就会出现一些奇怪的问题。
  注:图和结论都是从UnityGems.com 上得来的,经过验证发现与实际不符,D.S.Qiu用的是Unity 4.3.4f1 进行测试的。经过测试验证,协程至少是每帧的LateUpdate()后去运行。
  协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法 Current 和 MoveNext() ,迭代器方法运行到 yield return 语句时,会返回一个expression表达式并保留当前在代码中的位置。 当下次调用迭代器函数时执行从该位置重新启动。unity3d在每帧做的工作就是:调用协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。详情见这篇博客。
  如果理解了这张图,之前显示人物对话的功能最后提到的那些疑惑也就很容易理解了:


协程和Update()一样更新,自然可以使用Time.deltaTime了,而且这个Time.deltaTime和在Update()当中使用是一样的效果(使用yield return null的情况下)
协程并不是多线程,它和Update()一样是在主线程中执行的,所以不需要处理线程的同步与互斥问题
yield return null其实没什么神奇的,只是unity3d封装以后,这个协程在下一帧就被自动调用了
可以理解为ShowDialog()构造了一个自己的Update(),因为yield return null让这个函数每帧都被调用了
0 0