搞引擎的去看项目

做引擎的时候通篇C++,上手Unity后,总觉得C#在C++面前应该就好比玩具一般。但是随着项目代码看的越多,越发现,C#还是有点花头的。首先这个协程(Coroutine)我之前还真没用过。当然也可能因为我C++的语法也不是那么好/捂脸,哈哈。

Unity项目中出现了大量的协程(Coroutine),看懂还是ok的,但是这种是似懂非懂的,心里总是个坎,今天看到一系列的文章,文笔清晰,先转载过来,一睹为快。

Unity协程(Coroutine)管理类——TaskManager工具分享(转载自DSQiu,链接:http://dsqiu.iteye.com/blog/2022992)

本篇D.S.Qiu要分享的TaskManager就是一个协程 管理类。 TaskManager —— Unity3D Managed Coroutines with Start, Stop, Resume ,看着就觉得很强大,当然是对于我这种对协程理解不深的来说。下面贴出 The Motivation of the author:

/// The motivation for this is twofold:
///
/// 1. The existing coroutine API provides no means of stopping specific
///    coroutines; StopCoroutine only takes a string argument, and it stops
///    all coroutines started with that same string; there is no way to stop
///    coroutines which were started directly from an enumerator.  This is
///    not robust enough and is also probably pretty inefficient.
///
/// 2. StartCoroutine and friends are MonoBehaviour methods.  This means
///    that in order to start a coroutine, a user typically must have some
///    component reference handy.  There are legitimate cases where such a
///    constraint is inconvenient.  This implementation hides that
///    constraint from the user.

代码很简单,但却很解渴,Unity官方只听过了StopCoroutine(string methodName)或StopAllCoroutine() 这两个停止方法,从api就会觉得Unity的整体方法论还不完善,所以才会觉得TaskManager的难能可贵。由于源码简单,就不做解释了,See source for document :

/// Simple, really.  There is no need to initialize or even refer to TaskManager.  
/// When the first Task is created in an application, a "TaskManager" GameObject  
/// will automatically be added to the scene root with the TaskManager component  
/// attached.  This component will be responsible for dispatching all coroutines  
/// behind the scenes.  
///  
/// Task also provides an event that is triggered when the coroutine exits.  
  
using UnityEngine;  
using System.Collections;  
  
/// A Task object represents a coroutine.  Tasks can be started, paused, and stopped.  
/// It is an error to attempt to start a task that has been stopped or which has  
/// naturally terminated.  
public class Task  
{  
    /// Returns true if and only if the coroutine is running.  Paused tasks  
    /// are considered to be running.  
    public bool Running {  
        get {  
            return task.Running;  
        }  
    }  
      
    /// Returns true if and only if the coroutine is currently paused.  
    public bool Paused {  
        get {  
            return task.Paused;  
        }  
    }  
      
    /// Delegate for termination subscribers.  manual is true if and only if  
    /// the coroutine was stopped with an explicit call to Stop().  
    public delegate void FinishedHandler(bool manual);  
      
    /// Termination event.  Triggered when the coroutine completes execution.  
    public event FinishedHandler Finished;  
  
    /// Creates a new Task object for the given coroutine.  
    ///  
    /// If autoStart is true (default) the task is automatically started  
    /// upon construction.  
    public Task(IEnumerator c, bool autoStart = true)  
    {  
        task = TaskManager.CreateTask(c);  
        task.Finished += TaskFinished;  
        if(autoStart)  
            Start();  
    }  
      
    /// Begins execution of the coroutine  
    public void Start()  
    {  
        task.Start();  
    }  
  
    /// Discontinues execution of the coroutine at its next yield.  
    public void Stop()  
    {  
        task.Stop();  
    }  
      
    public void Pause()  
    {  
        task.Pause();  
    }  
      
    public void Unpause()  
    {  
        task.Unpause();  
    }  
      
    void TaskFinished(bool manual)  
    {  
        FinishedHandler handler = Finished;  
        if(handler != null)  
            handler(manual);  
    }  
      
    TaskManager.TaskState task;  
}  
  
class TaskManager : MonoBehaviour  
{  
    public class TaskState  
    {  
        public bool Running {  
            get {  
                return running;  
            }  
        }  
  
        public bool Paused  {  
            get {  
                return paused;  
            }  
        }  
  
        public delegate void FinishedHandler(bool manual);  
        public event FinishedHandler Finished;  
  
        IEnumerator coroutine;  
        bool running;  
        bool paused;  
        bool stopped;  
          
        public TaskState(IEnumerator c)  
        {  
            coroutine = c;  
        }  
          
        public void Pause()  
        {  
            paused = true;  
        }  
          
        public void Unpause()  
        {  
            paused = false;  
        }  
          
        public void Start()  
        {  
            running = true;  
            singleton.StartCoroutine(CallWrapper());  
        }  
          
        public void Stop()  
        {  
            stopped = true;  
            running = false;  
        }  
          
        IEnumerator CallWrapper()  
        {  
            yield return null;  
            IEnumerator e = coroutine;  
            while(running) {  
                if(paused)  
                    yield return null;  
                else {  
                    if(e != null && e.MoveNext()) {  
                        yield return e.Current;  
                    }  
                    else {  
                        running = false;  
                    }  
                }  
            }  
              
            FinishedHandler handler = Finished;  
            if(handler != null)  
                handler(stopped);  
        }  
    }  
  
    static TaskManager singleton;  
  
    public static TaskState CreateTask(IEnumerator coroutine)  
    {  
        if(singleton == null) {  
            GameObject go = new GameObject("TaskManager");  
            singleton = go.AddComponent();  
        }  
        return new TaskState(coroutine);  
    }  
}  

小结:

本文主要是分享我的收藏的一些“干货”,TaskManager 和 vp_Timer 在项目中发挥了很大的作用,D.S.Qiu 一再觉得强大的东西不都是复杂的,能够使用最简单的本质方法解决问题才是代码设计的追求。文末附上了相关的链接以及TaskManager的代码。

Unity协程(Coroutine)原理深入剖析(转载自DSQiu,链接:http://dsqiu.iteye.com/blog/2029701)

记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield return ,就可以通过StartCoroutine调用了。后来也是一直稀里糊涂地用,上网google些基本都是例子,很少能帮助深入理解Unity协程的原理的。

本文只是从Unity的角度去分析理解协程的内部运行原理,而不是从C#底层的语法实现来介绍(后续有需要再进行介绍),一共分为三部分:

  • 线程(Thread)和协程(Coroutine)
  • Unity中协程的执行原理
  • IEnumerator & Coroutine

之前写过一篇《Unity协程(Coroutine)管理类——TaskManager工具分享》主要是介绍TaskManager实现对协程的状态控制,没有Unity后台实现的协程的原理进行深究。虽然之前自己对协程还算有点了解了,但是对Unity如何执行协程的还是一片空白,在UnityGems.com上看到两篇讲解Coroutine,如数家珍,当我看到Advanced Coroutine后面的Hijack类时,顿时觉得十分精巧,眼前一亮,遂动了写文分享之。

线程(Thread)和协程(Coroutine)

D.S.Qiu觉得使用协程的作用一共有两点:1)延时(等待)一段时间执行代码;2)等某个操作完成之后再执行后面的代码。总结起来就是一句话:控制代码在特定的时机执行。

