UniRx入门

UniRx - Unity响应式编程插件  
插件作者Yoshifumi Kawai(neuecc) 
本篇参照博客:1.凉鞋小班博客
            2.https://blog.csdn.net/zhenghongzhi6/article/details/79229585

简介

UniRx简介

UniRx 是 Unity Reactive Extensions 的缩写,意思是 Unity 的响应式扩展集。重写了.Net的响应式扩展。
.Net官方的Rx很棒,但是在Unity中无法使用,并且与IOS的IL2CPP有兼容性问题。这个库这些问题并且添加了一些Unity专属的工具类。
支持的平台有:PC/Mac/Android/iOS/WP8/WindowsStore/等等,并且支持Unity4.6之后的所有版本。

UniRx 在 Unity Asset Store 的地址(免费):http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT

响应式

响应式是什么意思:一旦A做了事情,B就得到了通知。
为什么unirx是响应式:因为UniRx通过观察者模式实现了这种响应。

扩展

在C#语法中对静态this关键字进行了扩展,类似于Linq的关键字扩展。
例:
Observable.EveryUpdate() // 每一帧发送一次通知
.Where(=>Timer.frameCount % 5 == 0) // Linq Where 关键字,每 5 帧发送一次通知
.Subscribe(
=>Debug.Log(“Every 5 Frame”)) // 订阅 输出

为什么用Rx

拿网络操作进行举例:
网络操作需要用到WWW和Coroutine。但是使用Coroutine会遇到下面列问题
1.协程不能有返回值,因为返回类型必须是IEnumerator
2.协程不能处理异常,因为 yield return 语句没办法被 try-catch
这样就会造成代码大面试的强耦合。
游戏循环 (every Update, OnCollisionEnter, etc), 传感器数据 (Kinect, Leap Motion, VR Input, etc.) 都是事件。Rx将事件转化为响应式的序列,通过LINQ操作可以很简单地组合起来,还支持时间操作。

Unity通常是单线程,但是UniRx可以让多线程更容易。

UniRx 可以简化 uGUI 的编程,所有的UI事件 (clicked, valuechanged, etc) 可以转化为 UniRx 的事件流。

总结

Reactive Extensions 指的就是: 观察者模式 + Linq。

快速入门

案例

代码如下:

using UnityEngine;
using UniRx;

namespace QF.Master.Example
{
    public class UniRxBasicUsage : MonoBehaviour
    {
        void Start()
        {
            Observable.EveryUpdate()
                .Where(_ => Time.frameCount % 5 == 0)
                .Subscribe(_=> Debug.Log("hello"));
        }
    }
}

运行结果:

代码分析

主要代码:

Observable.EveryUpdate()
  .Where(_ => Time.frameCount % 5 == 0)
  .Subscribe(_=> Debug.Log("hello"));

代码分为三部分,分别是:

Observable:Observable.EveryUpdate()
Linq 操作符:Where(_ => Time.frameCount % 5 == 0)
订阅、注册事件的处理:Subscribe(_=> Debug.Log("Hello"))

以上是一个 UniRx 的基本使用格式,每个 UniRx 的完整使用,都会包含以上三个部分。

观察者模式

观察者模式分别有:观察者(Observer)、主题(Subject)、通知(Notify)、订阅(Subscribe)。
Observable 中文的意思是可观察者的,是一个形容词。
主题(Subject)就是可观察的主体。
对比下列代码

Observable.EveryUpdate()	// Observable
  .Where(_ => Time.frameCount % 5 == 0)		//可以不写,代表没有限制条件
  .Subscribe(_=> Debug.Log("hello"));	// Subscribe

代码中的 EveryUpdate 意思是:每次执行 Update 时,发送一次通知(Notify)。
发送的通知由 Subscribe 中的 lambda 表达式接收。

Where

Where(_ => Time.frameCount % 5 == 0)

Where 是相当于一个条件过滤,意思是,当 Where 方法中的 lambda 表达式返回 true 时,则保留,否则丢弃。
Where 是一个 Linq 操作符。
举例:

