【unity】简要分析yield及协同程序的用法

在学习unity3d的时候很容易看到下面这个例子:html

复制代码

1 void Start () {2     StartCoroutine(Destroy());3 }4 5 IEnumerator Destroy(){6     yield return WaitForSeconds(3.0f);7     Destroy(gameObject);8 }

复制代码

  这个函数干的事情很简单:调用StartCoroutine函数开启协程,yield等待一段时间后,销毁这个对象;因为是协程在等待,因此不影响主线程操做。通常来讲,看到这里的时候都还不会晕,yield就是延时一段时间之后继续往下执行呗,恩,学会了,看着还蛮好用的。java

====================================================分割线====================================================express

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

  unity3d官方对于协程的解释是:一个协同程序在执行过程当中,能够在任意位置使用yield语句。yield的返回值控制什么时候恢复协同程序向下执行。协同程序在对象自有帧执行过程当中堪称优秀。协同程序在性能上没有更多的开销。StartCoroutine函数是马上返回的,可是yield能够延迟结果。直到协同程序执行完毕。(原文:The execution of a coroutine can be paused at any point using the yield statement. The yield return value specifies when the coroutine is resumed. Coroutines are excellent when modelling behaviour over several frames. Coroutines have virtually no performance overhead. StartCoroutine function always returns immediately, however you can yield the result. This will wait until the coroutine has finished execution.)异步

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

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逐渐上涨,而不是直接显示最终分数

  • 人物对话时,文字一个一个很快的出现,而不是一下忽然出现

  • 十、九、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 initialization17     void Start () {18     19     }20     21     // Update is called once per frame22     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 initialization11     void Start () {12         ShowDialog();13     }14     15     // Update is called once per frame16     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 frame14     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中执行的。使用协程你不用考虑同步和锁的问题。

  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 ,就会在同一帧再次被唤醒。若是没有考虑这个细节就会出现一些奇怪的问题。
  注:图和结论都是从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让这个函数每帧都被

 

  1. yield:声明一种特殊的return,能够传值如Waitforsecond(5.0),实现延时或某一事件发生后触发

若是有须要有依赖关系顺序执行的函数,如

private var state = 0;

function Update() {
if (state == 0) {
// do step 0
state = 1;
return;
}
if (state == 1) {
// do step 1
state = 2;
return;
}
// ...
}

可这样使用 yield

while(true) {
// do step 0
yield; // wait for one frame
// do step 1
yield; // wait for one frame
// ...
}

能够把 yield 理解为一种挂起 ,当函数执行到它时不在返回而是执行下一个函数的语句,一旦收到yield后定义的函数为true的消息,则执行yield返回并进行下面的代码。

2.coroutine:做为function StartCoroutine (routine : IEnumerator) 的返回值。一个函数中包含yield声明,就能够构成一个coroutine,这个函数的调用时必由StartCoroutine来进行调用,以下

function Awake(){

 StartCoroutine(MyDo());

}

function MyDo(){

 while (true) {

  print("first");

  yield WaitForSeconds(3.0f);

  print("second");

  yield ;

  break;

 }

}

也能够在如start等公用函数中直接使用yield,不用StartCoroutine.当消息返回后,系统会自动从新执行yield后的代码,以下:

function Start () {     // Start a download of the given URL    var www : WWW = new WWW (url);    // Wait for download to complete    yield www;    // assign texture    renderer.material.mainTexture = www.texture;
}
相关文章
相关标签/搜索