随风逐叶 随风逐叶
首页
  • Quick Reference (opens new window)
  • EgretEngine开发者文档 (opens new window)
  • TinaX框架
  • SSH教程
  • VSCode插件开发
关于
  • 分类
  • 标签
  • 归档

rontian

从事游戏开发10多年的老菜鸟一枚!
首页
  • Quick Reference (opens new window)
  • EgretEngine开发者文档 (opens new window)
  • TinaX框架
  • SSH教程
  • VSCode插件开发
关于
  • 分类
  • 标签
  • 归档
  • 框架简介
  • TinaX.Core
  • 基于TinaX创建一个扩展库
  • TinaX.VFS
  • TinaX.UIKit
  • TinaX.I18N
  • TinaX.Lua
  • XLua

  • Google.Protobuf
  • Lua-Protobuf
  • 一些优秀的第三方库

    • CatLib

    • UniRx

      • 简介
      • 基础教程
      • 官方入门文档
      • UniRx入门系列一
      • UniRx入门系列二
      • UniRx入门系列三
      • UniRx入门系列四
      • UniRx入门系列五
      • UniTask

    目录

    UniRx入门系列五

    # 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("协程结束");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    输出如下:

    协程开始
    协程结束
    OnNext
    OnCompleted
    
    1
    2
    3
    4

    注意,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("协程结束");
        }
    
    1
    2
    3
    4
    5
    6
    7
    8

    # 取出 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;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    # 在协程内部发布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;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    # 以更轻便、高效的方式执行协程


    • 使用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)
    
    1
    2
    3
    4
    5

    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;
            }
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # 将协程转化为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("好了,按下成功");
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    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 + "发生碰撞");
            }
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 从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 完成");
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 同时启动多个协程,并等待执行结果


    同时启动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();
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    执行结果输出如下:

    CoroutineB 开始
    CoroutineA 开始
    B 1秒等待结束
    A 3秒等待结束
    result:协程A 执行完成
    result:协程B 执行完成
    
    1
    2
    3
    4
    5
    6

    # 将耗时的处理转移到另外一个线程上执行,并在协程上处理执行结果


    利用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; }
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    上面的实现方式虽然没有下面的实现方式简洁,但是它详细解释了整个执行过程,以下是,上面的缩写:

      ObservableWWW.Get("http://api.hogehoge.com/resouces/enemey.xml")
            .SelectMany(x => Observable.Start(() => ParseXML(x)))
            .ObserveOnMainThread()
            .Subscribe(onNext: result =>
            {
                /*对执行结果进行处理 */
            },
            onError: ex => Debug.LogError(ex));
    
    1
    2
    3
    4
    5
    6
    7
    8

    # 总结


    • 流和协程之间可以互相转换
    • 通过使用协程,可以创建仅仅依靠操作符无法创建的流
    • 使用UniRx的协程机制可以提高标准Unity协程的可用性和性能。
    • 通过将流转换成协程,可以执行类似于原生C#中 Task 的 async 和 await
    上次更新: 2023/10/17, 14:09:52 访问次数: 0
    UniRx入门系列四
    UniTask

    ← UniRx入门系列四 UniTask→

    最近更新
    01
    一些Shell常用的功能写法整理
    10-20
    02
    删除git仓库submodule的步骤
    10-20
    03
    django基本命令
    10-16
    更多文章>
    Copyright © 2017-2025 随风逐叶
    沪ICP备18008791号-1 | 沪公网安备31011502401077号

    网站访问总次数: 0次
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式