var ages = new int[] { 1,2,3};
var ageList = ages.Where(age=>age >= 2).ToList();
// 此时 ageList 为 [2,3]

UniRx 中的 Linq 操作符

Linq 有大量的类似 Where 这样的操作符,而 UniRx 也同样支持了大量的操作符,不过 UniRx 是在自己的库中实现的,与 C# 的 Linq 操作符并不是完全一致的。
我们知道 C# 中的 Linq 操作符是查询内存数据用的,这里的内存数据指的是各种可枚举集合(IEnumrable),比如 Array,Queue,Stack,Dictionary 等所有实现 IEnumrable 接口的类。
而 UniRx 中的 Linq 操作符是用来查询各种事件源的 (IObservable),比如 Observable.EveryUpdate() 返回的对象就是一个 IObservable 对象。
C# 中的 Where 操作符是过滤一个一个数据。
而 UniRx 中的 Where 操作符是过滤一个一个通知(事件)

Subscribe

.Subscribe(_=> Debug.Log("hello"));

Subscribe就是订阅的意思,在代码中其实是被订阅的意思,意思就是被观察者订阅。
代码结构目前是:

Observable.Linq.Subcribe(callback)

讲到这里,观察者模式中,我们看到了主题(Observable)、订阅(Subscribe)、通知(Notify),但是还没有**观察者(Observer)**。
我们来看看Subscribe的底层是如何定义的

public static XXXYYYZZZ Subscribe(IObserver observer)
{
  ...
}

Subscribe 的参数实际上就是一个观察者,而在最上层的 API 中,UniRx 将 Observer 简化为了一个委托。

总结

UniRx 的最核心的三个概念分别是:
Observable(可观察的)
Linq 操作符
Subscribe(Observer)
这是UniRx最基础的三个概念,只要熟练掌握这三个概念就可以运用UniRx的所有API了。
niRx 有大量的 Observable 和 Linq 操作符,我们学习的 EveryUpdate 只是其中一个 Observable。

扩展

TimeSpan(延迟功能)

代码如下:

using UnityEngine;
using UniRx;
using System;
public class UniRxTimerExample : MonoBehaviour 
{
    void Start () 
    {
        Debug.Log("此时:" + DateTime.Now);

        Observable.Timer(TimeSpan.FromSeconds(2.0f))
             .Subscribe(_=>Debug.Log("此时:" + DateTime.Now));
    }
}

AddTo(MonoBehaviour)

上述延迟有个问题,就是如果延迟期间gameobject被销毁,就会访问到NULL数据
UniRx 提供了一个方案:生命周期绑定
当 GameObject 销毁时,自动销毁延时任务:AddoTo(MonoBehaviour)

using UnityEngine;
using UniRx;
using System;
public class UniRxTimerExample : MonoBehaviour 
{
    void Start () 
    {
        Debug.Log("此时:" + DateTime.Now);

        Observable.Timer(TimeSpan.FromSeconds(2.0f))
            .Subscribe(_=> Debug.Log("此时:"  + DateTime.Now))
            .AddTo(this);

        Observable.Timer(TimeSpan.FromSeconds(0.5f))
            .Subscribe(_=> Destroy(gameObject));
    }
}

运行的结果:(2s后就没有输出了)

IDisposable

而 AddTo 实际上是 UniRx 对 IDisposable(.NET中用于释放对象资源的接口) 的一个静态扩展,也就是说所有的实现 IDisposable 接口的对象都可以使用 AddTo 将销毁绑定到 MonoBehaviour 或 GameObject 上。
而 Observbale.Timer 在 Subscribe 之后能够调用 AddTo 是因为 Subscribe 方法返回了一个 IDisposable 对象。

public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext)
{
    ...
}

使用 IDisposable 和 AddTo 这对组合我们可以在做模块或库设计的时候非常容易实现生命周期绑定的功能,IDisposable 实际上可以充当一个卸载过程。

