UniRx 插件入门

在完善 Unity 开发的游戏框架时,看到框架 TinaX

使用了 TweenRx 插件 TweenRx

而这个插件,又使用到了一个名为 UniRx 的插件 UniRx

看到框架 QFramework 也用到了该插件 QFramework

于是了解了响应式编程这个概念,经过一番学习之后,个人理解是:

这是一种基于异步通信数据流的编程模式。
这个流的最大特点是:
每一个数据都可以转化为流;
对流的操作,产生新的流;
数据变化后,中间的“操作”是不变的,会使用同一个处理流程。

它是一种全新的编程理念,可以让我们更加关注于业务逻辑,而非程序逻辑,这大大启发了我。

这一篇主要记录下这个插件的基本使用方法。

学习的过程中,除了参考官方的 README.md 之外,也参考了这些资料:

Unity UniRX中文教程-Chinar
什么是响应式编程
The introduction to Reactive Programming you’ve been missing

UniRx 是什么?

官方 README :

UniRx (Reactive Extensions for Unity) is a reimplementation of the .NET Reactive Extensions. The Official Rx implementation is great but doesn’t work on Unity and has issues with iOS IL2CPP compatibility. This library fixes those issues and adds some specific utilities for Unity. Supported platforms are PC/Mac/Android/iOS/WebGL/WindowsStore/etc and the library.

UniRx 全称 Reactive Extensions for Unity,它重新实现了 .NET Reactive Extensions,官方的Rx (Reactive Extensions)虽然很给力,但是无法在 Unity 上正常的工作,而且在 iOS IL2CPP 的兼容性上有些问题。UniRx 修复了这些问题,并为 Unity 额外添加了一些特殊的工具。支持的平台包括 PC/Mac/Android/iOS/WebGL/WindowsStore 等等。

它不但在 Github 上开源,而且在 Unity 插件商店 也是免费的。

为什么要用 UniRx?

总结官方的话,就是这几个点:
1、Unity 中经常要使用的 Coroutine,对于异步操作不好用;
怎么不好用?
1.1、Coroutine 返回对是 IEnumerator 对象,而不是一些值对象(Coroutines can’t return any values, since its return type must be IEnumerator.)
1.2、异常处理不友好(Coroutines can’t handle exceptions, because yield return statements cannot be surrounded with a try-catch construction.)

那么 UniRx 的特点是什么?官方的原话如下:

UniRx is a library for composing asynchronous and event-based programs using observable collections and LINQ-style query operators.
The game loop (every Update, OnCollisionEnter, etc), sensor data (Kinect, Leap Motion, VR Input, etc.) are all types of events. Rx represents events as reactive sequences which are both easily composable and support time-based operations by using LINQ query operators.
Unity is generally single threaded but UniRx facilitates multithreading for joins, cancels, accessing GameObjects, etc.
UniRx helps UI programming with uGUI. All UI events (clicked, valuechanged, etc) can be converted to UniRx event streams.
Unity supports async/await from 2017 with C# upgrades, UniRx family prjects provides more lightweight, more powerful async/await integration with Unity. Please see CySharp/UniTask.
https://github.com/Cysharp/UniTask

以下可忽略,基本和官方一致。
UniRx 怎么用?

官方示例:捕获用户双击鼠标事件

using System;
using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{
    [SerializeField] Text m_Text = null;

    void Start()
    {
        var clickStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0));
        clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
            .Where(xs => xs.Count >= 2)
            .Subscribe(xs => Debug.LogFormat("DoubleClick Detected! Count: {0}", xs.Count));
    }
}

网络操作(官方不建议使用 ObservableWWW)
通用

using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{
    void Start()
    {
        ObservableWWW.Get("http://google.co.jp/")
            .Subscribe(
                x => Debug.Log(x.Substring(0, 100)), // onSuccess
                ex => Debug.LogException(ex)); // onError
            }
}
  1. 可组合也可取消,还可以使用 LINQ 类型的表达式
using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{
    [SerializeField] Text m_Text = null;

    void Start()
    {
        // composing asynchronous sequence with LINQ query expressions
        var query = from google in ObservableWWW.Get("http://google.com/")
                    from bing in ObservableWWW.Get("http://bing.com/")
                    from unknown in ObservableWWW.Get(google + bing)
                    select new { google, bing, unknown };

        var cancel = query.Subscribe(x => Debug.Log(x));

        // Call Dispose is cancel.
        cancel.Dispose();
    }
}
  1. 并行