很多初学者,都会下意识地觉得协程是异步执行的,都会觉得协程是C# 线程的替代品,是Unity不使用线程的解决方案。

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

Unity中协程的执行原理

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 ,就会在同一帧再次被唤醒。如果没有考虑这个细节就会出现一些奇怪的问题『1』。

『1』注 图和结论都是从UnityGems.com 上得来的,经过下面的验证发现与实际不符,D.S.Qiu用的是Unity 4.3.4f1 进行测试的。经过测试验证,协程至少是每帧的LateUpdate()后去运行。(Patrick:针对这个问题,我和D.S.Qiu测试方法和结果也不同,我使用的是Unity 5.6.2f1 (64-bit),测试方法是将D.S.Qiu的部分代码注释掉,测试结果是如果是 yield return null,那么在下一帧的Update和LateUpdate之间会被唤醒;如果是yield return new WaitForSeconds(1f),则是在1秒之后的那一帧的Update和LateUpdate之间会被唤醒。而从Unity官方文档也可以看出来,现在Coroutine的执行确实是在Update和LateUpdate之间。D.S.Qiu的测试代码有点问题,因为只能看出来协程是在第一帧的LateUpdate()后去运行的。)

下面使用 yield return new WaitForSeconds(1f); 在Start,Update 和 LateUpdate 中分别进行测试:

