天天看點

Unity3d中協程的原理,你要的yield return new xxx的真正了解之道之前還是要說一下疊代器c#裡的yield return回到unity3d應用之二強行總分總

之前

之前看了一天的部落格,各種文章巴拉巴拉,又說到疊代器了,又貼代碼了,看的我頭都暈了,還是啥都不懂。最後答案還是在微軟C#的官網找到了,可喜可賀,故發上來給大家看看,興許能賺個幾百評論呢(并沒有)?

還是要說一下疊代器

foreach(a in list)是怎麼實作的呢?

in的其實不是list本身,而是list裡面的一個疊代器。疊代器一般而言會實作以下兩個方法:

bool Next()

還有

object Current()

。在foreach過程中,in會先調用next方法,這個時候假如有下一個值可以傳回,那就把指針(之類的)指到那個object上面去,然後next方法會傳回一個true說,大佬你可取了,最後in調用Current方法,把東西拿走,該列印列印,做愛做的事。當next傳回一個false,好了沒東西拿了,foreach給我結束吧,然後就結束了這個循環。

c#裡的yield return

這裡我就不說我探索的過程那些廢話啦,直接貼微軟官網的意思。先來一段代碼吧,

foreach(string s in GetAllStrings()){
    print(string);
    print("string printed");
}

IEnumerator<string> GetAllStrings(){
    for(int i = ; i < ; i++){
        yield return "str" + i;
    }
}
           

讓我們看看GetAllStrings方法,這裡普通人的了解肯定是,為啥傳回類型是個IEnumerator,但是yield return的卻是一個string??

好了不用猜了,微軟的文檔說了,in GetAllStrings()并不是在執行GetAllStrings這個方法,而實質上是把這個方法體用一個疊代器抱起來了。

還記得前面說的疊代器的兩個方法麼?這裡編譯器對代碼做了點小處理,我實在是懶得去看了,是以隻能告訴你結論,就是當每次in操作外面執行next和current之後,GetAllStrings這裡面就會自動執行到下一句yield return語句,把你要的東西傳到外邊去,然後停住,注意,會馬上停住,不會往下執行了。

然後呢?

然後這一次的内容外面的foreach拿到了,做了愛做的事兒,然後繼續循環他,調他的next和current,于是方法體内的代碼繼續執行,由于yield return我下面沒寫啥語句,是以會繼續for循環,i從0變成1,然後又傳回一個值。假如我yield return下面寫了東西了,就會在下一次的疊代中被執行啦。

再看一下微軟的示例代碼,一個醜陋的疊代器也可以這麼寫

IEnumerator<string> GetAllStrings(){
    yield return "str" + ;
    //第一次執行foreach操作之後就此打住
    yield return "str" + ;
    //第二次執行之後就此打住
    yield return "str" + ;
}
           

回到unity3d

從上面的分析,有些同學已經可以猜到了某個用法,對了,就是自己手動調用next和current來控制程式的執行。假如調了第一次的foreach操作之後,我們用某種方式讓程式n秒後再調用這個foreach操作,不就可以在五秒後再執行yield return 後面的語句了嗎?

沒有錯!u3d就是這麼妙,就是用了這麼妙的設計(妙啊.jpg)。

首先,一個遊戲引擎每一幀會調用一次update這就不用我講了吧,如果你連這個也不知道,emmmm…不如Ctrl+w走一個?

設想這種情形:

StartCoroutine(foobar());

IEnumerator foobar(){
    yield return WaitForSeconds();
    print("hello qiangpozheng");
}
           

這個時候引擎做了啥事呢?首先來看一下這個WaitForSeconds的類,他和WWW以及其他某些類一樣,繼承了個YieldInstucment接口,這個接口裡面有個方法

bool keepWaiting()

傳回一個布爾值。首先,程式把foobar這個疊代器給了引擎。引擎接到了這個疊代器,二話不說先疊代他一次,得到一個WaitForSeconds,保住,存起來。下一幀的update到啦,update之後不打豆豆,update之後問一下那個疊代器傳回的WaitForSecond的keepWaiting方法,還要不要繼續等呀。此時兩種結果,一種是時間還沒到,傳回false,那好這幀不關它事了。而…

YieldInstrucment傳回了true啦

可以卷錢跑了。

ok,那代表我條件達成啦。這擱在WaitForSeconds是時間到了,擱在Resource是資源載入完成了,擱在WWW就是網絡操作搞定啦。