// Observable.WhenAll is for parallel asynchronous operation
// (It's like Observable.Zip but specialized for single async operations like Task.WhenAll)
var parallel = Observable.WhenAll(
    ObservableWWW.Get("http://google.com/"),
    ObservableWWW.Get("http://bing.com/"),
    ObservableWWW.Get("http://unity3d.com/"));

parallel.Subscribe(xs =>
{
    Debug.Log(xs[0].Substring(0, 100)); // google
    Debug.Log(xs[1].Substring(0, 100)); // bing
    Debug.Log(xs[2].Substring(0, 100)); // unity
});
  1. 下载以及通知 ObservableWWW / ScheduledNotifier
using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{
    [SerializeField] Text m_Text = null;

    void Start()
    {
        var _progressNotifier = new ScheduledNotifier<float>();
        ObservableWWW.GetAndGetBytes("https://github.com/neuecc/UniRx/releases/download/7.1.0/UniRx.unitypackage", progress: _progressNotifier).Subscribe();
        _progressNotifier.Subscribe(progressFloat => {
            Debug.Log("progress >> " + progressFloat);
        });
        _progressNotifier.SubscribeToText(m_Text);
    }
}
  1. 封装 Coroutine 的功能
// public method
public static IObservable<string> GetWWW(string url)
{
    // convert coroutine to IObservable
    return Observable.FromCoroutine<string>((observer, cancellationToken) => GetWWWCore(url, observer, cancellationToken));
}

// IObserver is a callback publisher
// Note: IObserver's basic scheme is "OnNext* (OnError | Oncompleted)?" 
static IEnumerator GetWWWCore(string url, IObserver<string> observer, CancellationToken cancellationToken)
{
    var www = new UnityEngine.WWW(url);
    while (!www.isDone && !cancellationToken.IsCancellationRequested)
    {
        yield return null;
    }

    if (cancellationToken.IsCancellationRequested) yield break;

    if (www.error != null)
    {
        observer.OnError(new Exception(www.error));
    }
    else
    {
        observer.OnNext(www.text);
        observer.OnCompleted(); // IObserver needs OnCompleted after OnNext!
    }
}

使用

void Start()
{
    var req = GetWWW("https://www.baidu.com")
        .Subscribe(
            x => Debug.Log(x.Substring(0, 100)), // onSuccess
            ex => Debug.LogException(ex)); // onError
}
  1. 调用多个 OnNext 的例子(同时告诉我们如何将一个对象转为 IObservable 类型对象)
public static IObservable<float> ToObservable(this UnityEngine.AsyncOperation asyncOperation)
{
    if (asyncOperation == null) throw new ArgumentNullException("asyncOperation");

    return Observable.FromCoroutine<float>((observer, cancellationToken) => RunAsyncOperation(asyncOperation, observer, cancellationToken));
}

static IEnumerator RunAsyncOperation(UnityEngine.AsyncOperation asyncOperation, IObserver<float> observer, CancellationToken cancellationToken)
{
    while (!asyncOperation.isDone && !cancellationToken.IsCancellationRequested)
    {
        observer.OnNext(asyncOperation.progress);
        yield return null;
    }
    if (!cancellationToken.IsCancellationRequested)
    {
        observer.OnNext(asyncOperation.progress); // push 100%
        observer.OnCompleted();
    }
}

// usecase
Application.LoadLevelAsync("testscene")
    .ToObservable()
    .Do(x => Debug.Log(x)) // output progress
    .Last() // last sequence is load completed
    .Subscribe();

如何每一帧调用某个函数? Observable.EveryUpdate()

using UnityEngine;
using UniRx;

public class MyUniRx_EveryUpdate : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Observable.EveryUpdate().Subscribe(_ =>
        {
            Debug.Log("EveryUpdate");
        });
    }
}

EveryUpdate().First() 第一帧执行

using UnityEngine;
using UniRx;

public class MyUniRx_EveryUpdate : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
            .First()
            .Subscribe(_ =>
            {
                Debug.Log("EveryUpdate");
            });
    }
}

First(过滤条件),按下鼠标左键时执行

using UnityEngine;
using UniRx;

public class MyUniRx_EveryUpdate : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
            .First(_ => Input.GetMouseButtonUp(0))
            .Subscribe(_ =>
            {
                Debug.Log("左键按下时执行,并且只执行一次");
            });
    }
}

AddTo 给事件流添加生命周期

using UnityEngine;
using UniRx;

public class MyUniRx_EveryUpdate : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
            .First(_ => Input.GetMouseButtonUp(0)).Subscribe(_ => {
                Debug.Log("Call On Left MouseButtonUp");
            })
            .AddTo(this); // 生命周期和 this 保持一致
    }
}

等价的写法

