找回密码
 立即注册
查看: 417|回复: 0

Unity StartCoroutine

[复制链接]
发表于 2021-12-7 16:18 | 显示全部楼层 |阅读模式
一、【Unity3D】协程Coroutine的运用


首先,在Unity3D的Update()的函数是不能被打断的,也就是说如下代码,如果绑定在任何一个对象上面,你的游戏将会被卡死,只能Ctrl+Alt+Delete立即结束了:
using UnityEngine;using System.Collections; public class MyCoroutine : MonoBehaviour{    private int a;    void Start()    {        a = 0;    }    void Update()    {        Debug.Log("0");        while (a == 0)        {            //去做些事情,然后做完让a!=0。        }        Debug.Log("1");    } }
本来我是打断,Update()函数你等我一下,然后处理一些事情,你读下面的代码,而我做完这些事情的标志就是让a不等于0。

可惜事与愿违,且不说Update()函数,每帧都被读取,也就说时刻在执行里面的代码,这个机制。单单是Unity3d是要读完Update()函数的代码,才会给你刷新一帧这个机制,已经足以让这游戏瞬间崩溃。因此,也启发了我,Update()尽可能地不要扔些循环给它做,里面顶多就放些条件判断好了,这样你的游戏才会流畅,才是所谓的“优化好”。

那么,爷确实有些比较耗时的任务,这怎办?那就通通开个子线程——协程Coroutine,别都写在主线程,Update()函数。
1.延迟执行某段代码

using UnityEngine;using System.Collections; public class MyCoroutine : MonoBehaviour{    void Start()    {        Debug.Log("0");        Debug.Log("1");        StartCoroutine(Thread1());    }     void Update()    {    }     IEnumerator Thread1()    {        yield return new WaitForSeconds(3.0f);        Debug.Log("2");    }}
yield return new WaitForSeconds(3.0f);这一句就是中断这线程3秒的意思,也就是在这行停3秒。并且,中断线程的语句,只能写在IEnumerator Thread1(){}这些协程里面,而不能写在Update()里面,因为Update()这个主线程根本不能被中断。
而开子线程Thread1,或者按照Unity3d的术语,应该说是 开协程Thread1的语句StartCoroutine(Thread1());应该放在只在开始执行一次的Start()里面,不然在Update()每帧都执行一次,子线程Thread1里面的程度,得开多少次啊?

另外,IEnumerator Thread1(){}在读完所有代码,自动死亡,会被系统的线程回收机制自动回收,我们自管开线程就行,其余的不用管!
2.每隔几秒执行某段代码


如果我不想每帧都执行某些代码,而是比如想每1秒i+1,初始=0的i,i++到10即停止,这又怎么办呢?你可以这样写:
using UnityEngine;using System.Collections; public class MyCoroutine : MonoBehaviour{    private int i;    void Start()    {        i = 0;        StartCoroutine(Thread1());    }     void Update()    {    }     IEnumerator Thread1()    {        while (true)        {            Debug.Log("i=" + i);            i++;            if (i > 10)            {                break;            }            yield return new WaitForSeconds(1.0f);        }    }}
这一段也很好理解,就是在Thread1中上个死循环,但死循环里面的代码并不是这么好读,读到 yield return new WaitForSeconds(1.0f);就要停顿1秒。读其余代码的时间可以忽略不计,因此,协程Coroutine配合一个有条件break的死循环,可以做到每隔几秒执行某段代码的效果。

但还是那句话,这一切通通都只能写到协程IEnumerator Thread1()里面,因为Update()不能停顿,游戏和动画一样,都是每一帧不停被刷新的页面。
3.同步


比如我想执行完某段代码,立即执行另一段代码,做到回调的效果,那该怎么办呢?

当然最简单就是在写完一段代码,在下一行写另一段代码。可是,如果这些代码不是立即完成的,需要等待,就要用到协程的同步。

比如,协程1需要耗时X秒,我并不知道,而协程2则需要在协程1之后马上执行,这又该怎么办呢?你可以这样写:
using UnityEngine;using System.Collections; public class MyCoroutine : MonoBehaviour{    private int i;    void Start()    {        i = 0;        StartCoroutine(Thread1());    }     void Update()    {    }     IEnumerator Thread1()    {        while (true)        {            Debug.Log("i=" + i);            i++;            if (i > 3)            {                break;            }            yield return new WaitForSeconds(1.0f);        }        Debug.Log("线程1已经完成了");        StartCoroutine(Thread2());    }     IEnumerator Thread2()    {        Debug.Log("线程2开始");        yield return null;//这句必须有,C#要求每个协程都要有yield return        //,虽然这句话看起来并没有什么卵用,但你就是要写-_-!    }}二、对yield return的理解


下面来看看两段显示人物对话的代码(对话随便复制了一段内容),功能是一样的,但是方法不一样:
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_easy : 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()?

参考Unity协程(Coroutine)原理深入剖析
协程不是线程,也不是异步执行的。协程和 MonoBehaviour 的 Update函数一样也是在MainThread中执行的。使用协程你不用考虑同步和锁的问题。
协程其实就是一个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让这个函数每帧都被调用了
三、Unity StartCoroutine 和 yield return 深入研究

public class MainTest : MonoBehaviour{    // Start is called before the first frame update    void Start()    {        Debug.Log("start1");        StartCoroutine(Test());        Debug.Log("start2");    }    IEnumerator Test()    {        Debug.Log("test1");        yield return null;        Debug.Log("test2");    }
运行结果是:
start1test1start2test2
当StartCoroutine刚调用的时候,可以理解为正常的函数调用,然后接着看调用的函数里面。当被调用函数执行到yield return null;(暂停协程,等待下一帧继续执行)时,根据Unity解释协同程序就会被暂停,其实我个人认为他这个解释不够精确,先返回开始协程的地方,然后再暂停协程。也就是先通知调用处,“你先走吧,不用管我”,然后再暂停协程。。这里如果把yeild return null改为yield return new WaitForSeconds(3);就可以看到test2是3秒之后才打印出来的。
四、Unity协程(一):彻底了解yield return null 和 yield return new WaitForSeconds


WaitForEndOfFrame,顾名思义是在等到本帧的帧末进行在进行处理

yield return null表示暂缓一帧,在下一帧接着往下处理,也有人习惯写成yield return 0或者yield return 1,于是误区就随之而来了,很多同学误认为yield return后面的数字表示的是帧率,比如yield return 10,表示的是延缓10帧再处理,实则不然,yield return num;的写法其实后面的数字是不起作用的,不管为多少,表示都是在下一帧接着处理。

yield return new WaitForSeconds,这个要注意的是1·实际时间等于给定的时间乘以Time.timeScale的值。2·触发间隔一定大等于1中计算出的实际时间,而且误差的大小取决于帧率,因为它是在每帧处理协程的时候去计算时间间隔是否满足条件,如果满足则继续执行。例如,当帧率为5的情况下,一帧的时间为200ms,这时即使时间参数再小,最快也要200ms之后才能继续执行剩余部分。

image.png

这是一张关于MonoBehaviour的执行顺序图关于协程的部分,由图可见,yield 是在yield WaitForSeconds之前处理的,再结合上段的分析可以得出一个结论:在同一帧里执行的两个协程,不论先后关系如何,不论WaitForSeconds给定的值为多少,yield return null所在的协程都要比yield return new WaitForSeconds的协程更先执行。同类型的协程则跟其开启的先后顺序相关

最后再提个点,yield return null和yield return new WaitForSeconds协程最好别一起混着用,特别是同时开启的这两个协程还有相互依赖的关系,因为帧率是不稳定的,所以有可能引起某些非必现的bug。
五、Unity 协程原理探究与实现

IEnumerator TestCoroutine(){    yield return null;              //返回内容为null    yield return 1;                 //返回内容为1    yield return "sss";             //返回内容为"sss"    yield break;                    //跳出,类似普通函数中的return语句    yield return 999;               //由于break语句,该内容无法返回}void Start(){    IEnumerator e = TestCoroutine();    while (e.MoveNext())    {        Debug.Log(e.Current);       //依次输出枚举接口返回的值    }}/* 枚举接口的定义public interface IEnumerator{    object Current    {        get;    }    bool MoveNext();    void Reset();}*//*运行结果:Null1sss*/
首先注意注释部分枚举接口的定义
Current属性为只读属性,返回枚举序列中的当前位的内容
MoveNext()把枚举器的位置前进到下一项,返回布尔值,新的位置若是有效的,返回true;否则返回false
Reset()将位置重置为原始状态

再看下Start函数中的代码,就是将yield return 语句中返回的值依次输出。
第一次MoveNext()后,Current位置指向了yield return 返回的null,该位置是有效的(这里注意区分位置有效和结果有效,位置有效是指当前位置是否有返回值,即使返回值是null;而结果有效是指返回值的结果是否为null,显然此处返回结果是无意义的)所以MoveNext()返回值是true;
第二次MoveNext()后,Current新位置指向了yield return 返回的1,该位置是有效的,MoveNext()返回true
第三次MoveNext()后,Current新位置指向了yield return 返回的"sss",该位置也是有效的,MoveNext()返回true
第四次MoveNext()后,Current新位置指向了yield break,无返回值,即位置无效,MoveNext()返回false,至此循环结束

先来回顾下Unity的协程具体有些功能:
    将协程代码中由yield return语句分割的部分分配到每一帧去执行。yield return 后的值是等待类(WaitForSeconds、WaitForFixedUpdate)时需要等待相应时间。yield return 后的值还是协程(Coroutine)时需要等待嵌套部分协程执行完毕才能执行接下来内容。
1.分帧


实现分帧执行之前,先将上述迭代器的代码简单修改下,看下输出结果
IEnumerator TestCoroutine(){    Debug.Log("TestCoroutine 1");    yield return null;    Debug.Log("TestCoroutine 2");    yield return 1;}void Start(){    IEnumerator e = TestCoroutine();    while (e.MoveNext())    {        Debug.Log(e.Current);       //依次输出枚举接口返回的值    }}/*运行结果TestCoroutine 1NullTestCoroutine 21*/
前面有说过,每次MoveNext()后会返回yield return后的内容,那yield return之前的语句怎么办呢?
当然也执行啊,遇到yield return语句之前的内容都会在MoveNext()时执行的。
到这里应该很清楚了,只要把MoveNext()移到每一帧去执行,不就实现分帧执行几段代码了么!

既然要分配在每一帧去执行,那当然就是Update和LateUpdate了。这里我个人喜欢将实现代码放在LateUpdate之中,为什么呢?因为Unity中协程的调用顺序是在Update之后,LateUpdate之前,所以这两个接口都不够准确;但在LateUpdate中处理,至少能保证协程是在所有脚本的Update执行完毕之后再去执行。


image.png

IEnumerator e = null;void Start(){    e = TestCoroutine();}void LateUpdate(){    if (e != null)    {        if (!e.MoveNext())        {            e = null;        }    }}IEnumerator TestCoroutine(){    Log("Test 1");    yield return null;              //返回内容为null    Log("Test 2");    yield return 1;                 //返回内容为1    Log("Test 3");    yield return "sss";             //返回内容为"sss"    Log("Test 4");    yield break;                    //跳出,类似普通函数中的return语句    Log("Test 5");    yield return 999;               //由于break语句,该内容无法返回}void Log(object msg){    Debug.LogFormat("<color=yellow>[{0}]</color>{1}", Time.frameCount, msg.ToString());}

image.png

再来看看运行结果,黄色中括号括起来的数字表示当前在第几帧,很明显我们的协程完成了每一帧执行一段代码的功能。
2.延时等待


要是完全理解了case1的内容,相信你自己就能完成“延时等待”这一功能,其实就是加了个计时器的判断嘛!
既然要识别自己的等待类,那当然要获取Current值根据其类型去判定是否需要等待。假如Current值是需要等待类型,那就延时到倒计时结束;而Current值是非等待类型,那就不需要等待,直接MoveNext()执行后续的代码即可。
这里着重说下“延时到倒计时结束”。既然知道Current值是需要等待的类型,那此时肯定不能在执行MoveNext()了,否则等待就没用了;接下来当等待时间到了,就可以继续MoveNext()了。可以简单的加个标志位去做这一判断,同时驱动MoveNext()的执行。
private void OnGUI(){    if (GUILayout.Button("Test"))       //注意:这里是点击触发,没有放在start里,为什么?    {        enumerator = TestCoroutine();    }}void LateUpdate(){    if (enumerator != null)    {        bool isNoNeedWait = true, isMoveOver = true;        var current = enumerator.Current;        if (current is MyWaitForSeconds)        {            MyWaitForSeconds waitable = current as MyWaitForSeconds;            isNoNeedWait = waitable.IsOver(Time.deltaTime);        }        if (isNoNeedWait)        {            isMoveOver = enumerator.MoveNext();        }        if (!isMoveOver)        {            enumerator = null;        }    }}IEnumerator TestCoroutine(){    Log("Test 1");    yield return null;              //返回内容为null    Log("Test 2");    yield return 1;                 //返回内容为1    Log("Test 3");    yield return new MyWaitForSeconds(2f);  //等待两秒               Log("Test 4");}

image.png

运行结果里黄色表示当前帧,青色是当前时间,很明显等待了2秒(虽然有少许误差但总体不影响)。
上述代码中,把函数触发放在了Button点击中而不是Start函数中?
这是因为我是用Time.deltaTime去做计时,假如放在了Start函数中,Time.deltaTime会受Awake这一帧执行时间影响,时间还不短(我测试时有0.1s左右),导致运行结果有很大误差,不到2秒就结束了,有兴趣的可以自己试一下~
六、从 各种点 理解Unity中的协程


什么是协同程序?什么是协程?
unity协程是一个能够暂停协程执行,暂停后立即返回主函数,执行主函数剩余的部分,直到中断指令完成后,从中断指令的下一行继续执行协程剩余的函数。函数体全部执行完成,协程结束。
由于中断指令的出现,使得可以将一个函数分割到多个帧里去执行。
性能:
在性能上相比于一般函数没有更多的开销
协程的好处:
让原来要使用异步 + 回调方式写的非人类代码, 可以用看似同步的方式写出来。
能够分步做一个比较耗时的事情,如果需要大量的计算,将计算放到一个随时间进行的协程来处理,能分散计算压力
协程的坏处:
协程本质是迭代器,且是基于unity生命周期的,大量开启协程会引起gc
如果同时激活的协程较多,就可能会出现多个高开销的协程挤在同一帧执行导致的卡帧
协程书写时的性能优化:
常见的问题是直接new 一个中断指令,带来不必要的 GC 负担,可以复用一个全局的中断指令对象,优化掉开销;在 Yielders.cs 这个文件里,已经集中地创建了上面这些类型的静态对象
这个链接分析了一下https://blog.csdn.net/liujunjie612/article/details/70623943
协程是在什么地方执行?
协程不是线程,不是异步执行;协程和monobehaviour的update函数一样也是在主线程中执行
unity在每一帧都会处理对象上的协程,也就是说,协程跟update一样都是unity每帧会去处理的函数
经过测试,协程至少是每帧的lateUpdate后运行的。
协程怎么结束?
方法一:StopCoroutine(string methodName);
方法二:stopAllCoroutines暂停的是当前脚本下的所有协程
方法三:gameObject.active = false 可以停止该对象上全部协程的执行,即使再次激活,也不能继续执行。但注意MonoBehaviour enabled = false 不能停止协程;对比 update却是可以在MonoBehaviour enabled = false 就中止
原因:由于协程在StartCoroutine时被注册到的GameObject上,他的生命期受限于GameObject的生命期,因此受GameObject是否active的影响。
结论:协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响。
协程结束的标志是什么?
如果最后一个 yield return 的 IEnumerator 已经迭代到最后一个是,MoveNext 就会 返回 false 。这时,Unity就会将这个 IEnumerator 从 cortoutines list 中移除。
只有当这个对象的 MoveNext() 返回 false 时,即该 IEnumertator 的 Current 已经迭代到最后一个元素了,才会执行 yield return 后面的语句。
中断函数类型:
null 在下一帧所有的Update()函数调用过之后执行

WaitForSeconds() 等待指定秒数,在该帧(延迟过后的那一帧)所有update()函数调用完后执行。即等待给定时间周期, 受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足。

WaitForFixedUpdate 等待一个固定帧,即等待物理周期循环结束后执行

WaitForEndOfFrame 等待帧结束,即等待渲染周期循环结束后执行

StartCoroutine 等待一个新协程暂停

WWW 等待一个加载完成,等待www的网络请求完成后,isDone=true后执行

协程的执行顺序:
开始协程->执行协程->遇到中断指令中断协程->返回上层函数继续执行上层函数的下一行代码->中断指令结束后,继续执行中断指令之后的代码->协程结束
协程可以嵌套协程吗?
可以,yield return StartCoroutine就是,执行顺序是:
子协程中断后,会返回父协程,父协程暂停,返回父协程的上级函数。
决定父协程结束的标志是子协程是否结束,当子协程结束后返回父协程执行其后的代码才算结束。
同一时刻同一脚本实例中能有多少个运行的协程?
在一个MonoBehaviour提供的主线程里只能有一个处于运行状态的协程。因为协程不是线程,不是并行的。同一时刻、一个脚本实例中可以有多个暂停的协程,但只有一个运行着的协程
协程和线程的区别?
线程是利用多核达到真正的并行计算,缺点是会有大量的锁、切换、等待的问题,而协程是非抢占式,需要用户自己释放使用权来切换到其他协程, 因此同一时间其实只有一个协程拥有运行权, 相当于单线程的能力。
协程是 C# 线程的替代品, 是 Unity 不使用线程的解决方案。
使用协程不用考虑同步和锁的问题
多个协程可以同时运行,它们会根据各自的启动顺序来更新
其他注意点:
1、IEnumerator 类型的方法不能带 ref 或者 out 型的参数,但可以带被传递的引用
2、在函数 Update 和 FixedUpdate 中不能使用 yield 语句,否则会报错, 但是可以启动协程
3、在一个协程中,StartCoroutine()和 yield return StartCoroutine()是不一样的。
前者仅仅是开始一个新的Coroutine,这个新的Coroutine和现有Coroutine并行执行。
后者是返回一个新的Coroutine,是一个中断指令,当这个新的Coroutine执行完毕后,才继承执行现有Coroutine。
七、实现自己的WaitForSeconds


在Unity中StartCoroutine/yield return这个模式到底是怎么应用的?其中的原理是什么?
Coroutine,你究竟干了什么?
Coroutine,你究竟干了什么?(小续)

WaitForSeconds本身是一个普通的类型,但是在StartCoroutine中,其被特殊对待了,一般而言,StartCoroutine就是简单的对某个IEnumerator 进行MoveNext()操作,但如果他发现IEnumerator其实是一个WaitForSeconds类型的话,那么他就会进行特殊等待,一直等到WaitForSeconds延时结束了,才进行正常的MoveNext调用,而至于WWW或者WaitForFixedUpdate等类型,StartCoroutine也是同样的特殊处理,如果用代码表示一下的话,大概是这个样子:
foreach(IEnumerator coroutine in coroutines){    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.        continue;    }     if(coroutine.Current is WaitForSeconds)    {        // update WaitForSeconds time value    }    else if(coroutine.Current is WaitForEndOfFrame)    {        // this iterator will MoveNext() at the end of the frame    }    else /* similar stuff for other YieldInstruction subtypes or WWW etc. */}2.嵌套

    IEnumerator UnityCoroutine()    {        Debug.Log("Unity coroutine begin at time : " + Time.time);        yield return new WaitForSeconds(2);        yield return StartCoroutine(InnerUnityCoroutine());        Debug.Log("Unity coroutine end at time : " + Time.time);    }    IEnumerator InnerUnityCoroutine()    {        Debug.Log("Inner Unity coroutine begin at time : " + Time.time);        yield return new WaitForSeconds(2);        Debug.Log("Inner Unity coroutine end at time : " + Time.time);    }    void Start()    {        StartCoroutine(UnityCoroutine());    }

image.png

“外层”的UnityCoroutine只有在“内层”的InnerUnityCoroutine“执行”完毕之后才会继续“执行”

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 04:48 , Processed in 0.092662 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表