Unity StartCoroutine

一捆愁、【Unity3D】協(xié)程Coroutine的運(yùn)用

首先穆趴,在Unity3D的Update()的函數(shù)是不能被打斷的脸爱,也就是說如下代碼,如果綁定在任何一個(gè)對象上面未妹,你的游戲?qū)豢ㄋ啦痉希荒蹸trl+Alt+Delete立即結(jié)束了:

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()函數(shù)你等我一下歪赢,然后處理一些事情,你讀下面的代碼导梆,而我做完這些事情的標(biāo)志就是讓a不等于0轨淌。

可惜事與愿違,且不說Update()函數(shù)看尼,每幀都被讀取递鹉,也就說時(shí)刻在執(zhí)行里面的代碼,這個(gè)機(jī)制媳拴。單單是Unity3d是要讀完Update()函數(shù)的代碼屈溉,才會給你刷新一幀這個(gè)機(jī)制偿荷,已經(jīng)足以讓這游戲瞬間崩潰小压。因此怠益,也啟發(fā)了我蜻牢,Update()盡可能地不要扔些循環(huán)給它做抢呆,里面頂多就放些條件判斷好了,這樣你的游戲才會流暢梯码,才是所謂的“優(yōu)化好”轩娶。

那么闯捎,爺確實(shí)有些比較耗時(shí)的任務(wù)瓤鼻,這怎辦茬祷?那就通通開個(gè)子線程——協(xié)程Coroutine,別都寫在主線程沃粗,Update()函數(shù)最盅。

1.延遲執(zhí)行某段代碼
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(){}這些協(xié)程里面锋喜,而不能寫在Update()里面段标,因?yàn)閁pdate()這個(gè)主線程根本不能被中斷逼庞。
而開子線程Thread1,或者按照Unity3d的術(shù)語砸逊,應(yīng)該說是 開協(xié)程Thread1的語句StartCoroutine(Thread1());應(yīng)該放在只在開始執(zhí)行一次的Start()里面穆咐,不然在Update()每幀都執(zhí)行一次对湃,子線程Thread1里面的程度,得開多少次安鹧丁种呐?

另外,IEnumerator Thread1(){}在讀完所有代碼阔墩,自動(dòng)死亡,會被系統(tǒng)的線程回收機(jī)制自動(dòng)回收忘苛,我們自管開線程就行蜀肘,其余的不用管西乖!

2.每隔幾秒執(zhí)行某段代碼

如果我不想每幀都執(zhí)行某些代碼薄腻,而是比如想每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中上個(gè)死循環(huán),但死循環(huán)里面的代碼并不是這么好讀弥搞,讀到 yield return new WaitForSeconds(1.0f);就要停頓1秒邮绿。讀其余代碼的時(shí)間可以忽略不計(jì),因此攀例,協(xié)程Coroutine配合一個(gè)有條件break的死循環(huán)船逮,可以做到每隔幾秒執(zhí)行某段代碼的效果。

但還是那句話粤铭,這一切通通都只能寫到協(xié)程IEnumerator Thread1()里面挖胃,因?yàn)閁pdate()不能停頓,游戲和動(dòng)畫一樣,都是每一幀不停被刷新的頁面赌躺。

3.同步

比如我想執(zhí)行完某段代碼悄泥,立即執(zhí)行另一段代碼,做到回調(diào)的效果瓢姻,那該怎么辦呢儡嘶?

當(dāng)然最簡單就是在寫完一段代碼,在下一行寫另一段代碼「幌遥可是,如果這些代碼不是立即完成的律杠,需要等待股耽,就要用到協(xié)程的同步盖矫。

比如证芭,協(xié)程1需要耗時(shí)X秒,我并不知道,而協(xié)程2則需要在協(xié)程1之后馬上執(zhí)行孙援,這又該怎么辦呢?你可以這樣寫:

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已經(jīng)完成了");
        StartCoroutine(Thread2());
    }
 
    IEnumerator Thread2()
    {
        Debug.Log("線程2開始");
        yield return null;//這句必須有吱瘩,C#要求每個(gè)協(xié)程都要有yield return
        //,雖然這句話看起來并沒有什么卵用盆色,但你就是要寫-_-强霎!
    }
}
二、對yield return的理解