using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class MyUniRx_EveryUpdate : MonoBehaviour
{
    void Start()
    {
        this.UpdateAsObservable()
            .First(_ => Input.GetMouseButtonUp(0)).Subscribe(_ => {
                Debug.Log("Call On Left MouseButtonUp");
            });
    }
}

Interval

Observable.Interval(TimeSpan.FromSeconds(1))
    .Subscribe(_ =>
    {
        Debug.Log("Interval");
    })
    .AddTo(this);

数据绑定绑定到 UI

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class MyUniRx_EveryUpdate : MonoBehaviour
{
    [SerializeField] Text m_Text = null;

    private ReactiveProperty<int> m_IntRxProperty = new ReactiveProperty<int>(9);

    void Start()
    {
        m_IntRxProperty.SubscribeToText(m_Text);

        this.UpdateAsObservable()
            .Subscribe(_ => {
                if(Input.GetMouseButtonDown(0))
                {
                    // 左键按下时,修改数据值后,查看当前文本
                    m_IntRxProperty.Value++;
                }
                if(Input.GetMouseButtonDown(1))
                {
                    // 右键按下时,修改文本后,查看当前值
                    m_Text.text = "99999";
                    Debug.Log(m_IntRxProperty.Value);

                    // 值和文本不一样,说明只是数据绑定到 UI
                }
            });
    }
}

按钮点击事件绑定

using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class MyUniRx_ButtonClickBinding : MonoBehaviour
{
    [SerializeField] Button m_Button = null;

    private ReactiveProperty<int> m_IntRxProperty = new ReactiveProperty<int>(9);

    void Start()
    {
        m_Button.OnClickAsObservable().Subscribe(_ =>
        {
            Debug.Log("Click");
        });
    }
}

给按钮添加两个点击事件,能够同时响应

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class MyUniRx_ButtonClickBinding : MonoBehaviour
{
    [SerializeField] Button m_Button = null;

    private ReactiveProperty<int> m_IntRxProperty = new ReactiveProperty<int>(9);

    void Start()
    {
        m_Button.OnClickAsObservable()
            .Subscribe(_ =>
            {
                Debug.Log("Click");
            })
            .AddTo(this);

        m_Button.OnClickAsObservable()
            .Subscribe(_ =>
            {
                Debug.Log("Click 1");
            })
            .AddTo(this);
    }
}

图片拖拽事件

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class MyUniRxTest : MonoBehaviour
{
    [SerializeField] Image m_Image = null;

    void Start()
    {
        m_Image.OnBeginDragAsObservable()
            .Subscribe(_ =>
            {
                Debug.Log("BeginDrag");
            })
            .AddTo(this);
        m_Image.OnDragAsObservable()
            .Subscribe(_ =>
            {
                m_Image.transform.position = Input.mousePosition;
            })
            .AddTo(this);
        m_Image.OnEndDragAsObservable()
            .Subscribe(_ =>
            {
                Debug.Log("EndDrag");
            })
            .AddTo(this);
    }
}

Coroutine 转 Observable
using System.Collections;
using UnityEngine;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{
    void Start()
    {
        Observable.FromCoroutine(TestIEnumerator)
            .Subscribe()
            .AddTo(this);
    }

    private IEnumerator TestIEnumerator()
    {
        Debug.Log("开始");
        yield return new WaitForSeconds(1.0f);
        Debug.Log("结束");
    }
}
Observale 转 IEnumerator
using System;
using System.Collections;
using UnityEngine;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{

    void Start()
    {
        Observable.FromCoroutine(TestIEnumerator)
            .Subscribe()
            .AddTo(this);
    }

    private IEnumerator TestIEnumerator()
    {
        Debug.Log("开始");
        yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
        Debug.Log("结束");
    }
}

既然 Observable 可以转 IEnuerator,因此也就有了这样的使用方式:

using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class MyUniRxTest : MonoBehaviour
{

    void Start()
    {
        Observable.FromCoroutine(TestIEnumerator)
            .Subscribe()
            .AddTo(this);
    }

    private IEnumerator TestIEnumerator()
    {
        Debug.Log("开始");
        yield return this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(0)).First().ToYieldInstruction();
        Debug.Log("结束");
    }
}

其根本原因,还是在于 Observable 的底层实现,是 Coroutine 的这套机制;

多个任务并行

using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class MyUniRxTest : MonoBehaviour
{

    void Start()
    {
        var _task1 = Observable.FromCoroutine(Task1);
        var _task2 = Observable.FromCoroutine(Task2);
        Observable.WhenAll(_task1, _task2)
            .Subscribe(_=> {
                Debug.Log("所有任务完成!");
            })
            .AddTo(this);
    }

    private IEnumerator Task1()
    {
        Debug.Log("开始 1");
        yield return this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(0)).First().ToYieldInstruction();
        Debug.Log("结束 1");
    }

    private IEnumerator Task2()
    {
        Debug.Log("开始 2");
        yield return this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(1)).First().ToYieldInstruction();
        Debug.Log("结束 2");
    }
}