Subject使用

Subject Observable

Subject 即主题,UniRx 是提供了具体的 Subject 这个概念的。
举例代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

public class UniRxSubjectExample : MonoBehaviour 
{
    void Start () 
    {
        var subject = new Subject<int>();

        subject.Subscribe(number => {
            Debug.Log(number);
        });

        subject.OnNext(1);
        subject.OnNext(2);
        subject.OnNext(3);
    }
}

Subject是一个Observable,是一个事件源,是可以被订阅的(Subscribe).
而 Subject 是一个泛型类,可以制定要发送通知的类型。
代码中指定了发送类型为int。

var subject = new Subject<int>();

而指定什么类型,那么在订阅的时候就会接收到什么类型的数据

subject.Subscribe(number => {
  Debug.Log(number);
});

一个 subject 光是创建对象和被订阅是没什么用的,要让 subject 发送数据,代码如下:

subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);

运行结果如下:

Subjecrt是什么?

在 UniRx 中 Subject 是一个 Observable(事件源),它可以让我们通过做少的代码实现观察者模式。

Subject 与委托相比有什么不同?

在使用上,Subject 与委托的使用方式差不多,两者之间共同的地方有:

需要声明,定义参数类型
需要注册、订阅
委托需要调用,而 Subject 需要通过 OnNext 发送数据。
委托给人的感觉是一根线,而 Subject 给人感觉是一个管道。
委托不同的地方有:
可以定义多个接收参数
接收数据之后比较难以再组织整理

Subject 不同的地方有:

只能定一个一个参数类型
接收数据之后可以使用 Linq 对数据进行再组织整理

OnNext 与 IObserver

OnNext简介

从前面内容可以推测出Subject 继承了 Observable。
真实情况底层代码Subject实现了一个IObservable接口

namespace UniRx
{
    public interface IObservable<T>
    {
        IDisposable Subscribe(IObserver<T> observer);
    }
}

从代码中可以看出**IObservable.Subscribe(IObserver)**结构
OnNext 没有在 IObservable 中定义,但是在 IObserver 中定义了,IObserver 代码如下:

using System;

namespace UniRx
{
    public interface IObserver<T>
    {
        void OnCompleted();
        void OnError(Exception error);
        void OnNext(T value);
    }
}

Subject 既是 Observable 也是 Observer。它同时实现了 IObservable 和 IObserver 两个接口。

这样 Subject 就能做到,既可以被订阅(Subscribe),又可以让我们通过其 OnNext 方法传输数据。

到此,大家可能会比较乱,IObservable 和 IObserver 分不清楚。

没关系,IObservable 和 IObserver 在接下来的文章中会着重介绍,因为它俩是 UniRx 的两个核心接口。

OnNext 是什么?

OnNext 是 IObserver 定义的 API,在 UniRx 内部,OnNext 是被 IObservable 调用的。当 IObservable 每次发送事件时,都是通过 OnNext 向 IObserver 发送事件了,如下图所示:

而 Subject 既是 IObservable 又是 IObserver 所以既可以通过 OnNext 传输数据,又可以被 Subscribe。
Subject 就是这样的一个特殊的存在。

IObserver

底层代码:

using System;

namespace UniRx
{
    public interface IObserver<T>
    {
        void OnCompleted();
        void OnError(Exception error);
        void OnNext(T value);
    }
}

OnComplete

OnCompleted意思是,当完成时,也就是说当 Observable 发送完事件时会调用 IObserver 的 OnCompleted 的方法。
Observable.EveryUpdate 没有完成的时刻,所以是不会接收Oncomplete事件。
UniRx中我们就知道有两种 Observable 了,即可结束的(比如 Observable.Timer)和不可结束的(Observable.EveryUpdate)
OnCompleted 是在Subscribe 的第二个参数中使用。
我们知道 Subscribe 可以传入一个委托,次委托实际上是对应的是 OnNext 事件,那么第二个无参委托则是对应的是 OnCompleted 事件。

