上节回顾
上次我们分析了如何将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