using UnityEngine;  
using System.Collections;  
  
public class TestCoroutine : MonoBehaviour {  
  
    private bool isStartCall = false;  //Makesure Update() and LateUpdate() Log only once  
    private bool isUpdateCall = false;  
    private bool isLateUpdateCall = false;  
    // Use this for initialization  
    void Start () {  
        //if (!isStartCall)  					//Comment by Patrick
        //{  									//Comment by Patrick
            Debug.Log("Start Call Begin");  
            StartCoroutine(StartCoutine());  
            Debug.Log("Start Call End");  
            //isStartCall = true;  				//Comment by Patrick
        //}  									//Comment by Patrick
      
    }  
    IEnumerator StartCoutine()  
    {  
          
        Debug.Log("This is Start Coroutine Call Before");  
        yield return new WaitForSeconds(1f);  
        Debug.Log("This is Start Coroutine Call After");  
             
    }  
    // Update is called once per frame  
    void Update () {  
        //if (!isUpdateCall)  					//Comment by Patrick
        //{  									//Comment by Patrick
            Debug.Log("Update Call Begin");  
            StartCoroutine(UpdateCoutine());  
            Debug.Log("Update Call End");  
            //isUpdateCall = true;  			//Comment by Patrick
        //}  									//Comment by Patrick
    }  
    IEnumerator UpdateCoutine()  
    {  
        Debug.Log("This is Update Coroutine Call Before");  
        yield return new WaitForSeconds(1f);  
        Debug.Log("This is Update Coroutine Call After");  
    }  
    void LateUpdate()  
    {  
        //if (!isLateUpdateCall)  				//Comment by Patrick
        //{  									//Comment by Patrick
            Debug.Log("LateUpdate Call Begin");  
            StartCoroutine(LateCoutine());  
            Debug.Log("LateUpdate Call End");  
            //isLateUpdateCall = true;  		//Comment by Patrick
        //}  									//Comment by Patrick
    }  
    IEnumerator LateCoutine()  
    {  
        Debug.Log("This is Late Coroutine Call Before");  
        yield return new WaitForSeconds(1f);  
        Debug.Log("This is Late Coroutine Call After");  
    }  
}  

(Patrick:由于Unity也对Coroutine的执行顺序执行了更改,那么我就直接把我的测试结果帖出来了)

Start Call Begin
This is Start Coroutine Call Before
Start Call End
Update Call Begin
This is Update Coroutine Call Before
Update Call End
LateUpdate Call Begin
This is Late Coroutine Call Before
LateUpdate Call End
Update Call Begin
This is Update Coroutine Call Before
Update Call End
...
Update Call Begin
This is Update Coroutine Call Before
Update Call End
This is Start Coroutine Call After
This is Update Coroutine Call After
This is Late Coroutine Call After
LateUpdate Call Begin
This is Late Coroutine Call Before
LateUpdate Call End
Update Call Begin
This is Update Coroutine Call Before
Update Call End
This is Update Coroutine Call After
This is Late Coroutine Call After
LateUpdate Call Begin
This is Late Coroutine Call Before
LateUpdate Call End
...

然后将yield return new WaitForSeconds(1f);改为 yield return null;

(Patrick:由于Unity也对Coroutine的执行顺序执行了更改,那么我就直接把我的测试结果帖出来了)

Start Call Begin
This is Start Coroutine Call Before
Start Call End
Update Call Begin
This is Update Coroutine Call Before
Update Call End
LateUpdate Call Begin
This is Late Coroutine Call Before
LateUpdate Call End
Update Call Begin
This is Update Coroutine Call Before
Update Call End
This is Start Coroutine Call After
This is Update Coroutine Call After
This is Late Coroutine Call After
LateUpdate Call Begin
This is Late Coroutine Call Before
LateUpdate Call End
Update Call Begin
This is Update Coroutine Call Before
Update Call End
This is Update Coroutine Call After
This is Late Coroutine Call After
LateUpdate Call Begin
This is Late Coroutine Call Before
LateUpdate Call End
...

