Unity之学习协程

协程是什么

理解协程技术,一种Unity提供的伪多线程,协程在主程序运行的同时开启另一段逻辑处理,来协同当前程序的执行的方法。是一个能暂停,暂停后立即返回,知道中断指令完成后继续执行的函数,它类似一个子线程单独出来处理一些问题,性能开销较小,但是在一个MononBehaviour提供的主线程里只能有一个处于运行状态的协程

进程、线程和协程的理解
  • 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度
  • 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)
  • 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
  • 协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力
  • 举个例子:
    假设有一个操作系统,是单核的,系统上没有其他的程序需要运行,有两个线程A和B,A和B在单独运行时都需要10秒来完成自己的任务,而且任务都是运算操作,A,B之间也没有竞争和共享数据的问题,现在A B两个线程并行,操作系统会不停的在AB两个线程之间切换,达到一种伪并行的效果,假设切换的频率是每秒一次,切换的成本是0.1秒(主要是栈切换),总共需要20 + 19 * 0.1 = 21.9秒,如果使用协程的方式,可以先运行A,A结束的时候让位给协程B,只发生一次切换,总时间是20 + 1 * 0.1 = 20.1秒,如果系统是双核的,而且线程是标准线程,那么AB两个线程就可以真正并行,总时间只需要10秒,而协程的方案仍然需要20.1秒
协程的作用

主要是用来解决方法的执行顺序

协同程序的特点
  1. 协程在中断指令(yieldInstruction)产生时暂停执行
  2. 协程一暂停执行立即返回//中断协程后返回主函数,暂停结束后继续执行协程剩余的函数
  3. 中断指令完成后从中断指令的下一行继续执行
  4. 同一时刻、同一脚本实例中可以有多个暂停的协程,但只有一个运行着的协程
  5. 函数体全部执行后,协程结束
  6. 协程可以很好地控制跨越一定帧数后执行的行为
  7. 协程在性能上、相比于一般函数几乎没有更所的开销
协程的使用方法
  1. 创建一个协程函数
IEnumerator methodName(Object parameter1, Object parameter2){
    //to do something
    yield return YieldInstruction/other/null;
    //to do something
}
*注意*:
协同函数的返回值的类型必须是Coroutine, Coroutine继承与Yieldinstruction  
所以协同程序的函数类型就只能是null,等待时间,等待的帧数。由此可见WWW也是实现了Coroutine的
  1. 开始一个协同程序
    通过MonoBehaviour提供的StartCoroutine方法来实现启动协同程序,包括StartCoroutine(string methodName) 和 StartCoroutine(IEnumerator routine)都可以开启一个线程。
方法优点缺点
StartCoroutine(IEnumerator)灵活,性能开销小无法单独的停止这个协程,如果需要停止这个协程就只能等待协同程序运行完毕或者使用StopAllCoroutine()放大
StartCoroutin(method:string, value: object = null);可以直接通过传入协同程序的方法名来停止这个协程:StopCoroutine(string methodName);性能的开销较大,只能传递一个参数
  1. 停止协同程序
    1. StopCoroutine(string methodName);
    2. StopAllCoroutine();
    3. 设置gameObject的active为false时可以终止协同程序,但是再次设置为true后协程不会再启动。如果是将
      协同程序所在的脚本的enabled设置为false则不会生效,这是因为协同程序在被开启后作为一个线程在运行,而MonoBehaviour也是一个线程,他们成为互不干扰的模块,除非代码中在调用,他们作用于同一个对象,只有当对象不可见才能同时终止这两个线程。然而,为了管理我们额外开启的线程,Unity3D将协同程序的调用放在了MonoBehaviour中,这样我们在编程时就可以方便的调用指定脚本中的协同程序,而不是无法去管理,特别是对于只根据方法名来判断线程的方式在多人开发中很容易出错,这样的设计保证了对象、脚本的条理化管理,并防止了重名
  2. 协同程序的执行顺序
    开始协同程序 -> 执行协同程序 ->中断协同程序(中断指令) ->返回上层继续执行 -> 中断指令结束后继续执行协同程序剩下的内容
  3. 协同程序的注意事项
    1. 不能再Update或者FixUpdate方法中使用协同程序,否则会报错。
    2. 关于中断指令:中断指令/YieldInstruction,一个协程收的到中断指令后暂停执行,返回上层执行同时等待这个指令达成后继续执行
    3. Unity里与协同程序有关的函数
指令描述实现
WaitForSeconds等待指定秒数yield return new WaitForSeconds(2);
WaitForFixedUpdate等待一个固定帧yield return new WaitForFixedUpdate();
WaitForEndOfFrame等待帧结束yield return new WaitForEndOfFrame();
StartCoroutine等待一个新协程暂停yield return StartCoroutine();
WWW等待一个加载完成yield return www;