下面來看看兩段顯示人物對話的代碼(對話隨便復(fù)制了一段內(nèi)容)惨好,功能是一樣的煌茴,但是方法不一樣:

 1 using UnityEngine;
 2 using System.Collections;
 3 
 4 public class dialog_easy : MonoBehaviour {
 5     public string dialogStr = "yield return的作用是在執(zhí)行到這行代碼之后,
將控制權(quán)立即交還給外部日川。yield return之后的代碼會在外部代碼再次
調(diào)用MoveNext時(shí)才會執(zhí)行蔓腐,直到下一個(gè)yield return——或是迭代結(jié)束。
雖然上面的代碼看似有個(gè)死循環(huán)龄句,但事實(shí)上在循環(huán)內(nèi)部我們始終會把控制權(quán)
交還給外部回论,這就由外部來決定何時(shí)中止這次迭代散罕。有了yield之后,我們
便可以利用“死循環(huán)”傀蓉,我們可以寫出含義明確的“無限的”斐波那契數(shù)列欧漱。";
 6     public float speed = 5.0f;
 7 
 8     private float timeSum = 0.0f;
 9     private bool isShowing = false;
10     // Use this for initialization
11     void Start () {
12         ShowDialog();
13     }
14     
15     // Update is called once per frame
16     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 }

這段代碼實(shí)現(xiàn)了在GUIText中逐漸顯示一個(gè)字符串的功能,速度為每秒5個(gè)字僚害,這也是新手常用的方式硫椰。如果只是簡單的在GUIText中顯示一段文字,ShowDialog()函數(shù)可以做的很好萨蚕;但是如果要讓字一個(gè)一個(gè)蹦出來靶草,就需要借助游戲的循環(huán)了,最簡單的方式就是在Update()中更新GUIText岳遥。

從功能角度看奕翔,這段代碼完全沒有問題;但是從代碼封裝性的角度來看浩蓉,這是一段很惡心的代碼派继,因?yàn)楸緫?yīng)由ShowDialog()完成的功能放到了Update()中,并且在類中還有兩個(gè)private變量為這個(gè)功能服務(wù)捻艳。如果將來要修改或者刪除這個(gè)功能驾窟,需要在ShowDialog()和Update()中修改,并且還可能修改那兩個(gè)private變量∪瞎欤現(xiàn)在代碼比較簡單绅络,感覺還不算太壞,一旦Update()中再來兩個(gè)類似的的功能嘁字,估計(jì)寫完代碼一段時(shí)間之后自己修改都費(fèi)勁恩急。

如果通過yield return null實(shí)現(xiàn)幀與幀之間的同步,則代碼優(yōu)雅了很多:

 1 using UnityEngine;
 2 using System.Collections;
 3 
 4 public class dialog_easy : MonoBehaviour {
 5     public string dialogStr = "yield return的作用是在執(zhí)行到這行代碼之后纪蜒,
將控制權(quán)立即交還給外部衷恭。yield return之后的代碼會在外部代碼再次
調(diào)用MoveNext時(shí)才會執(zhí)行,直到下一個(gè)yield return——或是迭代結(jié)束纯续。
雖然上面的代碼看似有個(gè)死循環(huán)随珠,但事實(shí)上在循環(huán)內(nèi)部我們始終會把控制權(quán)
交還給外部,這就由外部來決定何時(shí)中止這次迭代杆烁。有了yield之后牙丽,我們
便可以利用“死循環(huán)”,我們可以寫出含義明確的“無限的”斐波那契數(shù)列兔魂。";
 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 frame
14     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 }