using UnityEngine;
using UniRx;
using System;

public class UniRxOnCompletedExample : MonoBehaviour 
{
    void Start () 
    {    
        Observable.Timer(TimeSpan.FromSeconds(3.0f))
            .Subscribe(_ => Debug.Log("Delayed 3 seconds"),()=>Debug.Log("completed"));
    }
}

OnNext

Subscribe 的第一个有参委托就是接收的是 OnNext 事件,这里就不多说了。

OnError

Observable(事件源)会发送一些异常或错误,这个时候错误和异常就会发送到 IObserver 的 OnError 中,而 OnError 的使用也非常简单,即:Subscribe 的第二个有参委托接收的就是 OnError 事件

ReactiveProperty

ReactiveProperty 即响应式属性,它是一个 Property(属性),同时也是响应式的,说明它是一个可以被 Subscribe 的 Property。

C#中的Property

public class SomeClass
{
  public int Age {get;set;}
}

SomeClass 中的 Age 是一个 Property(属性)

UniRx中的Property

public class SomeClass
{
  public ReactiveProperty<int> Age = new ReactiveProperty<int>();
}

ReactiveProperty 的作用与 C# 的 Property 作用是一致的,就是可以设置值和获取值。
得到值的方法是:属性名.Value
测试代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

public class UniRxReactiveProperty : MonoBehaviour {

    ReactiveProperty<int> mAge = new ReactiveProperty<int>();

    void Start () {

        mAge.Subscribe(age=>Debug.Log(age));

        mAge.Value = 10;
        mAge.Value = 10;
        mAge.Value = 10;
        mAge.Value = 11;
    }
}

得到结果:

这里多输出了一个 0,这是因为,ReactiveProperty 会把初始默认值当做第一次值改变发送给 Subscribe 的委托。
解决方法Skip(1):

 mAge.Skip(1)
     .Subscribe(age=>Debug.Log(age));

这里我们要注意一下,ReactiveProperty 把初始值当做值改变事件发送是合理的,因为有的时候我们是需要得到初始值的,那么至于大家在使用时需不需要初始值,大家可以自行决定,而屏蔽掉初始值的方式就是通过 Skip 操作符。

Skip

skip 是 Linq 的操作符,意思是忽略,而 Skip 的参数是 Count,就是说可以通过 Count 来决定忽略多少个数据、事件。
在 ReactiveProperty 的用例代码中,Skip 的 Count 参数为 1,意思是忽略 1 个数据、事件。

总结

ReactiveProperty 是一个事件源(Observable),而 Skip 是一个 Linq 操作符。
ReactiveProperty 是经常使用的设计工具,笔者常用它来写 Model 层的代码。

ReactiveCollection

这是一种List形式的属性,用法和ReactiveProperty差不多:

public ReactiveCollection<PlayerData> mPlayerDataList = new ReactiveCollection<PlayerData>();

是支持类似于List的Add一类的函数的。这里先不用做过多的详解

基于UniRx的事件系统

案例一

代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UniRx;

public class UniRxTypeEventSystem 
{
    /// <summary>
    /// 接口 只负责存储在字典中
    /// </summary>
    interface IRegisterations
    {

    }

    /// <summary>
    /// 多个注册
    /// </summary>
    class Registerations<T> : IRegisterations
    {
        /// <summary>
        /// 不需要 List<Action<T>> 了
        /// 因为委托本身就可以一对多注册
        /// </summary>
        public Subject<T> Subject = new Subject<T>();
    }

    /// <summary>
    /// 
    /// </summary>
    private static Dictionary<Type, IRegisterations> mTypeEventDict = new Dictionary<Type, IRegisterations>();

    /// <summary>
    /// 注册事件
    /// </summary>
    /// <param name="onReceive"></param>
    /// <typeparam name="T"></typeparam>
    public static Subject<T> GetEvent<T>()
    {
        var type = typeof(T);

        IRegisterations registerations = null;

        if (mTypeEventDict.TryGetValue(type, out registerations))
        {
            var reg = registerations as Registerations<T>;
            return reg.Subject;
        }
        else
        {
            var reg = new Registerations<T>();
            mTypeEventDict.Add(type, reg);
            return reg.Subject;
        }
    }

