作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明。如果你喜欢这篇文章,请点【推荐】。谢谢!

为什么需要协程

在游戏中有许多过程(Process)需要花费多个逻辑帧去计算。

  • 你会遇到“密集”的流程,比如说寻路,寻路计算量非常大,所以我们通常会把它分割到不同的逻辑帧去进行计算,以免影响游戏的帧率。
  • 你会遇到“稀疏”的流程,比如说游戏中的触发器,这种触发器大多数时候什么也不做,但是一旦被调用会做非常重要的事情(比图说游戏中自动开启的门就是在门前放了一个Empty Object作为trigger,人到门前就会触发事件)。

不管什么时候,如果你想创建一个能够历经多个逻辑帧的流程,但是却不使用多线程,那你就需要把一个任务来分割成多个任务,然后在下一帧继续执行这个任务。

比如,A*算法是一个拥有主循环的算法,它拥有一个open list来记录它没有处理到的节点,那么我们为了不影响帧率,可以让A*算法在每个逻辑帧中只处理open list中一部分节点,来保证帧率不被影响(这种做法叫做time slicing)。

再比如,我们在处理网络传输问题时,经常需要处理异步传输,需要等文件下载完毕之后再执行其他任务,一般我们使用回调来解决这个问题,但是Unity使用协程可以更加自然的解决这个问题,如下边的程序:

private IEnumerator Test()
{
WWW www = new WWW(ASSEST_URL);
yield return www;
AssetBundle bundle = www.assetBundle;}

协程是什么

从程序结构的角度来讲,协程是一个有限状态机,这样说可能并不是很明白,说到协程(Coroutine),我们还要提到另一样东西,那就是子例程(Subroutine),子例程一般可以指函数,函数是没有状态的,等到它return之后,它的所有局部变量就消失了,但是在协程中我们可以在一个函数里多次返回,局部变量被当作状态保存在协程函数中,知道最后一次return,协程的状态才别清除。

简单来说,协程就是:你可以写一段顺序的代码,然后标明哪里需要暂停,然后在下一帧或者一段时间后,系统会继续执行这段代码。

协程怎么用?

一个简单的C#代码,如下:

IEnumerator LongComputation()
{
while(someCondition)
{
/* 做一系列的工作 */ // 在这里暂停然后在下一帧继续执行
        yield return null;
}
}

协程是怎么工作的

注意上边的代码示例,你会发现一个协程函数的返回值是IEnumerator,它是一个迭代器,你可以把它当成指向一个序列的某个节点的指针,它提供了两个重要的接口,分别是Current(返回当前指向的元素)和MoveNext()(将指针向前移动一个单位,如果移动成功,则返回true)。IEnumerator是一个interface,所以你不用担心的具体实现。

通常,如果你想实现一个接口,你可以写一个类,实现成员,等等。迭代器块(iterator block)是一个方便的方式实现IEnumerator没有任何麻烦-你只是遵循一些规则,并实现IEnumerator由编译器自动生成。

一个迭代器块具备如下特征:

  1. 返回IEnumerator
  2. 使用yield关键字

所以yield关键词是干啥的?它声明序列中的下一个值或者是一个无意义的值。如果使用yield x(x是指一个具体的对象或数值)的话,那么movenext返回为true并且current被赋值为x,如果使用yield break使得movenext()返回false。

那么我举例如下,这是一个迭代器块:

public void Consumer()
{
foreach(int i in Integers())
{
Console.WriteLine(i.ToString());
}
} public IEnumerable<int> Integers()
{
yield return 1;
yield return 2;
yield return 4;
yield return 8;
yield return 16;
yield return 16777216;
}

注意上文在迭代的过程中,你会发现,在两个yield之间的代码只有执行完毕之后,才会执行下一个yield,在Unity中,我们正是利用了这一点,我们可以写出下面这样的代码作为一个迭代器块:


IEnumerator TellMeASecret(){
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null; Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null; PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}

然后我们可以使用下文这样的客户代码,来调用上文的程序,就可以实现延时的效果。

IEnumerator e = TellMeASecret();
while(e.MoveNext()) {
// do whatever you like
}

协程是如何实现延时的?

如你所见,yield return返回的值并不一定是有意义的,如null,但是我们更感兴趣的是,如何使用这个yield return的返回值来实现一些有趣的效果。

Unity声明了YieldInstruction来作为所有返回值的基类,并且提供了几种常用的继承类,如WaitForSeconds(暂停一段时间继续执行),WaitForEndOfFrame(暂停到下一帧继续执行)等等。更巧妙的是yield 也可以返回一个Coroutine真身,Coroutine A返回一个Coroutine B本身的时候,即等到B做完了再执行A。下面有详细说明:

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.
实现延时的关键代码是在StartCoroutine里面,以为笔者也没有见过Unity的源码,那么我只能猜想StartCoroutine这个函数的内部构造应该是这样的:
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes; foreach(IEnumerator coroutine in unblockedCoroutines){
if(!coroutine.MoveNext())
// This coroutine has finished
continue; if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
shouldRunNextFrame.Add(coroutine);
continue;
} if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */} unblockedCoroutines = shouldRunNextFrame;

当然了,我们还可以为YieldInstruction添加各种的子类,比如一个很容易想到的就是yield return new WaitForNotification(“GameOver”)来等待某个消息的触发,关于Unity的消息机制可以参考这篇文章:【Unity3D技巧】在Unity中使用事件/委托机制(event/delegate)进行GameObject之间的通信 (二) : 引入中间层NotificationCenter