相關(guān)代碼都被封裝到了ShowDialog()中烤芦,這么一來,不論是要增加析校、修改或刪除功能构罗,都變得容易了很多铜涉。根據(jù)官網(wǎng)手冊的描述,yield return null可以讓這段代碼在下一幀繼續(xù)執(zhí)行遂唧。在ShowDialog()中芙代,每次更新文字以后yield return null,直到這段文字被完整顯示盖彭∥婆耄看到這里,可能有童鞋不解:

  • 為什么在協(xié)程中也可以用Time.deltaTime召边?
  • 協(xié)程中的Time.deltaTime和Update()中的一樣嗎铺呵?
  • 這樣使用協(xié)程,會不會出現(xiàn)與主線程訪問共享資源沖突的問題隧熙?(線程的同步與互斥問題)
  • yield return null太神奇了片挂,為什么會在下一幀繼續(xù)執(zhí)行這個(gè)函數(shù)?
  • 這段代碼是不是相當(dāng)于為ShowDialog()構(gòu)造了一個(gè)自己的Update()贞盯?

參考Unity協(xié)程(Coroutine)原理深入剖析

協(xié)程不是線程音念,也不是異步執(zhí)行的。協(xié)程和 MonoBehaviour 的 Update函數(shù)一樣也是在MainThread中執(zhí)行的躏敢。使用協(xié)程你不用考慮同步和鎖的問題闷愤。
協(xié)程其實(shí)就是一個(gè)IEnumerator(迭代器),IEnumerator 接口有兩個(gè)方法 Current 和 MoveNext() 件余,迭代器方法運(yùn)行到 yield return 語句時(shí)肝谭,會返回一個(gè)expression表達(dá)式并保留當(dāng)前在代碼中的位置。 當(dāng)下次調(diào)用迭代器函數(shù)時(shí)執(zhí)行從該位置重新啟動(dòng)蛾扇。unity3d在每幀做的工作就是:調(diào)用協(xié)程(迭代器)MoveNext() 方法,如果返回 true 魏滚,就從當(dāng)前位置繼續(xù)往下執(zhí)行镀首。

  • 協(xié)程和Update()一樣更新,自然可以使用Time.deltaTime了鼠次,而且這個(gè)Time.deltaTime和在Update()當(dāng)中使用是一樣的效果(使用yield return null的情況下)
  • 協(xié)程并不是多線程更哄,它和Update()一樣是在主線程中執(zhí)行的,所以不需要處理線程的同步與互斥問題
  • yield return null其實(shí)沒什么神奇的腥寇,只是unity3d封裝以后成翩,這個(gè)協(xié)程在下一幀就被自動(dòng)調(diào)用了
  • 可以理解為ShowDialog()構(gòu)造了一個(gè)自己的Update(),因?yàn)閥ield return null讓這個(gè)函數(shù)每幀都被調(diào)用了
三赦役、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");
    }

運(yùn)行結(jié)果是:

start1
test1
start2
test2

當(dāng)StartCoroutine剛調(diào)用的時(shí)候麻敌,可以理解為正常的函數(shù)調(diào)用,然后接著看調(diào)用的函數(shù)里面掂摔。當(dāng)被調(diào)用函數(shù)執(zhí)行到y(tǒng)ield return null术羔;(暫停協(xié)程赢赊,等待下一幀繼續(xù)執(zhí)行)時(shí),根據(jù)Unity解釋協(xié)同程序就會被暫停级历,其實(shí)我個(gè)人認(rèn)為他這個(gè)解釋不夠精確释移,先返回開始協(xié)程的地方,然后再暫停協(xié)程寥殖。也就是先通知調(diào)用處玩讳,“你先走吧,不用管我”嚼贡,然后再暫停協(xié)程熏纯。。這里如果把yeild return null改為yield return new WaitForSeconds(3);就可以看到test2是3秒之后才打印出來的编曼。

四豆巨、Unity協(xié)程(一):徹底了解yield return null 和 yield return new WaitForSeconds

WaitForEndOfFrame,顧名思義是在等到本幀的幀末進(jìn)行在進(jìn)行處理

yield return null表示暫緩一幀掐场,在下一幀接著往下處理往扔,也有人習(xí)慣寫成yield return 0或者yield return 1,于是誤區(qū)就隨之而來了熊户,很多同學(xué)誤認(rèn)為yield return后面的數(shù)字表示的是幀率萍膛,比如yield return 10,表示的是延緩10幀再處理嚷堡,實(shí)則不然蝗罗,yield return num;的寫法其實(shí)后面的數(shù)字是不起作用的,不管為多少蝌戒,表示都是在下一幀接著處理串塑。

