UniRx入門系列(五)

上節回顧


上次我們分析瞭如何將Unity中的Update轉換爲UniRx中的Observable來使用;這一節,我們將講解一下,如何將UniRx中的協程和UniRx相結合。

Coroutine(協程)和UniRx


默認情況下,Unity中提供了一個叫做“協程”的東西。這個功能在C#中利用IEnumerator和Yield關鍵字在迭代器迭代過程中實現調用。在Unity主線程中實現類似異步處理的功能。(Unity中的協程並不是多線程,其仍然是在主線程上調用的,他和Update一樣,且運行時間也大致相同)。那麼我們如何既能夠使用Unity的協程來描述處理邏輯,同時有可以使用UniRx來靈活的處理異常呢?

從Coroutinue轉換成IObservable

我們先介紹一種從協程轉換爲IObservable的方法;如果你把協程轉換爲流,那麼你就可以將協程處理結果和UniRx的操作符連接起來。另外,在創建一個複雜行爲流的時候,採用協程實現並轉化爲流的方式,有時,比僅僅使用UniRx操作鏈構建流要簡單的多。

等待攜程結束時將其轉化爲流


  • 使用Observable.FromCoroutine()方法
  • 返回值 IObservable
  • 參數一:Func coroutine
  • 參數二:bool publishEveryYield=false

利用Observable.FromCoroutine()方法,我們可以在協程結束時,將其流化處理。當你需要在協程結束時發出通知,可以使用如下:

public class TestUniRX : MonoBehaviour
{
    void Start()
    {
        Observable.FromCoroutine(NantokaCoroutine, publishEveryYield: false)
        .Subscribe(
            onNext: _ =>
            {
                Debug.Log("OnNext");
            },
            onCompleted: () =>
            {
                Debug.Log("OnCompleted");

            }).AddTo(gameObject);
    }
    private IEnumerator NantokaCoroutine()
    {
        Debug.Log("協程開始");
        yield return new WaitForSeconds(3);
        Debug.Log("協程結束");
    }
}

輸出如下:

協程開始
協程結束
OnNext
OnCompleted

注意,Observable.FromCoroutine每被Subscribe一次,就會創建並啓動一個新的協程。如果,你只啓動了一個協程,並想共享這個流的話,那麼你就需要進行流的hot轉換。另外,通過Observable.FromeCoroutine啓動的協程在終止時會被自動Dispose。如果你想在協程上檢測流是否被釋放了,可以通過向協程中傳遞參數來檢測流是否被Dispose,如下:

Observable.FromCoroutine(token=>NantokaCoroutine(token));

private IEnumerator NantokaCoroutine(CancellationToken tk)
    {
        Debug.Log("協程開始");
        yield return new WaitForSeconds(3);
        Debug.Log("協程結束");
    }

取出 yield return 迭代的結果


  • 使用Observable.FromCoroutineValue()方法
  • 返回值 IObservable
  • 參數一:Func coroutine
  • 參數二:bool nullAsNextUpdate=true

我們都知道。Unity中的協程的返回值只能是IEnumerator;在協程中,我們不能向使用普通方法那樣,將攜程的迭代結果賦予一個變量。現在UniRx賦予我們這個能力,我們可以把每次yield的值取出來作爲數據流。因爲Unity協程中的yield return 每次調用都會停止一幀,可以利用其在某一幀發佈值(注意,是指在一幀中執行):

public class TestUniRX : MonoBehaviour
{
    public List<Vector2> moveList=new List<Vector2>(100);
    void Start()
    {
        Observable.FromCoroutineValue<Vector2>(MovePositionCoroutine)
        .Subscribe(x=>Debug.Log(x));

    
    }

    private IEnumerator MovePositionCoroutine()
    {
        foreach (var item in moveList)
        {
            yield return item;
        }
    }
}

在協程內部發布OnNext


  • 使用Observable.FromCoroutine方法
  • 返回值IObservable
  • 參數一:Func<IObserver,IEnumerator> coroutine,第一個參數爲IObserver

此實現將IObserver的實現傳遞給協程,可以在協程執行過程中發佈OnNext.通過使用這個方法,可以在Coroutine和UniRx中進行:內部實現,外部使用。即,內部的實現在協程中異步處理,同時將其作爲流對外發布。個人覺得,這也是UniRx提供的最好用的功能之一。另外,記住,此方法的OnCompleted並不會自動發佈,所以你需要在協程結束時自己手動發佈OnCompleted.