題外話: 一般别的語言我們的操作就是搞定了,回調一下剛才傳進來的函數吧,沒錯這邊的操作也很像。

好了傳回true了,這個時候引擎把剛才存下來又黑又亮的寶貝疊代器掏了出來,然後執行一次foreach。如果你腦袋還算靈光沒忘記我在上文講的東西的話,你就會發現yield return的代碼,被執行啦。

好了,至此,WaitForSeconds的原理就此結束啦。

好玩的應用

假設我們需要在遊戲裡每一幀執行某個操作,直至某個條件失效,沒有協程我們一般是怎麼寫的呢?

void update(){
    if(_goOnUpdate){
        //巴拉巴拉
        if(bbb < ){
            //在某個條件下結束
            _goOnUpdate = false;
        }
    }
}
           

這麼寫兩個問題,一個是繁瑣,程式員最讨厭的事。第二個,就算你_goOnUpdate為false了,照樣每一幀要check一次這個布爾值,看着煩。

現在用剛才學到的知識寫,可以怎麼寫?

StartCoroutine(foobar());
//求不要吐槽我偶爾的小駝峰命名,我剛從Java那疙瘩過來的
IEnumerator foobar(){
    while(true){
        // do sth. xxx可以是随便啥值,我試過好像就算是null也沒問題
        yield return xxx;
        if(bbb < ){
            yield break;
        }
    }
}
           

這裡不用while true循環行不行呢?我以前也想過這個問題,不行。程式當是這樣,當你執行完本幀想做的操作之後, yield return,于是這段代碼被暫停,然後下一幀(為啥是下一幀?因為你傳回的是null,不是YieldInstrucment或者别的東西,沒有上面所說的那個keepWaiting)系統又繼續從下一句調用,執行下面的語句,如果達到某個條件yield break(之前忘了講這個方法可以跳出疊代)結束,如果不符合條件,那繼續執行。繼續執行意味着走到while體的最後一行,然後由于是while true,會重新跳到wihle體的第一行,然後又是執行第一行和yield return之間的代碼。

總結,這樣子就可以每一幀都執行一次啦~

當然如果你要每一幀都調用的話,那就别寫yield break啦。

應用之二

設想我們需要在背景開個線程做什麼東西,然後更改UI。更改UI是絕對不允許在主線程之外做的,是以我們根據上面的知識,實作

CustomYieldInstructment

接口來實作。好了寫的好累了,直接貼代碼。順便說一句這個代碼是不能用的,因為AssetDatabase不允許在非主線程調用。

#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System.Threading;
using UnityEngine;
#endif
public class AssetDatabaseThreadHolder {

    #if UNITY_EDITOR

    string _url;
    public bool IsDone;
    public Texture2D ResourceObject{ get; set;}

    public AssetDatabaseThreadHolder (string url){
        _url = url;
    }

    public void StartThread(){
        new Thread (GetResource).Start ();
    }

    private void GetResource(){
        IsDone = false;
        ResourceObject = AssetDatabase.LoadAssetAtPath<Texture2D>(_url);
        IsDone = true;
    }

    #endif
}
           

然後是實作接口的地方

#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
public class AssetDatabaseAsync:CustomYieldInstruction
{
    #if UNITY_EDITOR
    private AssetDatabaseThreadHolder _threadHolder; 

    public AssetDatabaseAsync(string url){
        _threadHolder = new AssetDatabaseThreadHolder (url);
        _threadHolder.StartThread ();
    }

    public override bool keepWaiting{
        get{
            return !(_threadHolder.IsDone);
        }
    }

    public Texture2D GetResourceObject{
        get{
            return _threadHolder.ResourceObject;
        }

    }

    #else
    public override bool keepWaiting{
        get{
            return true;
        }
    }
    #endif
}
           

最後是調用的地方:

StartCoroutine(_UseAssetDBAsync());

    IEnumerator _UseAssetDBAsync(string url){
        //實際效果是做不到的,這個鬼東西不允許在非主線程運作
        AssetDatabaseAsync adba = new AssetDatabaseAsync(url);
        yield return adba;
        Texture2D t2d = adba.GetResourceObject;
        _SetImageByTexture(t2d);
    }
           

強行總分總

寫完!舒暢!學u3d之類的東西果然要多看國外的原始文檔啊,國内的部落格什麼的太難了解了,這篇除外。希望大家夥兒看到這兒能真正懂得yield return new xx的原理,然後寫出自己滿意的bug代碼!!

繼續閱讀