yield return new WaitForSeconds,這個(gè)要注意的是1·實(shí)際時(shí)間等于給定的時(shí)間乘以Time.timeScale的值北苟。2·觸發(fā)間隔一定大等于1中計(jì)算出的實(shí)際時(shí)間桩匪,而且誤差的大小取決于幀率,因?yàn)樗窃诿繋幚韰f(xié)程的時(shí)候去計(jì)算時(shí)間間隔是否滿足條件友鼻,如果滿足則繼續(xù)執(zhí)行傻昙。例如,當(dāng)幀率為5的情況下彩扔,一幀的時(shí)間為200ms妆档,這時(shí)即使時(shí)間參數(shù)再小,最快也要200ms之后才能繼續(xù)執(zhí)行剩余部分虫碉。

image.png

這是一張關(guān)于MonoBehaviour的執(zhí)行順序圖關(guān)于協(xié)程的部分贾惦,由圖可見,yield 是在yield WaitForSeconds之前處理的,再結(jié)合上段的分析可以得出一個(gè)結(jié)論:在同一幀里執(zhí)行的兩個(gè)協(xié)程纤虽,不論先后關(guān)系如何乳绕,不論WaitForSeconds給定的值為多少,yield return null所在的協(xié)程都要比yield return new WaitForSeconds的協(xié)程更先執(zhí)行逼纸。同類型的協(xié)程則跟其開啟的先后順序相關(guān)

最后再提個(gè)點(diǎn)洋措,yield return null和yield return new WaitForSeconds協(xié)程最好別一起混著用,特別是同時(shí)開啟的這兩個(gè)協(xié)程還有相互依賴的關(guān)系杰刽,因?yàn)閹适遣环€(wěn)定的菠发,所以有可能引起某些非必現(xiàn)的bug。

五贺嫂、Unity 協(xié)程原理探究與實(shí)現(xiàn)
IEnumerator TestCoroutine()
{
    yield return null;              //返回內(nèi)容為null

    yield return 1;                 //返回內(nèi)容為1

    yield return "sss";             //返回內(nèi)容為"sss"

    yield break;                    //跳出滓鸠,類似普通函數(shù)中的return語句

    yield return 999;               //由于break語句,該內(nèi)容無法返回
}

void Start()
{
    IEnumerator e = TestCoroutine();
    while (e.MoveNext())
    {
        Debug.Log(e.Current);       //依次輸出枚舉接口返回的值
    }
}
/* 枚舉接口的定義
public interface IEnumerator
{
    object Current
    {
        get;
    }

    bool MoveNext();

    void Reset();
}*/

/*運(yùn)行結(jié)果:
Null
1
sss
*/

首先注意注釋部分枚舉接口的定義
Current屬性為只讀屬性第喳,返回枚舉序列中的當(dāng)前位的內(nèi)容
MoveNext()把枚舉器的位置前進(jìn)到下一項(xiàng)糜俗,返回布爾值,新的位置若是有效的曲饱,返回true悠抹;否則返回false
Reset()將位置重置為原始狀態(tài)

再看下Start函數(shù)中的代碼,就是將yield return 語句中返回的值依次輸出扩淀。
第一次MoveNext()后楔敌,Current位置指向了yield return 返回的null,該位置是有效的(這里注意區(qū)分位置有效和結(jié)果有效驻谆,位置有效是指當(dāng)前位置是否有返回值卵凑,即使返回值是null;而結(jié)果有效是指返回值的結(jié)果是否為null胜臊,顯然此處返回結(jié)果是無意義的)所以MoveNext()返回值是true勺卢;
第二次MoveNext()后,Current新位置指向了yield return 返回的1象对,該位置是有效的值漫,MoveNext()返回true
第三次MoveNext()后,Current新位置指向了yield return 返回的"sss"织盼,該位置也是有效的,MoveNext()返回true
第四次MoveNext()后酱塔,Current新位置指向了yield break沥邻,無返回值,即位置無效羊娃,MoveNext()返回false唐全,至此循環(huán)結(jié)束