public class TestUniRX : MonoBehaviour
{
    public bool isPaused = false;
    void Start()
    {
        Observable.FromCoroutine<long>(observer => MovePositionCoroutine(observer))
        .Subscribe(x =>
        {
            Debug.Log(x);
        });
    }
    private IEnumerator MovePositionCoroutine(IObserver<long> observer)
    {
        long current = 0;
        float deltaTime = 0;
        while (true)
        {
            if (!isPaused)
            {
                deltaTime+=Time.deltaTime;
                if (deltaTime>=1.0f){
                    var integePart=(int)Mathf.Floor(deltaTime);
                    current+=integePart;
                    deltaTime-=integePart;
                    observer.OnNext(current);
                }
            }
            yield return null;
        }
    }
}

以更輕便、高效的方式執行協程


  • 使用Observable.FromMicroCoroutine/Observable.FromMicroCoroutine
  • 參數一:Func coroutine / Func<IObserver, IEnumerator> coroutine
  • 參數二:FrameCountType frameCountType=FrameCountType.Update協程的執行時機
 public static IObservable<Unit> FromMicroCoroutine(Func<IEnumerator> coroutine, bool publishEveryYield = false, FrameCountType frameCountType = FrameCountType.Update)

 public static IObservable<Unit> FromMicroCoroutine(Func<CancellationToken, IEnumerator> coroutine, bool publishEveryYield = false, FrameCountType frameCountType = FrameCountType.Update)

 public static IObservable<T> FromMicroCoroutine<T>(Func<IObserver<T>, IEnumerator> coroutine, FrameCountType frameCountType = FrameCountType.Update)

Observable.FromMicroCoroutine和Observable.FromMicroCoroutine 的行爲幾乎和我們之前說過的一致;但是,他們的內部實現卻大不相同。雖然在協程內部有yield return 的限制,但是,與Unity標準的協程相比,UniRx提供的MicroCoroutine的啓動和運行是非常快速的。它在UniRx中被稱之爲微協程。以更低的成本啓動並運行協程,而不是使用Unity標準的StartCoroutine.

 void Start()
    {
        Observable.FromMicroCoroutine<long>(observer => CountCoroutine(observer))
        .Subscribe(x => Debug.Log(x))
        .AddTo(gameObject);
    }
    IEnumerator CountCoroutine(IObserver<long> observer)
    {
        long number = 0;
        while (true)
        {
            number++;
            observer.OnNext(number);
            yield return null;
        }
    }

將協程轉化爲IObservable總結


  • 使用UniRx提供的方法將協程轉化爲IObservable
  • 使用Observable.FromCoroutine啓動並執行的協程會被委託給MainThreadDispatcher管理,因此不要忘記手動釋放
  • 使用Observable.FromCoroutine會在Subscribe時生成並啓動新的協同程序,因此如果你想共享一個協程並進行多次Subscribe時,需要進行hot變換。

從IObservable轉換成協程


我們將介紹如何將UniRx流轉換成協程流。利用流轉化成協程的這個技巧,可以實現諸如在協程上等待流執行結果後繼續處理這樣的方法。類似於C# 中 Task 的 await .

將流轉換爲協程


  • 使用 ObservableYieldInstruction ToYieldInstruction(IObservable observable)方法
  • 參數一: CancellationToken cancel 處理進程中斷(可選)
  • 參數二: bool throwOnError 發生錯誤時,是否拋出異常

通過使用ToYieldInstruction,你可以在協程中執行等待。

 void Start()
    {
        StartCoroutine(WaitCoroutine());
    }
    IEnumerator WaitCoroutine()
    {
        Debug.Log("等待一秒鐘");
        yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
        Debug.Log("按下鍵盤上的任意鍵");
        yield return this.UpdateAsObservable()
        .FirstOrDefault(_ => Input.anyKeyDown)
        .ToYieldInstruction();

        Debug.Log("好了,按下成功");
    }

ToYieldInstruction收到OnCompleted時會終止yield return.因此,如果你不自己手動發佈OnCompleted,那麼流就永遠不會被終止,這是非常危險的。此外,如果你要使用流發出OnNext信息,可以將ToYieldInstruction的返回值存儲在ObservableYieldInstruction變量中。

 void Start()
    {
        StartCoroutine(WaitCoroutine());
    }
    IEnumerator DetectCoroutine()
    {
        Debug.Log("協程開始");
        var o = this.OnCollisionEnterAsObservable()
        .FirstOrDefault()
        .Select(x => x.gameObject)
        .Timeout(TimeSpan.FromSeconds(3))
        .ToYieldInstruction(throwOnError: false);

        yield return o;
        if (o.HasError || !o.HasResult)
        {
            Debug.Log("沒有和任何對象發生碰撞");
        }
        else
        {
            var hitObj = o.Result;
            Debug.Log("和" + hitObj.name + "發生碰撞");
        }
    }