注意

  1. 在一个协程A里在中断指令里再启动一个协程B,在yield return StartCoroutine时执行的顺序是:
    1. 先执行新协程B;
    2. 新协程暂停后向上返回协程A,A协程暂停,返回协程A的上层函数
    3. 因为决定协程A时候结束的标志是新协程B是否结束,所以当新协程B结束后返回协程A执行余下的内容
    4. 协程A执行结束
  2. 关于WWW的中断指令可参考API:
    You can inspect the isDone property to see if the download has completed or yield the download object to automatically wait until it is(without blocking the rest of the game)
    你可以检查isDone属性来查看时候已经下载完成,或者yield自动等待下载物体,直到它被下载完成(不会影响游戏的其余部分)。
  3. 协同程序的中断返回机制也可用于指定时间间隔执行一个程序
//每3秒执行一次
while(true){
    //to do something
    yield return new WaitForSeconds(3);
}

  1. 协同程序的执行流程
    /// <summary>
    /// Start,协程的执行流程
    /// Start函数运行,输出"1",然后开始协程Do
    /// Do输出"2",然后开启协程NewDo()
    /// NewDo输出"3",产生中断指令后暂停,立即返回Do
    /// Do()产生中断指令后暂停,Do暂停后立即返回Start()函数
    /// Start执行StarCoroutine的下一条语句:输出"4";
    /// 2秒后,NewDo的中断指令完成并继续执行,输出"5",协程NewDo()结束;
    /// Do的中断指令因为协程NewDo的结束而完成并继续执行,输出"6", 协程Do结束
    /// </summary>
    private void Start()
    {
        Debug.Log("------------1------------");
        StartCoroutine(Do());
        Debug.Log("------------4------------");
    }

    IEnumerator Do()
    {
        Debug.Log("------------2------------");
        yield return StartCoroutine(NewDo());
        Debug.Log("------------6------------");
    }

    IEnumerator NewDo()
    {
        Debug.Log("------------3------------");
        yield return new WaitForSeconds(2);
        Debug.Log("------------5------------");
    }
	void Start ()
	{
        Debug.Log("---------------1----------------");
        StartCoroutine("TestCoroutineMethod");
        Debug.Log("---------------2----------------");
        Debug.Log("---------------3----------------");
	}


    IEnumerator TestCoroutineMethod()
    {
        Debug.Log("---------------4----------------");
        yield return new WaitForSeconds(1f);
        Debug.Log("---------------5----------------");
    }

输出结果: 1-4-2-3-5

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

使用协程的几个例子
实例1:计时器
    private void Start()
    {
        StartCoroutine(CountDown(3));
        Debug.Log("Start Time is " + Time.time);
    }


    IEnumerator CountDown(float seconds)
    {
        for (float  timer = seconds; timer > 0; timer -= Time.deltaTime)
        {
            yield return 0;
        }

        Debug.Log("This Message appears after " + seconds + " seconds. NowTime is " + Time.time);
    }

image
理解CountDown方法:

  1. IEnumerator的返回值
  2. For循环中的yield return
    为了能在连续的多帧中(在这个例子中,3秒钟等同于很多帧)调用该方法,Unity必须通过某种方式来储存这个方法的状态,这是通过IEnumerator中使用yield return语句得到的返回值,当你"yield"一个方法,你相当于说了,“现在停止这个方法,然后在下一帧中从这里重新开始”
    注意:用0或者null来yield的意思是告诉协程等待下一帧,知道继续执行完为止。当然也可以继续yield其他协程
实例2:多次输入"Hello"
    private void Start()
    {
        StartCoroutine(SayHellFiveTimes());
    }

    IEnumerator SayHellFiveTimes()
    {
        yield return StartCoroutine(CountDown(1));
        Debug.Log("Hello1" + Time.time);
        yield return StartCoroutine(CountDown(2));
        Debug.Log("Hello2" + Time.time);
        yield return StartCoroutine(CountDown(3));
        Debug.Log("Hello3" + Time.time);
        yield return StartCoroutine(CountDown(4));
        Debug.Log("Hello4" + Time.time);
        yield return StartCoroutine(CountDown(5));
        Debug.Log("Hello5" + Time.time);
    }

    IEnumerator CountDown(float seconds)
    {
        for (float timer = seconds; timer > 0; timer -= Time.deltaTime)
        {
            yield return 0;
        }
    }
实例3:多次输入"Hello"

通过在while循环中使用yield,你可以得到一个无限循环的协程,这几乎就跟一个Update()循环等同