先來回顧下Unity的協(xié)程具體有些功能:

  • 將協(xié)程代碼中由yield return語句分割的部分分配到每一幀去執(zhí)行。
  • yield return 后的值是等待類(WaitForSecondsWaitForFixedUpdate)時(shí)需要等待相應(yīng)時(shí)間邮利。
  • yield return 后的值還是協(xié)程(Coroutine)時(shí)需要等待嵌套部分協(xié)程執(zhí)行完畢才能執(zhí)行接下來內(nèi)容弥雹。
1.分幀

實(shí)現(xiàn)分幀執(zhí)行之前,先將上述迭代器的代碼簡單修改下延届,看下輸出結(jié)果

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);       //依次輸出枚舉接口返回的值
    }
}
/*運(yùn)行結(jié)果
TestCoroutine 1
Null
TestCoroutine 2
1
*/

前面有說過剪勿,每次MoveNext()后會返回yield return后的內(nèi)容,那yield return之前的語句怎么辦呢方庭?
當(dāng)然也執(zhí)行啊厕吉,遇到y(tǒng)ield return語句之前的內(nèi)容都會在MoveNext()時(shí)執(zhí)行的。
到這里應(yīng)該很清楚了械念,只要把MoveNext()移到每一幀去執(zhí)行头朱,不就實(shí)現(xiàn)分幀執(zhí)行幾段代碼了么!

既然要分配在每一幀去執(zhí)行龄减,那當(dāng)然就是Update和LateUpdate了项钮。這里我個(gè)人喜歡將實(shí)現(xiàn)代碼放在LateUpdate之中,為什么呢希停?因?yàn)閁nity中協(xié)程的調(diào)用順序是在Update之后烁巫,LateUpdate之前,所以這兩個(gè)接口都不夠準(zhǔn)確脖苏;但在LateUpdate中處理程拭,至少能保證協(xié)程是在所有腳本的Update執(zhí)行完畢之后再去執(zhí)行。


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;              //返回內(nèi)容為null
    Log("Test 2");
    yield return 1;                 //返回內(nèi)容為1
    Log("Test 3");
    yield return "sss";             //返回內(nèi)容為"sss"
    Log("Test 4");
    yield break;                    //跳出棍潘,類似普通函數(shù)中的return語句
    Log("Test 5");
    yield return 999;               //由于break語句恃鞋,該內(nèi)容無法返回
}

void Log(object msg)
{
    Debug.LogFormat("<color=yellow>[{0}]</color>{1}", Time.frameCount, msg.ToString());
}
image.png

再來看看運(yùn)行結(jié)果,黃色中括號括起來的數(shù)字表示當(dāng)前在第幾幀亦歉,很明顯我們的協(xié)程完成了每一幀執(zhí)行一段代碼的功能恤浪。

2.延時(shí)等待

要是完全理解了case1的內(nèi)容,相信你自己就能完成“延時(shí)等待”這一功能肴楷,其實(shí)就是加了個(gè)計(jì)時(shí)器的判斷嘛水由!
既然要識別自己的等待類,那當(dāng)然要獲取Current值根據(jù)其類型去判定是否需要等待赛蔫。假如Current值是需要等待類型砂客,那就延時(shí)到倒計(jì)時(shí)結(jié)束;而Current值是非等待類型呵恢,那就不需要等待鞠值,直接MoveNext()執(zhí)行后續(xù)的代碼即可。
這里著重說下“延時(shí)到倒計(jì)時(shí)結(jié)束”渗钉。既然知道Current值是需要等待的類型彤恶,那此時(shí)肯定不能在執(zhí)行MoveNext()了钞钙,否則等待就沒用了;接下來當(dāng)?shù)却龝r(shí)間到了声离,就可以繼續(xù)MoveNext()了芒炼。可以簡單的加個(gè)標(biāo)志位去做這一判斷术徊,同時(shí)驅(qū)動(dòng)MoveNext()的執(zhí)行本刽。