线程

using System.Threading;
using UnityEngine;
using UniRx;

public class MyUniRxTest : MonoBehaviour
{

    void Start()
    {
        var _task1 = Observable.Start(() =>
        {
            Thread.Sleep(2);
            Debug.Log("任务 1 Sleep 2 结束");
            return 1;
        });

        var _task2 = Observable.Start(() =>
        {
            Thread.Sleep(1);
            Debug.Log("任务 2 Sleep 1 结束");
            return 2;
        });

        Observable.WhenAll(_task1, _task2).ObserveOnMainThread().Subscribe(ret =>
        {
            Debug.Log(ret[0]);
            Debug.Log(ret[1]);
        });
    }
}

从结果看,完成的顺序是 任务2 先于任务1,结果 ret 保持了 WhenAll 中的任务顺序。

MicroCoroutine
限制:
只能由 StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine 这几个方法调用;
只支持 yield return null

UniRx makes it possible to implement the MVP(MVRP) Pattern.
UniRx 有助于实现 MVP 模式

ReactiveCommand

public class Player
{
    public ReactiveProperty<int> HP; 
    public ReactiveCommand Resurrect;
    public Player()
    {
        HP = new ReactiveProperty<int>(1000);
        Resurrect = HP.Select(x => x < 0).ToReactiveCommand();
        Resurrect.Subscribe(_ =>
        {
            Debug.Log("Add 2000");
            HP.Value += 2000;
        });
    }
}

public class MyUniRxTest : MonoBehaviour
{
    [SerializeField] Text m_Text = null;
    [SerializeField] Button m_Button = null;

    Player m_Player;

    void Start()
    {
        m_Player = new Player();
        m_Player.Resurrect.BindTo(m_Button);
        m_Player.HP.SubscribeToText(m_Text);

        Observable.Interval(TimeSpan.FromSeconds(1))
            .Subscribe(_ =>
            {
                m_Player.HP.Value -= 500;
            })
            .AddTo(this);
    }
}

一个 ReactiveCommand 可以绑定到一个 UGUI 按钮点击事件上
而 ReactiveCommand 本身可以通过 IObservable 对象创建,创建的过程可以附加执行的前置条件。

基于类型的订阅发布机制:MessageBroker, AsyncMessageBroker

public class TestArgs
{
    public int Value { get; set; }
}

---

// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));

// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });

那么如果存在继承关系呢?经过测试,订阅的消息,不受继承关系的影响,
AsyncMessageBroker

AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
    // show after 3 seconds.
    return Observable.Timer(TimeSpan.FromSeconds(3))
        .ForEachAsync(_ =>
        {
            UnityEngine.Debug.Log(x);
        });
});

AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
    .Subscribe(_ =>
    {
        UnityEngine.Debug.Log("called all subscriber completed");
    });

上述机制,适合建立对象的异步回收策略;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这里给您举一个使用RxJava框架的实例: 假设有一个需求,要求从一个API接口中获取用户信息并显示在界面上。我们可以使用RxJava来实现这个需求: 首先,在对应的Activity或Fragment中,我们定义一个Observable对象,用来发出网络请求并获取用户信息: ```java Observable<User> userObservable = Api.getUserInfo(userId); ``` 然后,我们可以使用subscribeOn()方法指定请求在IO线程中执行,使用observeOn()方法指定结果在主线程中回调: ```java userObservable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<User>() { @Override public void onSubscribe(Disposable d) { // 可以在这里做一些初始化操作,比如显示loading等 } @Override public void onNext(User user) { // 获取到用户信息后,更新UI显示 updateUi(user); } @Override public void onError(Throwable e) { // 出现异常时,可以做一些错误处理,比如弹出Toast提示等 showError(e.getMessage()); } @Override public void onComplete() { // 请求完成后,可以在这里做一些清理工作,比如隐藏loading等 } }); ``` 在上面的代码中,我们通过实现Observer接口来处理请求结果。在onNext()方法中,我们可以拿到获取到的用户信息,然后更新UI显示。在onError()方法中,我们可以处理请求出现异常的情况,比如弹出Toast提示。在onComplete()方法中,我们可以做一些清理工作,比如隐藏loading。 这就是一个简单的使用RxJava来获取用户信息的示例。通过使用RxJava,我们可以简化异步编程的复杂度,使代码更加清晰易读。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值