还有些更好玩的?

第一个有趣的地方是,yield return可以返回任意YieldInstruction,所以我们可以在这里加上一些条件判断:

YieldInstruction y;

if(something)
y = null;else if(somethingElse)
y = new WaitForEndOfFrame();else
y = new WaitForSeconds(1.0f); yield return y;

第二个,由于一个协程只是一个迭代器块而已,所以你也可以自己遍历它,这在一些场景下很有用,例如在对协程是否执行加上条件判断的时候:

IEnumerator DoSomething(){
/* ... */} IEnumerator DoSomethingUnlessInterrupted(){
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}}

第三个,由于协程可以yield协程,所以我们可以自己创建一个协程函数,如下:

IEnumerator UntilTrueCoroutine(Func fn){
while(!fn()) yield return null;} Coroutine UntilTrue(Func fn){
return StartCoroutine(UntilTrueCoroutine(fn));} IEnumerator SomeTask(){
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */}

【Unity3D基础教程】给初学者看的Unity教程(五):详解Unity3D中的协程(Coroutine)的更多相关文章

  1. 【Unity3D基础教程】给初学者看的Unity教程(四):通过制作Flappy Bird了解Native 2D中的RigidBody2D和Collider2D

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 在第一篇文章[Unity3D基础教程] ...

  2. 【Unity3D基础教程】给初学者看的Unity教程(一):GameObject,Compoent,Time,Input,Physics

    作者:王选易,出处:http://www.cnblogs.com/neverdie/  欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点推荐.谢谢! Unity3D重要模块的类图 最近刚刚完成了一 ...

  3. 【Unity3D基础教程】给初学者看的Unity教程(七):在Unity中构建健壮的单例模式(Singleton)

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点推荐.谢谢! 该博客中的代码均出自我的开源项目 : 迷你微信 ...

  4. 【Unity3D基础教程】给初学者看的Unity教程(二):所有脚本组件的基类 -- MonoBehaviour的前世今生

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 上一次我们讲了GameObject,C ...

  5. 【Unity3D基础教程】给初学者看的Unity教程(零):如何学习Unity3D

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点推荐.谢谢! Unity3D有什么优势 Unity3D是一个跨 ...

  6. 【Unity3D基础教程】给初学者看的Unity教程(六):理解Unity的新GUI系统(UGUI)

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点推荐.谢谢! 理解UGUI的基础架构 UGUI是Unity在4 ...

  7. 【Unity3D基础教程】给初学者看的Unity教程(三):通过制作Flappy Bird了解Native 2D中的Sprite,Animation

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 上一次我们讲了MonoBehaviou ...

  8. Unity协程(Coroutine)管理类——TaskManager工具分享

    博客分类: Unity3D插件学习,工具分享 源码分析   Unity协程(Coroutine)管理类——TaskManager工具分享 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处 ...

  9. Unity中的协程(一)

    这篇文章很不错的问题,推荐阅读英文原版: Introduction to Coroutines Scripting with Coroutines   这篇文章转自:http://blog.csdn. ...

随机推荐

  1. 【zz】matlab 直方图匹配

    原文地址:http://www.cnblogs.com/tiandsp/archive/2012/12/19/2825418.html 直方图匹配或叫做直方图规定化都可以,是把原图像的直方图按照给定的 ...

  2. UML学习---交互

    在系统中,对象都不是孤立存在的,它们相互之间通过传递消息进行交互.交互是一种行为,由语境中的一组对象为达到某一目的而交换的一组消息构成.

  3. Java和eclipse常用操作

    eclipse: ctrl+F10 显示行号 ctrl+shift+F 自动对齐 ctrl+/ 注释 java: jar包: Manifest-Version - 指定清单文件的版本号 Main-Cl ...

  4. label标签

  5. Ngui中Sprite,SlicedSprite,Tiled Sprite,FilledSprite的区别

    Sprite:标准Sprite控件,自适应图片大小. Sliced Sprite:一个含有9个切片的Sprite,创建固定边框的控件最佳选择,固定大小,不会随图片大小而改变,可做人物头像等. Tile ...

  6. RSA_SHA256数字签名

    ------------------ rsa 加密与解密 ----------------------------- 数论知识的实际应用: rsa加密 把问题归结为对数字的加密. 被加密的数字为明文. ...

  7. ResultSet 结果集带回来的一些信息

    ResultSet.getMetaData() 得到结果集的结构信息,比如字段数.字段名等. ResultSet.getMetaData().getTableName(1) 就可以返回表名. Resu ...

  8. oracle xmltype导入并解析Excel数据 (一)创建表与序

    表说明: T_EXCEL_IMPORT_DATASRC: Excel数据存储表,(使用了xmltype存储Excel数据) 部分字段说明: BUSINESSTYPE: Excel模板类型,一个Exce ...

  9. vimperator setting records

    vimperator confugration files :highlight Hint color:#000;background:rgb(250,230,150);border-radius:4 ...

  10. 对字符串进行简单的字符数字统计 探索java中的List功能

    题目: 统计一个字符串中数字和字符串的个数,并分别进行排列,要求 1.数字,字符串可以从键盘获取. 2.储存在list 3.统计数字个数,字符串个数 4.把数字和字符串按从小到大的顺序输出 5.不能使 ...