private void OnGUI()
{
    if (GUILayout.Button("Test"))       //注意:這里是點(diǎn)擊觸發(fā),沒有放在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;              //返回內(nèi)容為null
    Log("Test 2");
    yield return 1;                 //返回內(nèi)容為1
    Log("Test 3");
    yield return new MyWaitForSeconds(2f);  //等待兩秒           
    Log("Test 4");
}
image.png

運(yùn)行結(jié)果里黃色表示當(dāng)前幀,青色是當(dāng)前時(shí)間世囊,很明顯等待了2秒(雖然有少許誤差但總體不影響)别瞭。
上述代碼中,把函數(shù)觸發(fā)放在了Button點(diǎn)擊中而不是Start函數(shù)中株憾?
這是因?yàn)槲沂怯肨ime.deltaTime去做計(jì)時(shí)蝙寨,假如放在了Start函數(shù)中,Time.deltaTime會受Awake這一幀執(zhí)行時(shí)間影響嗤瞎,時(shí)間還不短(我測試時(shí)有0.1s左右)墙歪,導(dǎo)致運(yùn)行結(jié)果有很大誤差,不到2秒就結(jié)束了贝奇,有興趣的可以自己試一下~

六虹菲、從 各種點(diǎn) 理解Unity中的協(xié)程

什么是協(xié)同程序?什么是協(xié)程掉瞳?
unity協(xié)程是一個(gè)能夠暫停協(xié)程執(zhí)行毕源,暫停后立即返回主函數(shù),執(zhí)行主函數(shù)剩余的部分陕习,直到中斷指令完成后霎褐,從中斷指令的下一行繼續(xù)執(zhí)行協(xié)程剩余的函數(shù)。函數(shù)體全部執(zhí)行完成该镣,協(xié)程結(jié)束冻璃。
由于中斷指令的出現(xiàn),使得可以將一個(gè)函數(shù)分割到多個(gè)幀里去執(zhí)行损合。
性能:
在性能上相比于一般函數(shù)沒有更多的開銷
協(xié)程的好處:
讓原來要使用異步 + 回調(diào)方式寫的非人類代碼, 可以用看似同步的方式寫出來省艳。
能夠分步做一個(gè)比較耗時(shí)的事情,如果需要大量的計(jì)算嫁审,將計(jì)算放到一個(gè)隨時(shí)間進(jìn)行的協(xié)程來處理跋炕,能分散計(jì)算壓力
協(xié)程的壞處:
協(xié)程本質(zhì)是迭代器,且是基于unity生命周期的土居,大量開啟協(xié)程會引起gc
如果同時(shí)激活的協(xié)程較多,就可能會出現(xiàn)多個(gè)高開銷的協(xié)程擠在同一幀執(zhí)行導(dǎo)致的卡幀
協(xié)程書寫時(shí)的性能優(yōu)化:
常見的問題是直接new 一個(gè)中斷指令,帶來不必要的 GC 負(fù)擔(dān)擦耀,可以復(fù)用一個(gè)全局的中斷指令對象棉圈,優(yōu)化掉開銷;在 Yielders.cs 這個(gè)文件里眷蜓,已經(jīng)集中地創(chuàng)建了上面這些類型的靜態(tài)對象
這個(gè)鏈接分析了一下https://blog.csdn.net/liujunjie612/article/details/70623943
協(xié)程是在什么地方執(zhí)行分瘾?
協(xié)程不是線程,不是異步執(zhí)行吁系;協(xié)程和monobehaviour的update函數(shù)一樣也是在主線程中執(zhí)行
unity在每一幀都會處理對象上的協(xié)程德召,也就是說,協(xié)程跟update一樣都是unity每幀會去處理的函數(shù)
經(jīng)過測試汽纤,協(xié)程至少是每幀的lateUpdate后運(yùn)行的上岗。
協(xié)程怎么結(jié)束?
方法一:StopCoroutine(string methodName);
方法二:stopAllCoroutines暫停的是當(dāng)前腳本下的所有協(xié)程
方法三:gameObject.active = false 可以停止該對象上全部協(xié)程的執(zhí)行蕴坪,即使再次激活肴掷,也不能繼續(xù)執(zhí)行。但注意MonoBehaviour enabled = false 不能停止協(xié)程背传;對比 update卻是可以在MonoBehaviour enabled = false 就中止
原因:由于協(xié)程在StartCoroutine時(shí)被注冊到的GameObject上呆瞻,他的生命期受限于GameObject的生命期,因此受GameObject是否active的影響径玖。
結(jié)論:協(xié)程雖然是在MonoBehvaviour啟動(dòng)的(StartCoroutine)但是協(xié)程函數(shù)的地位完全是跟MonoBehaviour是一個(gè)層次的痴脾,不受MonoBehaviour的狀態(tài)影響。
協(xié)程結(jié)束的標(biāo)志是什么梳星?
如果最后一個(gè) yield return 的 IEnumerator 已經(jīng)迭代到最后一個(gè)是赞赖,MoveNext 就會 返回 false 。這時(shí)丰泊,Unity就會將這個(gè) IEnumerator 從 cortoutines list 中移除薯定。
只有當(dāng)這個(gè)對象的 MoveNext() 返回 false 時(shí),即該 IEnumertator 的 Current 已經(jīng)迭代到最后一個(gè)元素了瞳购,才會執(zhí)行 yield return 后面的語句话侄。
中斷函數(shù)類型:
null 在下一幀所有的Update()函數(shù)調(diào)用過之后執(zhí)行

