zoukankan      html  css  js  c++  java
  • 【Unity3D基础教程】给初学者看的Unity教程(五):详解Unity3D中的协程(Coroutine)

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

    QQ图片20140529123319

    为什么需要协程

    在游戏中有许多过程(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);
      /* ... */}
  • 相关阅读:
    使用 yo 命令行向导给 SAP UI5 应用添加一个新的视图
    SAP Fiori Elements 应用的 manifest.json 文件运行时如何被解析的
    SAP UI5 标准应用的多语言支持
    微软 Excel 365 里如何设置下拉菜单和自动高亮成指定颜色
    SAP Fiori Elements 应用里的 Title 显示的内容是从哪里来的
    本地开发好的 SAP Fiori Elements 应用,如何部署到 ABAP 服务器上?
    如何在 Cypress 测试代码中屏蔽(Suppress)来自应用代码报出的错误消息
    教你一招:让集群慢节点无处可藏
    应用架构步入“无服务器”时代 Serverless技术迎来新发展
    MySQL数据库事务隔离性的实现
  • 原文地址:https://www.cnblogs.com/android-blogs/p/6133044.html
Copyright © 2011-2022 走看看