上節回顧
上次我們分析瞭如何將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
更多內容,歡迎訪問: