上节回顾


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