    /// <summary>
    /// 发送事件
    /// </summary>
    /// <param name="t"></param>
    /// <typeparam name="T"></typeparam>
    public static void Send<T>(T t)
    {
        var type = typeof(T);

        IRegisterations registerations = null;

        if (mTypeEventDict.TryGetValue(type, out registerations))
        {
            var reg = registerations as Registerations<T>;
            reg.Subject.OnNext(t);
        }
    }
}

调用方法:

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UniRx;

public class UniRxTypeEventSystemTest : MonoBehaviour
{
    class A
    {
    }

    class B
    {
        public int    Age;
        public string Name;
    }

    IDisposable mEventADisposable;

    private void Start()
    {
        mEventADisposable = UniRxTypeEventSystem.GetEvent<A>()
            .Subscribe(ReceiveA); // 可以获取 IDisposable 对象

        UniRxTypeEventSystem.GetEvent<B>()
            .Subscribe(ReceiveB)
            .AddTo(this); // 可以绑定
    }

    void ReceiveA(A a)
    {
        Debug.Log("received A");
    }

    void ReceiveB(B b)
    {
        Debug.LogFormat("received B:{0} {1}", b.Name, b.Age);
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            UniRxTypeEventSystem.Send(new A());
        }

        if (Input.GetMouseButtonDown(1))
        {
            UniRxTypeEventSystem.Send(new B()
            {
                Age = 10,
                Name = "凉鞋"
            });
        }

        if (Input.GetKeyDown(KeyCode.U))
        {
            mEventADisposable.Dispose();
        }
    }
}

案例二(自写)

using System;
using System.Collections;
using System.Collections.Generic;
using QF;
using UniRx;
using UnityEngine;

/// <summary>
/// 接口 只负责存储在字典中
/// </summary>
interface IRegisterations
{

}
/// <summary>
/// 多个注册
/// </summary>
class Registerations : IRegisterations
{
    /// <summary>
    /// 委托本身就可以一对多注册
    /// </summary>
    public Subject<object[]> Subject = new Subject<object[]>();
}
public class MyEventSystem
{
    /// <summary>
    /// 存储已经注册的事件
    /// </summary>
    private static Dictionary<MyEventType, IRegisterations> mTypeEventDict 
        = new Dictionary<MyEventType, IRegisterations>();

    /// <summary>
    /// 注册事件
    /// </summary>
    public static Subject<object[]> GetEvent(MyEventType type)
    {

        IRegisterations registerations = null;

        if (mTypeEventDict.TryGetValue(type, out registerations))
        {
            var reg = registerations as Registerations;
            return reg.Subject;
        }
        else
        {
            var reg = new Registerations();
            mTypeEventDict.Add(type, reg);
            return reg.Subject;
        }
    }

    /// <summary>
    /// 发送事件
    /// </summary>
    public static void Send(MyEventType type, params object[] param)
    {

        IRegisterations registerations = null;

        if (mTypeEventDict.TryGetValue(type, out registerations))
        {
            var reg = registerations as Registerations;
            reg.Subject.OnNext(param);
        }
    }
    
}

调用方法:

MyEventSystem.Send(MyEventType.SelectNewPlayer, date);

MyEventSystem.GetEvent(MyEventType.SelectNewPlayer)
            .Subscribe(ChangePlayerShow)
            .AddTo(this);

网络操作

使用 ObservableWWW 进行异步网络操作。它的 Get/Post 方法返回可订阅(Subscribe)的 IObservables:

ObservableWWW.Get("http://google.co.jp/")
             .Subscribe(
                 x => Debug.Log(x.Substring(0, 100)), // onSuccess
                 ex => Debug.LogException(ex)); // onError

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 841774407@qq.com

×

喜欢就点赞,疼爱就打赏