從IObservable轉換成協程總結


  • 使用ToYieldInstruction或者StartAsCoroutine將流轉換爲協程
  • 可以在協程執行過程中執行等待併發布特定的事件行爲。

應用實例


串聯協程

 void Start()
    {
        Observable.FromCoroutine(CoroutineA)
        .SelectMany(CoroutineB)
        .Subscribe(_=>Debug.Log("CoroutineA 和CoroutineB 執行完成"));
    }
    IEnumerator CoroutineA()
    {
        Debug.Log("CoroutineA 開始");
        yield return new WaitForSeconds(3);
        Debug.Log("CoroutineB 完成");
    }
    IEnumerator  CoroutineB()
    {
        Debug.Log("CoroutineB 開始");
        yield return new WaitForSeconds(1);
        Debug.Log("CoroutineB 完成");
    }

同時啓動多個協程,並等待執行結果


同時啓動CoroutineA和CoroutineB,全部結束之後再彙總處理

void Start()
    {
        Observable.WhenAll(
            Observable.FromCoroutine<string>(o => CoroutineA(o)),
            Observable.FromCoroutine<string>(o => CoroutineB(o))
        ).Subscribe(xs =>
        {
            foreach (var item in xs)
            {
                Debug.Log("result:" + item);
            }
        });
    }
    IEnumerator CoroutineA(IObserver<string> observer)
    {
        Debug.Log("CoroutineA 開始");
        yield return new WaitForSeconds(3);
        observer.OnNext("協程A 執行完成");
        Debug.Log("A 3秒等待結束");
        observer.OnCompleted();
    }
    IEnumerator CoroutineB(IObserver<string> observer)
    {
        Debug.Log("CoroutineB 開始");
        yield return new WaitForSeconds(1);
        observer.OnNext("協程B 執行完成");
        Debug.Log("B 1秒等待結束");
        observer.OnCompleted();
    }

執行結果輸出如下:

CoroutineB 開始
CoroutineA 開始
B 1秒等待結束
A 3秒等待結束
result:協程A 執行完成
result:協程B 執行完成

將耗時的處理轉移到另外一個線程上執行,並在協程上處理執行結果


利用Observable.Start()處理邏輯移到其它線程執行,將返回結果轉回到協程中處理

void Start()
    {
       StartCoroutine(GetEnemyDataFromServerCoroutine());
    }
    private IEnumerator GetEnemyDataFromServerCoroutine()
    {
        var www=new WWW("http://api.hogehoge.com/resouces/enemey.xml");
        yield return www;
        if (!string.IsNullOrEmpty(www.error)){
            Debug.Log(www.error);
        }
        var xmlText=www.text;
        var o=Observable.Start(()=>ParseXML(xmlText)).ToYieldInstruction();

        yield return o;
        if (o.HasError){
            Debug.Log(o.Error);
            yield break;
        }
        var result=o.Result;

        Debug.Log(result);
    }

    Dictionary<string,EnemyParameter> ParseXML(string xml){
        return new Dictionary<string, EnemyParameter>();
    }
    struct EnemyParameter{
        public string Name { get; set; }
        public string Helth { get; set; }
        public string Power { get; set; }
    }

上面的實現方式雖然沒有下面的實現方式簡潔,但是它詳細解釋了整個執行過程,以下是,上面的縮寫:

  ObservableWWW.Get("http://api.hogehoge.com/resouces/enemey.xml")
        .SelectMany(x => Observable.Start(() => ParseXML(x)))
        .ObserveOnMainThread()
        .Subscribe(onNext: result =>
        {
            /*對執行結果進行處理 */
        },
        onError: ex => Debug.LogError(ex));

總結


  • 流和協程之間可以互相轉換
  • 通過使用協程,可以創建僅僅依靠操作符無法創建的流
  • 使用UniRx的協程機制可以提高標準Unity協程的可用性和性能。
  • 通過將流轉換成協程,可以執行類似於原生C#中 Task 的 async 和 await

更多內容,歡迎訪問:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章