前面在介绍TaskManager工具时,说到MonoBehaviour 没有针对特定的协程提供Stop方法,其实不然,可以通过MonoBehaviour enabled = false 或者 gameObject.active = false 就可以停止协程的执行『2』。

经过验证,『2』的结论也是错误的,正确的结论是,MonoBehaviour.enabled = false 协程会照常运行,但 gameObject.SetActive(false) 后协程却全部停止,即使在Inspector把 gameObject 激活还是没有继续执行:(Patrick:这次我和D.S.Qiu测试结果相同)

using UnityEngine;  
using System.Collections;  
  
public class TestCoroutine : MonoBehaviour {  
  
    private bool isStartCall = false;  //Makesure Update() and LateUpdate() Log only once  
    private bool isUpdateCall = false;  
    private bool isLateUpdateCall = false;  
    // Use this for initialization  
    void Start () {  
        if (!isStartCall)  
        {  
            Debug.Log("Start Call Begin");  
            StartCoroutine(StartCoutine());  
            Debug.Log("Start Call End");  
            isStartCall = true;  
        }  
      
    }  
    IEnumerator StartCoutine()  
    {  
          
        Debug.Log("This is Start Coroutine Call Before");  
        yield return new WaitForSeconds(1f);  
        Debug.Log("This is Start Coroutine Call After");  
             
    }  
    // Update is called once per frame  
    void Update () {  
        if (!isUpdateCall)  
        {  
            Debug.Log("Update Call Begin");  
            StartCoroutine(UpdateCoutine());  
            Debug.Log("Update Call End");  
            isUpdateCall = true;  
            this.enabled = false;  
            //this.gameObject.SetActive(false);  
        }  
    }  
    IEnumerator UpdateCoutine()  
    {  
        Debug.Log("This is Update Coroutine Call Before");  
        yield return new WaitForSeconds(1f);  
        Debug.Log("This is Update Coroutine Call After");  
        yield return new WaitForSeconds(1f);  
        Debug.Log("This is Update Coroutine Call Second");  
    }  
    void LateUpdate()  
    {  
        if (!isLateUpdateCall)  
        {  
            Debug.Log("LateUpdate Call Begin");  
            StartCoroutine(LateCoutine());  
            Debug.Log("LateUpdate Call End");  
            isLateUpdateCall = true;  
  
        }  
    }  
    IEnumerator LateCoutine()  
    {  
        Debug.Log("This is Late Coroutine Call Before");  
        yield return null;  
        Debug.Log("This is Late Coroutine Call After");  
    }  
}

先在Update中调用 this.enabled = false; 得到的结果:

Coroutine

然后把 this.enabled = false; 注释掉,换成 this.gameObject.SetActive(false); 得到的结果如下:

Coroutine

整理得到:通过设置MonoBehaviour脚本的enabled会终止对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。

yield 后面可以有的表达式:

  • a) null - the coroutine executes the next time that it is eligible
  • b) WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete
  • c) WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated
  • d) WaitForSeconds - causes the coroutine not to execute for a given game time period
  • e) WWW - waits for a web request to complete (resumes as if WaitForSeconds or null)
  • f) Another coroutine - in which case the new coroutine will run to completion before the yielder is resumed

值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足。

IEnumerator & Coroutine

协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法 Current 和 MoveNext() ,前面介绍的 TaskManager 就是利用者两个方法对协程进行了管理,只有当MoveNext()返回 true时才可以访问 Current,否则会报错。(Patrick:这个蛮重要的,Unity项目中很多在MoveNext中实现一些东西,然后Current返回null,引出yield return null;)迭代器方法运行到 yield return 语句时,会返回一个expression表达式并保留当前在代码中的位置。 当下次调用迭代器函数时执行从该位置重新启动。

Unity在每帧做的工作就是:调用 协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。

这里在介绍一个协程的交叉调用类 Hijack(参见附件):(Patrick:TODO吧,没太看明白,不过暂时用不到,就先不试了)

using System;  
using System.Collections.Generic;  
using System.Linq;  
using UnityEngine;  
using System.Collections;  
   
