天天看點

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

這裡himi強調一點:unity裡面的協程并不是線程,協程是在unity主線程中運作的,每一幀中處理一次,而并不與主線程并行。這就意味着在協程之間并不存在着所謂線程間的同步和互斥問題,不會出現死鎖。一般來說,通路同一個值也都是很安全的,用協程可以處理絕大多數的小問題,而且不用考慮複雜的線程間同步,還是很友善的。

要說協程的不足就是不能運用處理器的多核來提高處理性能,畢竟這個在運作時事實上是在一個線程中執行的。

【以下均為轉載内容】

在unity中,延時執行一段代碼或者一個方法或者幾個方法的情況非常普遍。

一般會用到invoke和invokerepeating方法。顧名思義,第一個是執行一次,第二個是重複執行。

看下定義:

void invoke(string methodname, float time);

第一個參數是方法名(注意是字元串形式),并不是更友善的委托。第二個是延時多少秒。隻執行一次。

void invokerepeating(string methodname, float time, float repeatrate);

invokerepeating第二個參數是延時多少秒後開始,第三個參數是每次執行間隔的秒數。

你有沒有發現這兩個方法有個弊端,就是必須輸入方法名!也就是我說,如果我想延時執行某段代碼,必須把代碼放在某個方法裡,然後使用這invoke或者invokerepeating方法來執行。

這樣對于上下文變量、屬性的引用就會尤為不便,而且不能傳參數!!!尼瑪,要他還有何用?

我猜你一定用過這樣的方法。沒錯,“協同”,聽起來還挺高大上的名字啊。

用startcoroutine來執行一個以ienumerator為傳回值的方法,通常用于異步下載下傳啊,等比較耗時又不能讓遊戲卡死的情況。

還有一個好的類waitforseconds,對,它就一個構造函數,用來延時的(延時………………比萬艾可好用?比希愛力好用?)。

好了不廢話了,以下是我自用的延時方法,放在一個類裡以靜态方法存在。可以在任何時候任何地方延時指定秒數的代碼。

using unityengine;

using system.collections;

using system;

public class delaytoinvoke : monobehaviour

{

public static ienumerator delaytoinvokedo(action action, float delayseconds)

yield return new waitforseconds(delayseconds);

action();

}

如何使用呢?

比如我點選ngui的一個button,則

void onclick()

startcoroutine(delaytoinvoke.delaytoinvokedo(() =>

application.loadlevel(“option”);

}, 0.1f));

尊重他人的勞動,支援原創,轉載請注明出處:http.dsqiu.iteye.com

記得去年6月份剛開始實習的時候,當時要我寫網絡層的結構,用到了協程,當時有點懵,完全不知道unity協程的執行機制是怎麼樣的,隻是知道函數的傳回值是ienumerator類型,函數中使用yield return ,就可以通過startcoroutine調用了。後來也是一直稀裡糊塗地用,上網google些基本都是例子,很少能幫助深入了解unity協程的原理的。

本文隻是從unity的角度去分析了解協程的内部運作原理,而不是從c#底層的文法實作來介紹(後續有需要再進行介紹),一共分為三部分:

線程(thread)和協程(coroutine)

unity中協程的執行原理

                        ienumerator & coroutine

d.s.qiu覺得使用協程的作用一共有兩點:1)延時(等待)一段時間執行代碼;2)等某個操作完成之後再執行後面的代碼。總結起來就是一句話:控制代碼在特定的時機執行。

很多初學者,都會下意識地覺得協程是異步執行的,都會覺得協程是c# 線程的替代品,是unity不使用線程的解決方案。

是以首先,請你牢記:協程不是線程,也不是異步執行的。協程和 monobehaviour 的 update函數一樣也是在mainthread中執行的。使用協程你不用考慮同步和鎖的問題。

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後去處理協程(檢查協程的條件是否滿足),但也有寫特例:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

從上圖的剖析就明白,協程跟update()其實一樣的,都是unity每幀對會去處理的函數(如果有的話)。如果monobehaviour 是處于激活(active)狀态的而且yield的條件滿足,就會協程方法的後面代碼。還可以發現:如果在一個對象的前期調用協程,協程會立即運作到第一個 yield return 語句處,如果是 yield return null ,就會在同一幀再次被喚醒。如果沒有考慮這個細節就會出現一些奇怪的問題『1』。

『1』注 圖和結論都是從unitygems.com 上得來的,經過下面的驗證發現與實際不符,d.s.qiu用的是unity 4.3.4f1 進行測試的。經過測試驗證,協程至少是每幀的lateupdate()後去運作。

下面使用 yield return new waitforseconds(1f); 在start,update 和 lateupdate 中分别進行測試:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

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;

    ienumerator updatecoutine()

        debug.log(“this is update coroutine call before”);

        debug.log(“this is update coroutine call after”);

    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”);

        debug.log(“this is late coroutine call after”);

得到日志輸入結果如下:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

然後将yield return new waitforseconds(1f);改為 yield return null; 發現日志輸入結果和上面是一樣的,沒有出現上面說的情況:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

        yield return null;

『今天意外發現monobehaviour的函數執行順序圖,發現協程的運作确實是在lateupdate之後,下面附上:』

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

                                                                       增補于:03/12/2014 22:14

經過驗證,『2』的結論也是錯誤的,正确的結論是,monobehaviour.enabled = false 協程會照常運作,但 gameobject.setactive(false) 後協程卻全部停止,即使在inspector把  gameobject 激活還是沒有繼續執行:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

            this.enabled = false;

            //this.gameobject.setactive(false);

        debug.log(“this is update coroutine call second”);

先在update中調用 this.enabled = false; 得到的結果:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

然後把 this.enabled = false; 注釋掉,換成 this.gameobject.setactive(false); 得到的結果如下:

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

整理得到:通過設定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

unity在每幀做的工作就是:調用 協程(疊代器)movenext() 方法,如果傳回 true ,就從目前位置繼續往下執行。

hijack

這裡在介紹一個協程的交叉調用類 hijack(參見附件):

【轉】【UNITY3D 遊戲開發之六】UNITY 協程COROUTINE與INVOKE

using system;

using system.collections.generic;

using system.linq;

[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;

            }

                //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;

            //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;

            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的無力,雖說容易上手,但是填坑的功夫也是必不可少的。

看來很多結論還是要通過自己的驗證才行,貿然複制粘貼很難出真知,切記!