IEnumerator SayHellEveryFrame(){
    whiel(true){
        //1. Say Hello
        Debug.Log("Hello");
        //2. Wait until next frame
        yield return 0;
    }//3.This is a forever-loop, goto step.1
}


/// <summary>
/// 这个方法突出了协程一个非常重要的特点:方法的状态被储存了,使得方法中定义的这些变量都会保存它们的值,即使不同的帧中。
/// </summary>
IEnumerator CountSeconds()
{
    int second = 0;

    while (true)
    {
        for (float timer = 0; timer < 1; timer += Time.deltaTime)
            //1. Wait until next frame
            yield return 0;
            //2. 条件未完成,继续循环
        //3.add 1
        second++;
        Debug.Log(second + "seconds have passed since the Coroutine started");
    }
}

实例4:定时打印指定次数的消息
/// <summary>
/// 重复打印消息
/// </summary>
/// <param name="count">重复次数</param>
/// <param name="frequency">打印频率</param>
/// <param name="message">内容</param>
IEnumerator RepeatMessage(int count, float frequency, string message)
{
    for (int i = 1; i <= count ; i++)
    {
        Debug.Log("the " + i + " time print message in " + Time.time + " sceonds" + ", the Message is " + message);
        for (float timer = 0; timer < 1; timer += Time.deltaTime)
        {
            yield return 0;              
        }
    }     
}

image

实例5:运动到某个位置
public Vector3 targetPosition;
public float moveSpeed;


private void Start()
{
    StartCoroutine(MoveToPositon(targetPosition));
}

IEnumerator MoveToPositon(Vector3 targetPosition)
{
    while(transform.position != targetPosition)
    {
        transform.position = Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime);
        yield return 0;
    }
}

注意:这个方法用了yield,但是并没有用0或者null,而是用了Wait()l来yield,相当于“不再继续执行本方法,直到Wait完成”

实例6:按指定路径前进
public Vector3[] path;
public float moveSpeed;
public bool isLoop;


private void Start()
{
    StartCoroutine(MoveOnPath(isLoop));
}

IEnumerator MoveOnPath(bool loop)
{
    do
    {
        foreach (var point in path)
        {
            yield return StartCoroutine(MoveToPosition(point));
        }
    } while (isLoop);
}

IEnumerator MoveToPosition(Vector3 point)
{
    while(transform.position != point)
    {
        transform.position = Vector3.MoveTowards(transform.position, point, moveSpeed * Time.deltaTime);
        yield return 0;
    }
    yield return StartCoroutine(PauseForSecond(3));
}

IEnumerator PauseForSecond(float sceconds)
{
    for (float timer = 0; timer < sceconds; timer += Time.deltaTime)
    {
        this.transform.Rotate(new Vector3(0, 1, 0));
        yield return 0;
    }    
}

注意

  • 在程序中调用StopCortine()方法只能终止字符串形式启动(开始)的协程;
  • 多个协程可以同时运行,他们会根据各自的启动顺序来更新
  • 协程可以嵌套任意多层
  • 如果想让多个脚本访问一个协程,那么你可以定义静态的协程
  • 协程不是多线程,他们运行在同一线程中,跟普通的脚本一样
  • 如果你的程序需要进行大量的计算,那么可以考虑在一个随时间进行的协程中处理它们
  • IEnumerator类型的方法不能带ref或者out型的参数,但可以带被传递的引用
  • 目前在Unity中没有简便的方法来检测作用于对象的协程数量以及具体是哪些协程作用在对象上。
关于加载指令(通过WWW加载本地文件)
/// <summary>
/// 大概执行流程,点击按钮后开始执行协同程序,WWW按照提供的url进行加载,完毕后yield return www;中断指令跳转到主线程
/// 主线程继续执行其他内容,www在加载完成后跳出中断继续执行余下内容
/// 加载完毕,实例化加载内容
/// </summary>
private string path = "file:/F:/Resources/Dragon.unity3d";

private void OnGUI()
{
    if(GUI.Button(new Rect(200, 200, 150, 30), "点击进入协同程序"))
    {
        Debug.Log("1");
        StartCoroutine(LoadingBundle(path));
        Debug.Log("3");
    }
}

private IEnumerator LoadingBundle(string url)
{
    Debug.Log(2);
    using(WWW www = new WWW(url))
    {
        yield return www;
        Debug.Log("4");
        if(www.error != null)
        {
            var bytes = www.bytes;
        }

        AssetBundle ab = www.assetBundle;
        GameObject gameObj = ab.mainAsset as GameObject;
        Instantiate(gameObj);
        Debug.Log("5");
        Debug.Log("load local assetBundle finished..." + gameObj);
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值