[RequireComponent(typeof(GUIText))]  
public class Hijack : MonoBehaviour {  
   
    //This will hold the counting up coroutine  
    IEnumerator _countUp;  
    //This will hold the counting down coroutine  
    IEnumerator _countDown;  
    //This is the coroutine we are currently  
    //hijacking  
    IEnumerator _current;  
   
    //A value that will be updated by the coroutine  
    //that is currently running  
    int value = 0;  
   
    void Start()  
    {  
        //Create our count up coroutine  
        _countUp = CountUp();  
        //Create our count down coroutine  
        _countDown = CountDown();  
        //Start our own coroutine for the hijack  
        StartCoroutine(DoHijack());  
    }  
   
    void Update()  
    {  
        //Show the current value on the screen  
        guiText.text = value.ToString();  
    }  
   
    void OnGUI()  
    {  
        //Switch between the different functions  
        if(GUILayout.Button("Switch functions"))  
        {  
            if(_current == _countUp)  
                _current = _countDown;  
            else  
                _current = _countUp;  
        }  
    }  
   
    IEnumerator DoHijack()  
    {  
        while(true)  
        {  
            //Check if we have a current coroutine and MoveNext on it if we do  
            if(_current != null && _current.MoveNext())  
            {  
                //Return whatever the coroutine yielded, so we will yield the  
                //same thing  
                yield return _current.Current;  
            }  
            else  
                //Otherwise wait for the next frame  
                yield return null;  
        }  
    }  
   
    IEnumerator CountUp()  
    {  
        //We have a local increment so the routines  
        //get independently faster depending on how  
        //long they have been active  
        float increment = 0;  
        while(true)  
        {  
            //Exit if the Q button is pressed  
            if(Input.GetKey(KeyCode.Q))  
                break;  
            increment+=Time.deltaTime;  
            value += Mathf.RoundToInt(increment);  
            yield return null;  
        }  
    }  
   
    IEnumerator CountDown()  
    {  
        float increment = 0f;  
        while(true)  
        {  
            if(Input.GetKey(KeyCode.Q))  
                break;  
            increment+=Time.deltaTime;  
            value -= Mathf.RoundToInt(increment);  
            //This coroutine returns a yield instruction  
            yield return new WaitForSeconds(0.1f);  
        }  
    }  
   
}  

上面的代码实现是两个协程交替调用,对有这种需求来说实在太精妙了。

小结:

今天仔细看了下UnityGems.com 有关Coroutine的两篇文章,虽然第一篇(参考①)现在验证的结果有很多错误,但对于理解协程还是不错的,尤其是当我发现Hijack这个脚本时,就迫不及待分享给大家。

本来没觉得会有UnityGems.com上的文章会有错误的,无意测试了发现还是有很大的出入,当然这也不是说原来作者没有经过验证就妄加揣测,D.S.Qiu觉得很有可能是Unity内部的实现机制改变了,这种东西完全可以改动,Unity虽然开发了很多年了,但是其实在实际开发中还是有很多坑,越发觉得Unity的无力,虽说容易上手,但是填坑的功夫也是必不可少的。

看来很多结论还是要通过自己的验证才行,贸然复制粘贴很难出真知记!

参考:

  • ①UnityGems.com: http://unitygems.com/coroutines/
  • ②UnityGems.com: http://unitygems.com/advanced-coroutines/
  • ③葱烧烙饼: http://blog.sina.com.cn/s/blog_5b6cb9500100xgmp.html

Tolua中协程的用法(转载自韦小逸,链接:http://blog.csdn.net/qq_30168505/article/details/52752868)

  • 协程函数的开启 : coroutine.start(协程函数)
  • 协程函数的挂起: coroutine.step
  • 协程函数的延时: coroutine.wait(延时时间) 注意:时间的单位是秒
  • 协程函数的结束: coroutine.stop(协程对象) 注意:协程函数关闭的协程对象是对应的协程开启函数的返回值
  • 协程下载: coroutine.www(网址)

其中,除了 coroutine.start(协程函数) 和 coroutine.stop(协程对象) 之外,其他的协程方法只允许在协程函数的内部使用

虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2017 年 11 月 1 日发表,原文链接(http://geekfaner.com/unity/blog7_Coroutine.html)