WaitForSeconds() 等待指定秒數(shù),在該幀(延遲過后的那一幀)所有update()函數(shù)調(diào)用完后執(zhí)行学赛。即等待給定時(shí)間周期年堆, 受Time.timeScale影響,當(dāng)Time.timeScale = 0f 時(shí)盏浇,yield return new WaitForSecond(x) 將不會滿足变丧。

WaitForFixedUpdate 等待一個(gè)固定幀,即等待物理周期循環(huán)結(jié)束后執(zhí)行

WaitForEndOfFrame 等待幀結(jié)束绢掰,即等待渲染周期循環(huán)結(jié)束后執(zhí)行

StartCoroutine 等待一個(gè)新協(xié)程暫停

WWW 等待一個(gè)加載完成痒蓬,等待www的網(wǎng)絡(luò)請求完成后童擎,isDone=true后執(zhí)行

協(xié)程的執(zhí)行順序:
開始協(xié)程->執(zhí)行協(xié)程->遇到中斷指令中斷協(xié)程->返回上層函數(shù)繼續(xù)執(zhí)行上層函數(shù)的下一行代碼->中斷指令結(jié)束后,繼續(xù)執(zhí)行中斷指令之后的代碼->協(xié)程結(jié)束
協(xié)程可以嵌套協(xié)程嗎攻晒?
可以顾复,yield return StartCoroutine就是,執(zhí)行順序是:
子協(xié)程中斷后鲁捏,會返回父協(xié)程芯砸,父協(xié)程暫停,返回父協(xié)程的上級函數(shù)给梅。
決定父協(xié)程結(jié)束的標(biāo)志是子協(xié)程是否結(jié)束褥影,當(dāng)子協(xié)程結(jié)束后返回父協(xié)程執(zhí)行其后的代碼才算結(jié)束岩喷。
同一時(shí)刻同一腳本實(shí)例中能有多少個(gè)運(yùn)行的協(xié)程吓揪?
在一個(gè)MonoBehaviour提供的主線程里只能有一個(gè)處于運(yùn)行狀態(tài)的協(xié)程镣典。因?yàn)閰f(xié)程不是線程,不是并行的曹质。同一時(shí)刻婴噩、一個(gè)腳本實(shí)例中可以有多個(gè)暫停的協(xié)程,但只有一個(gè)運(yùn)行著的協(xié)程
協(xié)程和線程的區(qū)別羽德?
線程是利用多核達(dá)到真正的并行計(jì)算几莽,缺點(diǎn)是會有大量的鎖、切換宅静、等待的問題章蚣,而協(xié)程是非搶占式,需要用戶自己釋放使用權(quán)來切換到其他協(xié)程, 因此同一時(shí)間其實(shí)只有一個(gè)協(xié)程擁有運(yùn)行權(quán), 相當(dāng)于單線程的能力姨夹。
協(xié)程是 C# 線程的替代品, 是 Unity 不使用線程的解決方案纤垂。
使用協(xié)程不用考慮同步和鎖的問題
多個(gè)協(xié)程可以同時(shí)運(yùn)行,它們會根據(jù)各自的啟動(dòng)順序來更新
其他注意點(diǎn):
1磷账、IEnumerator 類型的方法不能帶 ref 或者 out 型的參數(shù)峭沦,但可以帶被傳遞的引用
2、在函數(shù) Update 和 FixedUpdate 中不能使用 yield 語句逃糟,否則會報(bào)錯(cuò)吼鱼, 但是可以啟動(dòng)協(xié)程
3、在一個(gè)協(xié)程中绰咽,StartCoroutine()和 yield return StartCoroutine()是不一樣的菇肃。
前者僅僅是開始一個(gè)新的Coroutine,這個(gè)新的Coroutine和現(xiàn)有Coroutine并行執(zhí)行取募。
后者是返回一個(gè)新的Coroutine琐谤,是一個(gè)中斷指令,當(dāng)這個(gè)新的Coroutine執(zhí)行完畢后玩敏,才繼承執(zhí)行現(xiàn)有Coroutine斗忌。

七质礼、實(shí)現(xiàn)自己的WaitForSeconds

在Unity中StartCoroutine/yield return這個(gè)模式到底是怎么應(yīng)用的?其中的原理是什么织阳?
Coroutine几苍,你究竟干了什么?
Coroutine陈哑,你究竟干了什么?(小續(xù))

WaitForSeconds本身是一個(gè)普通的類型伸眶,但是在StartCoroutine中惊窖,其被特殊對待了,一般而言厘贼,StartCoroutine就是簡單的對某個(gè)IEnumerator 進(jìn)行MoveNext()操作界酒,但如果他發(fā)現(xiàn)IEnumerator其實(shí)是一個(gè)WaitForSeconds類型的話,那么他就會進(jìn)行特殊等待嘴秸,一直等到WaitForSeconds延時(shí)結(jié)束了毁欣,才進(jìn)行正常的MoveNext調(diào)用,而至于WWW或者WaitForFixedUpdate等類型岳掐,StartCoroutine也是同樣的特殊處理凭疮,如果用代碼表示一下的話,大概是這個(gè)樣子:

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只有在“內(nèi)層”的InnerUnityCoroutine“執(zhí)行”完畢之后才會繼續(xù)“執(zhí)行”

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末串述,一起剝皮案震驚了整個(gè)濱河市执解,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纲酗,老刑警劉巖衰腌,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異觅赊,居然都是意外死亡右蕊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門吮螺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饶囚,“玉大人,你說我怎么就攤上這事规脸∨髟迹” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵莫鸭,是天一觀的道長闹丐。 經(jīng)常有香客問我,道長被因,這世上最難降的妖魔是什么卿拴? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任衫仑,我火速辦了婚禮,結(jié)果婚禮上堕花,老公的妹妹穿的比我還像新娘文狱。我一直安慰自己,他們只是感情好缘挽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布瞄崇。 她就那樣靜靜地躺著,像睡著了一般壕曼。 火紅的嫁衣襯著肌膚如雪苏研。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天腮郊,我揣著相機(jī)與錄音摹蘑,去河邊找鬼。 笑死轧飞,一個(gè)胖子當(dāng)著我的面吹牛衅鹿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播过咬,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼大渤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了掸绞?” 一聲冷哼從身側(cè)響起兼犯,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎集漾,沒想到半個(gè)月后切黔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡具篇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年纬霞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驱显。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡诗芜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出埃疫,到底是詐尸還是另有隱情伏恐,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布栓霜,位于F島的核電站翠桦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜销凑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一丛晌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斗幼,春花似錦澎蛛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至桐经,卻和暖如春斤贰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背次询。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓷叫,地道東北人屯吊。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像摹菠,于是被迫代替她去往敵國和親盒卸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容