【UniRx】第一季:入门精讲

前提:本人用的Unity2019.3.0f3,从AssetStore上直接下的UniRx 7.1.0;(摘自凉鞋)

【第一章节】

一、UniRx简介

UniRx 是一个 Unity3D 的编程框架。
专注于解决异步逻辑,使得异步逻辑的实现更加简洁优雅。

简洁优雅如何体现?
比如,实现一个”只处理第一次鼠标点击事件”这个功能,使用 UniRx 实现如下:

void Start()
    {
        Observable.EveryUpdate()                      //1.开启Update的事件监听
            .Where(_ => Input.GetMouseButtonDown(0))  //2.每次Update事件被调⽤时,进⾏⼀个⿏标是否抬起的条件判断。
            .First()                                  //3.如果判断通过,则进⾏计数,并且只获取第一次的点击的事件。
            .Subscribe(_ =>                           //4.订阅/处理事件
            {
                Debug.Log("鼠标已点击");
            });
    }

如果使⽤传统的⽅式实现”只处理第⼀次鼠标点击事件“,这个功能,不知道要写多少行代码,还要创建一个成员变量来记录点击次数,或者是否点击过。还要在脚本中创建一个 Update 方法来监听鼠标抬起事件。
如果在 Update 方法中,除了实现鼠标事件监听这个功能之外,还要实现其他的功能。那么Update 里就会充斥着大量的状态判断等逻辑。代码非常不容易阅读。

而 UniRx 提供了一种编程思维,使得平时一些比较难实现的异步逻辑(比如当前这种),使用 UniRx轻松搞定,并且不失代码的可读性。
当然 UniRx 的强大不仅如此。
它还可以:
•  优雅实现 MVP (MVC)架构模式。
•  对 UGUI/Unity API 提供了增强,很多需要写大量代码的 UI 逻辑,使用 UniRx 优雅实现。
•  轻松实现非常复杂的异步任务处理理。
•  等等。
最最重要的是,它可以提高我们的编码效率。还给我们的大脑提供一个强有力的编程模型。
UniRx 非常值得研究学习,就连大名鼎鼎的 uFrame 框架,在 1.6 版本之后使用 UniRx 做了大幅重构,底层使⽤ UniRx 强力驱动。

二、为什么要用 UniRx?

UniRx 就是 Unity Reactive Extensions。是 Unity 版本的 Reactive Extensions。
Reactive Extensions 的擅长的部分是处理时间上异步的逻辑。
游戏很多的系统都是在时间上异步的, 所以Unity 开发者要实现的异步(在时间上)任务,是⾮常多的。
这也是为什么 Unity 官⽅提供了 Coroutine (协程)这样的概念。
在游戏中,大部分的逻辑都是在时间上异步的。比如动画的播放、声音的播放、网络请求、资源加载/卸载、Tween、场景过渡等都是在时间上异步的逻辑。甚⾄是游戏循环(Every Update,OnCollisionEnter,etc),传感器数据(Kinect,Leap Motion,VR Input,etc.)都是(时间上)异步的事件。

我们往往在进行以上逻辑实现的时候经常用到大量的回调,最终随着项⽬的扩张导致传说中的”回调地狱”。
相对较好的方法则是使⽤消息/事件的发送,结果导致“消息满天飞”,导致代码非常难以阅读。
使⽤ Coroutine 也是非常不错的,但是 Coroutine 本身的定义,是以⼀个方法的格式定义的,写起来是非常面向过程的。当逻辑稍微复杂一点,就很容易造成 Coroutine 嵌套 Coroutine,代码是非常不容易阅读的(强耦合)。
而 UniRx 的出现刚好解决了这个问题,它介于回调和事件之间。它有事件的概念,只不过他的事件是像水⼀样流过来,而我们要做的则是简单地进行组织、变换、过滤、合并。它也用到了回调,只不过事件组织之后,只有简单一个回调就可以进行事件的处理了。
它的原理和 Coroutine(迭代器模式) 非常类似,但是比 Coroutine 强大得多。
UniRx 将(时间上)异步事件转化为 (响应式的) 事件序列,通过 LINQ 操作可以很简单地组合起来,还支持时间操作。
为什么要用 UniRx 呢?
总结为一句话就是,游戏本身有大量的(时间上)异步逻辑,而 UniRx 恰好擅长处理(时间上)异步逻辑,使⽤ UniRx 可以节省我们的时间,同时让代码更简洁易读。
Rx 只是一套标准,在其他语⾔也有实现,如果在 Unity 中熟悉了这套标准,在其他语言上也是可以很快上⼿的。比如 RxJava、Rx.cpp、SwiftRx 等等。

三、定时功能实现

方法一:一般定时器

using UnityEngine;

public class CommonTimerExample : MonoBehaviour
{
    private float mStartTime;

    void Start()
    {
        mStartTime = Time.time;
    }

    void Update()
    {
        if(Time.time-mStartTime>5)
        {
            Debug.Log("一般定时器");
            mStartTime = float.MaxValue;
        }
    }
}

方法二:Coroutine(协程)定时器

using System;
using System.Collections;
using UnityEngine;

public class CoroutineTimerExample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Timer(5, () =>
         {
             Debug.Log("协程定时器");
         }));
    }

    IEnumerator Timer(float seconds,Action callback)
    {
        yield return new WaitForSeconds(seconds);
        callback();
    }
}

方法三:UniRx定时器

using UnityEngine;
using System;
using UniRx;

public class UniRxTimerExample : MonoBehaviour
{
    void Start()
    {
        Observable.Timer(TimeSpan.FromSeconds(5.0f))
            .Subscribe(_ =>
            {
                Debug.Log("UniRx定时器");
            });
    }
}

比较可知,使用 UniRx很简单,当然以上代码是没有和 MonoBehaviour 进行生命周期绑定的。
要绑定很简单。

void Start()
    {
        Observable.Timer(TimeSpan.FromSeconds(5.0f))
            .Subscribe(_ =>
            {
                Debug.Log("UniRx计时器");
            })
            .AddTo(this);
    }

只要加上一个 AddTo(this) 就可以了了。
这样,当 this(MonoBehaviour) Destroy 的时候,这个延时逻辑也会销毁掉,从⽽避免造成空指针异常。
三行代码,大约 20 秒时间,就搞定了一个实现起来比较麻烦的逻辑。

四、独立的Update

监听鼠标左右键按下:

using UnityEngine;
using UniRx;

public class UpdateExample : MonoBehaviour
{
    enum ButtonState
    { 
        None,
        Clicked,
    }
    
    void Start()
    {
        ButtonState buttonState = ButtonState.None;
        bool buttonClicked = false;

        //监听鼠标左键
        Observable.EveryUpdate()
            .Subscribe(_ =>
            {
                if (Input.GetMouseButtonDown(0))
                {
                    Debug.Log("鼠标左键按下");
                    buttonClicked = true;
                }
            });

        //监听鼠标右键
        Observable.EveryUpdate()
            .Subscribe(_ =>
            {
                if (Input.GetMouseButtonDown(1))
                {
                    Debug.Log("鼠标右键按下");
                    buttonClicked = true;
                }
            });

        //监听鼠标状态
        if (buttonClicked && buttonState == ButtonState.None)
        {
            buttonState = ButtonState.Clicked;
        }
    }
}

虽然在代码长度上跟我们平时写的没有任何改善,但是最起码,这些 Update 逻辑互相之间独⽴了。
状态跳转、延时等等这些经常在 Update 里实现的逻辑,都可以使⽤以上这种方式独⽴。
我们使⽤ UniRx 对代码进行了一点改善,在接触 UniRx 之后,就再也没有使用过MonoBehaviour 提供的 Update 方法了。
不过这种 UniRx 的使⽤还比较初级,本节课所介绍的方式,随着对 UniRx 的深入,也会渐渐淘汰,因为后边有更好的实现方式。

五、AddTo

字⾯意思上理解为添加到。
添加到哪里呢?
其实就是 Unity 的 GameObject 或者 MonoBehaviour。

为什么要添加到 GameObject 或者 MonoBeaviour 呢?
是因为,GameObject 和 MonoBehaviour 可以获取到 OnDestroy 事件。也就是 GameObject 或者MonoBehaviour 的销毁事件。
那么用这个销毁事件干嘛呢?
答案是用来 进行与 UniRx 进行销毁事件的绑定,也就是当 GameObject 或者 MonoBehaviour 被销毁时,同样去销毁正在进行的 UniRx 任务。
这就是 AddTo API 的作用。
其实用起来很简单,代码如下:

Observable.Timer(TimeSpan.FromSeconds(1.0f)
.Subscribe()
.AddTo(this); // Or gameObejct

这样,当 this 所在的 GameObject 销毁时,这个 Timer 就会被销毁。

为什么会这样?
本质上, AddTo 是一个 静态扩展关键字,他对 IDisposable 进行了扩展。
只要任何实现了 IDisposable 的接口,都可以使用 AddTo API,不管是不是 UniRx 的 API。
当 GameObject 销毁时,就会调用 IDisposable 的 OnDispose 这个方法。
很容易理解。
AddTo 能做什么?
有了 AddTo,在开启 Observable.EveryUpdate 时调用当前脚本的方法,则不会造成引用异常等错
误,它使得 UniRx 的使用更加安全。

六、UniRx的基本语法格式

Observable.XXX().Subscribe() 是非常典型的 UniRx 格式。
只要理解什么意思就可以看懂大部分的 UniRx 的用法了。

首先解决词汇问题:
Observable: 可观察的,形容词,形容后边的词(Timer) 是可观察的,我们可以粗暴地把 Observable 后边的理解成发布者。
Timer: 定时器,名词,被 Observable 描述,所以是发布者,是事件的发送⽅。
Subscribe: 订阅,动词,订阅谁呢?当然是前边的 Timer,这里可以理解成订阅者,也就是事件的接收⽅。
AddTo: 暂不用理解。
连起来则是:可被观察(监听)的.Timer().订阅()
顺下来应该是:订阅可被观察的定时器。
其概念关系很容易理解。
•  Timer 是可观察的。
•  可观察的才能被订阅。

Observable.XXX().Subscribe();
可被观察(监听)的 XX,注册。
以上笔者从发布者和订阅者这个角度来进行的介绍,以便大家理解。
但是 UniRx 的侧重点,不是发布者和订阅者这两个概念如何使用,而是事件从发布者到订阅者之间的过程如何处理。
所以两个点不重要,重要的是两点之间的线,也就是事件的传递过程。
这里先不说得太深⼊,在入门之后,会用很大的篇幅去进行讲解。

七、操作符Where

UniRx 的侧重点,不是发布者和订阅者这两个概念如何使用,而是事件从发布者到订阅者之间的过程如何处理。

using UnityEngine;
using UniRx;

public class WhereExample : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
            .Where(_ => Input.GetMouseButtonDown(0))
            .Subscribe(_ =>
            {
                Debug.Log("鼠标点击");
            });

    }
}

Where 意思是在哪儿。这里我们理解成一个条件语句。也就是 if 语句。
类似:
if (Input.GetMouseButtonUp(0))
这段代码和之前的一样。
Where 是一个过滤的操作,过滤掉不满足条件的事件。

给大家一个比较容易理解的解释。
1. EveryUpdate 是事件的发布者。他会每帧会发送一个事件过来。
2. Subscribe 是事件的接收者,接收的是 EveryUpdate 发送的事件。
3. Where 则是在事件的发布者和接收者之间的一个过滤操作。会过滤掉不满足条件的事件。
所以,Subscribe 处理的事件,都是满足 Input.GetMouseButtonUp(0) 条件的事件。
看一下这两幅图,就可以理解了。

是圆形的,才可以通过。
事件的本身可以是参数,但是 EveryUpdate 没有参数,所以在 Where 这行代码中不需要接收参数,所以使用 _ 来表示,不用参数。当然 Subscribe 也是用了一个 _ 来接收参数。
在两幅图中,第一幅图,发送的事件的类型是整数类型。第二个不清楚,应该是自定义 class 吧。

八、操作符First

 

 很简单,就是获取第一个通过的事件。

using UnityEngine;
using UniRx;

public class FirstExample : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
            .First(_ => Input.GetMouseButtonDown(0))
            .Subscribe(_ =>
            {
                Debug.Log("鼠标点击");
            })
            .AddTo(this);
    }
}

九、UGUI的支持

UniRx 对 UGUI 进行了支持。
比如最常用的按钮点击事件注册。

using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class UIExample : MonoBehaviour
{
    void Start()
    {
        var button = GameObject.Find("Button").GetComponent<Button>();
        button.OnClickAsObservable()
            .Subscribe(_ => Debug.Log("按钮点击"));

        var toggle = GameObject.Find("Toggle").GetComponent<Toggle>();
        toggle.OnValueChangedAsObservable()
            .Where(on=>on)
            .Subscribe(on => Debug.Log(on));
    }
}

还支持 EventSystem 的各种 Trigger 接口的监听。

比如:Image 本身是 Graphic 类型的,Graphic 类,只要实现 IDragHandler 就可以进行拖拽事件的监听。
但是使用 UniRx 就不用那么麻烦。

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

public class UIExample : MonoBehaviour
{
    void Start()
    {
        var image = GameObject.Find("Image").GetComponent<Graphic>();
        image.OnBeginDragAsObservable().Subscribe(_ => Debug.Log("开始拖拽"));
        image.OnDragAsObservable().Subscribe(_ => Debug.Log("正在拖拽"));
        image.OnEndDragAsObservable().Subscribe(_ => Debug.Log("结束拖拽"));
    }
}

十、ReactiveProperty

UniRx 中有个一个非常强大的概念,叫做 ReactiveProperty。响应式属性。
强大在哪呢?它可以替代一切变量,给变量创造了很多功能。
假如我们想监听一个值是否发生了改变。
用通常的方法实现可能如下:

using UnityEngine;
using System;

public class ReactivePropertyExample : MonoBehaviour
{
    public Action<int> OnAgeChanged = null;
    private int mAge = 0;
    public int Age
    {
        get { return mAge;}
        set 
        {
            if(mAge!=value)
            {
                mAge = value;
                if(OnAgeChanged!=null)
                {
                    OnAgeChanged(value);
                }
            }
        }
    }

    void Start()
    {
        OnAgeChanged += age =>
          {
              Debug.Log("inner received age changed");
          };
    }
}

public class PersonView
{
    ReactivePropertyExample mReactivePropertyExample;
    void Init()
    {
        mReactivePropertyExample.OnAgeChanged += (age) =>
          {
              Debug.Log(age);
          };
    }
}

这样在类的内部,写一次 OnAgeChanged 是没问题的。但是我想在这个类的外部监听这个值的改变,那就要声明一个委托来搞定了。委托的维护成本比较低,是可以接受的,直到笔者发现了UniRx 的ReactiveProperty。就再也不想用委托来做这种工作了。

UniRx实现:

using UnityEngine;
using UniRx;

public class ReactivePropertyExample : MonoBehaviour
{
    public ReactiveProperty<int> Age = new ReactiveProperty<int>(0);

    void Start()
    {
        Age.Subscribe(age =>
          {
              Debug.Log("inner received age changed");
          });
        Age.Value = 10;
    }
}

public class PresonView
{
    ReactivePropertyExample mReactivePropertyExample;
    void Init()
    {
        mReactivePropertyExample.Age.Subscribe((age) =>
          {
              Debug.Log(age);
          });
    }
}

当任何时候,Age 的值被设置,就会通知所有 Subscribe 的回调函数。
而 Age 可以被 Subscribe 多次的。
并且同样支持 First、Where 等操作符。
这样可以实现一个叫做 MVP 的架构模式。
也就是在 Ctrl 中,进行 Model 和 View 的绑定。
Model 的所有属性都是用 ReactiveProperty,然后在 Ctrl 中进行订阅。
通过 View 更改 Model 的属性值。
形成一个 View->Ctrl->Model->Ctrl->View 这么一个事件响应环。

十一、MVP实现

我们来实现一个简洁的MVP 模式框架。

在上一讲有简单介绍过,UniRx 对 UGUI 进行了增强。UGUI 增强的原理很简单,就是对 UnityEvent 提供了 AsObservable 方法。

public Button mButton;
mButton.onClick.AsObservable().Subscribe(_=>Debug.Log("按钮点击"));

在此基础上,进一步对每个 UGUI 控件进行封装,从而可以像如下方式在 UGUI 中使用 UniRx。

public Toggle mToggle;
public InputField mInput;
public Text mText;
public Slider mSlider;
void Start()
{
    mToggle.OnValueChangedAsObservable().SubscribeToInteractable(mButton);
    mInput.OnValueChangedAsObservable()
        .Where(x=>x!=null)
        .SubscribeToText(mText);
    mSlider.OnValueChangedAsObservable()
        .SubscribeToText(mText,x=>Math.Round(x,2).ToString());
}

在实现MVP模式之前,先看下下面的代码:

public class Enemy
{
    public ReactiveProperty<long> CurrentHp{get;private set;}
    public ReactiveProperty<long> IsDead{get;private set;}
    public Enemy(int initialHp)
    {
        CurrentHp=new ReactiveProperty<long>(initialHp);
        IsDead=CurrentHp.Select(x=> x<=0).ToReactiveProperty();
    }
}

void Start()
{
   mButton.OnClickAsObservable().Subscribe(_=>enemy.CurrentHp.Value-=99);
   enemy.CurrentHp.SubscribeToText(MyText);
   enemy.IsDead.Where(isDead=>isDead)
               .Subscribe(_=>
               {
                  mButton.interactable=false;
               });
}

这段代码理解起来非常简单,Enemy 是一个数据类,我们可以理解成 Model。
而下边的 Start 部分则是 Ctrl 的代码。它将 Hierarchy 中的 UI 控件 与 Model 绑定在了一起。当
Model 有改变则通知 UI 更新,当从 UI 接收到点击事件则对 Model 进行值的更改。这就是一个非常简单的 MVP 模式。

你可以⽤ UnityEvent.AsObservable 将 ReactiveProperties,ReactiveCollections 和Observables 都组合起来。所有 UI 组件都提供了 XXXAsObservable在 Unity 里,序列化是一个很重要的功能,如果不可序列化,则在编辑器上就看不到参数。而ReactiveProperty 是泛型的,序列化起来比较麻烦。为了解决这个问题,UniRx 支持了可序列化的ReactiveProperty 类型,比如 Int/LongReactivePropety、Float/DoubleReactiveProperty、
StringReactiveProperty、BoolReactiveProperty,还有更多,请参见 InspectableReactiveProperty.cs。
以上都可以在 Inspector 中编辑。对于自定义的枚举 ReactiveProperty,写一个可检视的
ReactiveProperty[T] 也很容易。
如果你需要 [Multiline] 或者[Range] 添加到 ReactiveProperty 上,你可以使⽤
MultilineReactivePropertyAttribute 和 RangeReactivePropertyAttribute 替换 Multiline 和 Range。
这些 InspectableReactiveProperties 可以在 inspector 面板显示,并且当他们的值发生变化时发出通知,甚至在编辑器里变化也可以。
这个功能是实现在 InspectorDisplayDrawer。你可以通过继承这个类实现你自定义的
ReactiveProperties 在 Inspector 面板的绘制:

public enum Fruit
{
   Apple,Grape
}
[Serializable]
public class FruitReactiveProperty:ReactiveProperty<Fruit>
{
   public FruitReactiveProperty()
   {
   }
   public FruitReactiveProperty(Fruit initialValue):base(initialValue)
   {
   }
}

[UnityEditor.CustomPropertyDrawer(typeof(FruitReactiveProperty))]
[UnityEditor.CustomPropertyDrawer(typeof(YourSpecializedReactiveProperty2))]
//and others...
public class ExtendInspectorDisplayDrawer:InspectorDisplayDrawer
{
}

如果 ReactiveProperty 的值只在 Stream 中更新,你可以使用 ReadOnlyReactiveProperty 让这个属性只读。

MVP 设计模式 Model-View-(Reactive)Presenter Pattern

使用 UniRx 可以很容易地实现 MVP(MVRP)设计模式。
MVP 的结构图如下所示。

为什么应该用 MVP 模式而不是 MVVM 模式?Unity 没有提供 UI 绑定机制,创建一个绑定层过于复杂并且会对性能造成影响(使用反射)。尽管如此,视图还是需要更新。Presenters 层知道 View 组件并且能更新它们。
虽然没有真的绑定,但 Observables 可以通知订阅者,功能上也差不多。这种模式叫做 Reactive
Presenter:

// Presenter for scene(canvas) root.
public class ReactivePresenter : MonoBehaviour
{
    // Presenter is aware of its View (binded in the inspector)
    public Button mButton;
    public Toggle mToggle;
    // State-Change-Events from Model by ReactiveProperty
    Enemy enemy = new Enemy(1000);
void Start()
{
    // Rx supplies user events from Views and Models in a reactive manner
    mButton.OnClickAsObservable().Subscribe(_=>enemy.CurrentHp.Value -=
99);
    mToggle.OnValueChangedAsObservable().SubscribeToInteractable(mButton);
    // Models notify Presenters via Rx,and Presenters update their views
    enemy.CurrentHp.SubscribeToText(MyText);
    enemy.IsDead.Where(isDead => isDead)
      .Subscribe(_=>
      {
          mToggle.interactable = mButton.interactable = false;
      });
}
}

// The Mode. All property notify when their values change
public class Enemy
{
    public ReactiveProperty<long> CurrentHp { get;private set;}
    public ReactiveProperty<bool> IsDead { get;private set;}
    public Enemy(int initialHp)
    {
       // Declarative Property
       CurrentHp = new ReactiveProperty<long>(initialHp);
       IsDead = CurrentHp.Select(x => x <= 10).ToReactiveProperty();
    }
}

在 Unity 中,我们把 Scene 中的 GameObject 当做视图层,这些是在 Unity 的 Hierarchy 中定义的。
展示/控制层在 Unity 初始化时将视图层绑定。
SubscribeToText and SubscribeToInteractable 都是简洁的类似绑定的辅助函数。虽然这些工具很简单,但是非常实用。
在 Unity 中使用开发体验非常平滑,性能也很好,最重要的是让你的代码更简洁。
View -> ReactiveProperty -> Model -> RectiveProperty - View 完全用响应式的方式连接。UniRx 提供了所有的适配方法和类,不过其他的 MVVM (or MV*) 框架也可以使用。UniRx/ReactiveProperty 只是一个简单的⼯工具包。
使用 UniRx 实现的 MVP 模式结构图如下:

 

十二、操作符Merge

Merge 意思是合并。
合并什么呢?
我们之前学习过,UniRx 的世界里,任何东西都是以事件流的形式存在的。
而在之前我们使用的 Update、Timer 等,全都是开启了一条事件流。
但是到现在为止,老师有说过 UniRx 只能 Subscribe 一条事件流么?
没有。
UniRx 可以开启两个或多个事件流。
并使用 Merge 进行事件流的合并。

比如:

using UnityEngine;
using UniRx;

public class MergeExample : MonoBehaviour
{
    void Start()
    {
        var leftClickEvents = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0));
        var rightClickEvents = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(1));

        Observable.Merge(leftClickEvents, rightClickEvents)
            .Subscribe(_ =>
            {
                Debug.Log("鼠标点击");
            });
    }
}

以上代码的实现的逻辑是“当鼠标左键或右键点击时都会进行处理“。
也就是说,Merge 操作符将 leftMouseClickStream 和 rightMouseClickStream 合并成了一个事件流。
如下图所示:

 十三、实现“某个按钮点击时,使当前页面所有的按钮不可被点击”

我们一般的实现是加上一个事件的遮罩层,或者创建一个 bool 变量来进行标记。
而使用 UniRx 则会简单得多。
代码如下:

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

public class PanelEventLockExample : MonoBehaviour
{
    void Start()
    {
        var btnA = GameObject.Find("ButtonA").GetComponent<Button>();
        var btnB = GameObject.Find("ButtonB").GetComponent<Button>();
        var btnC = GameObject.Find("ButtonC").GetComponent<Button>();

        var aStream = btnA.OnClickAsObservable();
        var bStream = btnB.OnClickAsObservable();
        var cStream = btnC.OnClickAsObservable();

        Observable.Merge(aStream, bStream, cStream)
            .First()
            .Subscribe(_ =>
            {
                Debug.Log("按钮点击");
                Observable.Timer(TimeSpan.FromSeconds(3.0f)).Subscribe(__ =>
                {
                    GameObject.Find("Canvas").SetActive(false);
                });
            });
    }
}

使用 Merge 将三个按钮的事件流合并。并通过 First 只处理第一次点击事件。
这样标题的所说的功能就完成了。

十四、使用 Select 操作符进行功能改进

但是还有一点问题,就是当处理按钮事件的时候要知道是哪个按钮被点击了。
很简单,使用 Select 操作符就好了。
Select 是什么意思呢?
就是选择。
Select 本身是 Linq 的操作符。
一般是传⼊一个索引 i/index 然后根据索引返回具体的值。
对于一个 List,什么叫做 Selecte 呢?
看代码就懂了,如下:

var testNumbers = new List<int>(){ 1,2,3}
var selectedValue = testNumbers[2];

其中 testNumbers[2] 就是一个选择操作。
Select 的操作符会在第二章进行详细解释。
先用下边的图,试着理解一下就好。

 这里我们只要了解,使用 Select 操作符后,它返回的是一个值就好了,值的类型根据它返回的值的类型决定,也就是说是一个泛型的。
使用 Select 之后的代码如下:

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

public class PanelEventLockExample : MonoBehaviour
{
    void Start()
    {
        var btnA = GameObject.Find("ButtonA").GetComponent<Button>();
        var btnB = GameObject.Find("ButtonB").GetComponent<Button>();
        var btnC = GameObject.Find("ButtonC").GetComponent<Button>();

        var aStream = btnA.OnClickAsObservable().Select(_ => "A");
        var bStream = btnB.OnClickAsObservable().Select(_ => "B");
        var cStream = btnC.OnClickAsObservable().Select(_ => "C");

        Observable.Merge(aStream, bStream, cStream)
            .First()
            .Subscribe(btnId =>
            {
                Debug.LogFormat("按钮{0}点击",btnId);
                Observable.Timer(TimeSpan.FromSeconds(3.0f)).Subscribe(__ =>
                {
                    GameObject.Find("Canvas").SetActive(false);
                });
            });
    }
}

这样,标题所说的功能,完美实现了。
第一章的内容就结束了。
Merge 属于处理多个流的操作符,除此之外还有更多的类似的,比如 Zip 等等。学习完它们之后可以实现非常复杂的逻辑,比如 Coroutine 的支持,可以实现按顺序执行多个 Coroutine ,也支持等待所有 Coroutine 执行完成这种复杂的操作。

【第二章节】

一、简介

UniRx 之所以叫做 UniRx 是因为它是 Unity 版本的 Reactive Extensions。
是单独对 Unity 做了很多功能上的增强。
•  UI 增强
•  GameObject/MonoBehaviour 增强以及引擎的事件增强(OnApplicationPause、 UnityEvent)
•  Coroutine/Thread 增强
•  网络请求(WWW 等)
•  ReactiveProperty、ReactiveCollection、ReactiveDictionary 等。
•  ReactiveCommand 命令系统。
•  …
学习完以上的内容可以让我们的日常开发事半功倍。

二、UI增强

作为初学者,在日常开发中接触得最多的就是 UGUI 了。而 UGUI 的开发大多需要遵循着一个 MVC的模式。
但是 MVC 模式对很多人来说是一个非常模糊的架构模式。但是本质很简单,想办法把表现和数据分离。也就是 View 和 Model 分离。
用 Unity 实现的方式有非常多种。
而用 UniRx 的 Reactive Property 则是可以完全实现一种 MVC 的变种(MVP),并且是非常明确的。这样在开发的时候就不用再去纠结怎么实现了。
单单这一个概念,就让一个 UGUI 的开发简化了很多
除此之外,还支持了非常多的 UGUI 控件。
所有的 UGUI 控件支持列出如下 :

[SerializeField] Button mButton;
[SerializeField] Toggle mToggle;
[SerializeField] Scrollbar mScrollbar;
[SerializeField] ScrollRect mScrollRect;
[SerializeField] Slider mSlider;
[SerializeField] InputField mInputField;
void Start()
{
    mButton.OnClickAsObservable().Subscribe(_ => Debug.Log("On Button
Clicked"));
    mToggle.OnValueChangedAsObservable().Subscribe(on => Debug.Log("Toggle " +
on));
    mScrollbar.OnValueChangedAsObservable().Subscribe(scrollValue =>
Debug.Log("Scrolled " + scrollValue));
    mScrollRect.OnValueChangedAsObservable().Subscribe(scrollValue =>
Debug.Log("Scrolled " + scrollValue);
    mSlider.OnValueChangedAsObservable().Subscribe(sliderValue =>
Debug.Log("Slider Value " + sliderValue));
    mInputField.OnValueChangedAsObservable().Subscribe(inputText =>
Debug.Log("Input Text: " + inputText));
    mInputField.OnEndEditAsObservable().Subscribe(result =>
Debug.Log("Result :" + result));
}

以上就是所有的 Observable 支持。
当然除了 Observable 增强,还支持了 Subscribe 的增强。
比如 SubscribeToText

Text resultText = GetComponent<Text>();
mInputField.OnValueChangedAsObservable().SubscribeToText(resultText);

这段代码实现的功能是,当 mInputField 的输入值改变,则会马上显示在 resultText 上。也就是完成了,mInputField 与 resultText 的绑定。
除此之外还支持,SubscribeToInteractable。基本上这样就够用了。
本节课介绍的就是 UniRx 对 UI 的全部支持。

三、登录注册界面

using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class LoginPanel : MonoBehaviour
{
    Button mLoginBtn; 
    Button mRegisterBtn;
    InputField mUsername;
    InputField mPassword;

    void Start()
    {
        mLoginBtn = transform.Find("Login").GetComponent<Button>();
        mRegisterBtn = transform.Find("Register").GetComponent<Button>();
        mUsername = transform.Find("Username").GetComponent<InputField>();
        mPassword = transform.Find("Password").GetComponent<InputField>();

        mLoginBtn.OnClickAsObservable().Subscribe(_ =>
        {
            Debug.Log("loginBtn clicked");
        }); 
        mUsername.OnEndEditAsObservable().Subscribe(result =>
        {
            //回车键 直接进入下一行
            mPassword.Select();
        });
        mPassword.OnEndEditAsObservable().Subscribe(result =>
        {
            mLoginBtn.onClick.Invoke();
        });

        mRegisterBtn.OnClickAsObservable().Subscribe(_ =>
        {
            LoginRegisterExample.PanelMgr.loginPanel.gameObject.SetActive(false);
            LoginRegisterExample.PanelMgr.registerPanel.gameObject.SetActive(true);
        });
    }
}
using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class RegisterPanel : MonoBehaviour
{
    Button mRegisterBtn;
    Button mBackBtn;
    InputField mUsername;
    InputField mPassword1;
    InputField mPassword2;

    void Start()
    {
        mRegisterBtn = transform.Find("Register").GetComponent<Button>();
        mBackBtn = transform.Find("ReturnLogin").GetComponent<Button>();
        mUsername = transform.Find("Username").GetComponent<InputField>();
        mPassword1 = transform.Find("Password1").GetComponent<InputField>();
        mPassword2 = transform.Find("Password2").GetComponent<InputField>();

        mUsername.OnEndEditAsObservable().Subscribe(result =>
        {
            //回车键 直接进入下一行
            mPassword1.Select();
        });
        mPassword1.OnEndEditAsObservable().Subscribe(result =>
        {
            mPassword2.Select();
        }); 
        mPassword2.OnEndEditAsObservable().Subscribe(result =>
        {
            mRegisterBtn.onClick.Invoke();
        });

        mBackBtn.OnClickAsObservable().Subscribe(_ =>
        {
            LoginRegisterExample.PanelMgr.registerPanel.gameObject.SetActive(false);
            LoginRegisterExample.PanelMgr.loginPanel.gameObject.SetActive(true);
        });
    }
}
using UnityEngine;

public class LoginRegisterExample : MonoBehaviour
{
    public LoginPanel loginPanel;
    public RegisterPanel registerPanel;

    public static LoginRegisterExample PanelMgr;

    void Awake()
    {
        PanelMgr = this;
    }

    void Start()
    {
        loginPanel = transform.Find("LoginPanel").GetComponent<LoginPanel>();
        registerPanel = transform.Find("RegisterPanel").GetComponent<RegisterPanel>();
        loginPanel.gameObject.SetActive(true);
        registerPanel.gameObject.SetActive(false);
    }
}

四、Unity生命周期与Trigger

Observable.EveryUpdate()就是支持的Unity的API。

单单Update就是支持非常多细分类型的Update事件捕获。

Observable.EveryFixedUpdate().Subscribe(_ => {});
Observable.EveryEndOfFrame().Subscribe(_ => {});
Observable.EveryLateUpdate().Subscribe(_ => {});
Observable.EveryAfterUpdate().Subscribe(_ => {});
除了 Update 还支持其他的事件,⽐如 ApplicationPause,Quit 等。
Observable.EveryApplicationPause().Subscribe(paused => {});
Observable.EveryApplicationFocus().Subscribe(focused => {});
Observable.EveryApplicationQuit().Subscribe(_ => {}):
学习了以上这些,就不用再去创建一个单例类去实现一个诸如“应用程序退出事件监听”这种逻辑了。
命名几行代码就可以搞定的事情,何必再去创建一个类去搞定?

Trigger 简介
Observable.EveryUpdate() 这个 API 有的时候在某个脚本中实现,需要绑定 MonoBehaviour 的生命周期(主要是 OnDestroy),当然也有的时候是全局的,而且永远不会被销毁的。
需要绑定 MonoBehaviour 生命周期的 EveryUpdate。只需要一个 AddTo 就可以进行绑定了。⾮非常简单,代码如下:
Observable.EveryUpdate()
.Subscribe(_ => {})
.AddTo(this);
但其实有更简洁的实现:

this.UpdateAsObservable()
.Subscribe(_ => {});

(命名空间:Using UniRx.Triggers;)

这种类型的 Observable 是什么呢?
答案是:Trigger,即触发器。
字如其意,很容易理解。

Trigger 类型的关键字
触发器,字如其意,是当某个事件发生时,则会将该事件发送到 Subscribe 函数中,而这个触发器,本身是一个功能脚本,这个脚本挂在 GameObject 上,来监听 GameObject 的某个事件发⽣生,事件发生则会回调给注册它的 Subscribe 中。触发器的操作和其他的事件源 (Observable) 是一样的,都支持 Where、First、Merge 等操作符。
Trigger 类型的 Observable 和我们之前讲的所有的 Observable 在表现上有一点不一样:
1. Trigger 大部分都是 XXXAsObsrevable 命名形式的。
2. 在使用 Trigger 的 GameObject 上都会挂上对应的 Observable XXXTrigger.cs 的脚本。

Trigger 在此之前我们是接触过的。
AddTo() 这个 API 其实是封装了一种 Trigger: ObservableDestroyTrigger。
顾名思义,就是当 GameObject 销毁时获取事件的一个触发器。
一般的 Trigger 都会配合 MonoBehaviour 一起使用。
比如 ObservableDestroyTrigger 的使用代码如下:

this.OnDestroyAsObservable()
.Subscribe(_ => {});
除了 Destroy 还有非常多的 Trigger。
比如各种细分类型的 Update:
this.FixedUpdateAsObservable().Subscribe(_ => {});
this.LateUpdateAsObservable().Subscribe(_ => {});
this.UpdateAsObservable().Subscribe(_ => {});
还有各种碰撞的 Trigger:

this.OnCollisionEnterAsObservable(collision => {});
this.OnCollisionExitAsObservable(collision => {});
this.OnCollisionStayAsObservable(collision => {});
// 同样 2D 的也支持
this.OnCollision2DEnterAsObservable(collision2D => {});
this.OnCollision2DExitAsObservable(collision2D => {});
this.OnCollision2DStayAsObservable(collision2D => {});
一些脚本的参数监听:
this.OnEnableAsObservable().Subscribe(_ => {});
this.OnDisableAsObservable().Subscribe(_ => {});

除了 MonoBehaviour ,Trigger 也支持了其他组件类型,比如 RectTransform、Transform、
UIBehaviour 等等。这里不再赘述。
详情可以查看 ObservableTriggerExtensions.cs 和 ObervableTriggerExtensions.Component.cs 中的API。

五、UI Triggers

Trigger 也有支持 UI 的部分。
在上堂课的结尾说过,Trigger 除了 MonoBehaviour 还支持其他的类型,比如 Transform、
RectTransform、还有 UIBehaviour。
那么 这个 UIBehaviour 就是 本文要讲解的重点。
为什么?
因为 UIBehaivour 是 UGUI 所有控件的基类。
只要支持 UIBehaivour,就支持所有的 UGUI 控件等会继承 UIBehaviour 的支持。

那么从哪方⾯支持呢?
是从各种事件开始支持的。
比如所有的 Graphic 类型都支持 OnPointerDownAsObservable、OnPointerEnterAsObservable、OnPointerEnterAsObservable 等 Trigger。
Graphic 简单介绍下,所有的在 Inspector 上显示,Raycast Target 选定框的都是 Graphic 类型,包括Image、Text 等全部都是。

也就是说 Image、Text 全部支持 OnPointerDownAsObservable、OnPointerEnterAsObservable 等Trigger。
我们知道,如果想自己去接收一个 OnPointerDown 事件,需要实现一个 IPointerDownHandler 接口,而 UniRx 则把所有的 IXXXHandler 接口都做成 Trigger了。
这样再也不⽤需要网上到处流传的 UIEventListener.Get(gameObejct).onClick 这种方式了。
因为这种方式问题很多,比如,由于它继承了 EventTriggers,实现了所有的事件接口,他就会吞噬掉OnScroll 等事件。
而 UniRx 的实现非常细,也就是 一个 IXXXHandler 就是一个 Trigger(本来老师的 QFramework 也想全部都实现了)。需要一个全部实现并且吞并事件的版本也没关系,UniRx 也实现了一个 ObservableEventTrigger。和UIEventListener 一样的。
老师在项目中用的比较多的几个 Trigger:

mImage.OnBeginDragAsObservable().Subscribe(dragEvent => {});
mGraphic.OnDragAsObservable().Subscribe(dragEvent => {});
mText.OnEndDragAsObservable().Subscribe(dragEvent => {});
mImage.OnPointerClickAsObservable().Subscribe(clickEvent => {});
非常方便,导致 QFramework 的一些脚本都弃用了,哈哈哈。
除了常用的几个 Trigger 之外 还有非常多的实用的 Trigger。比如: OnSubmitAsObservable、OnDropAsObservable 等等。
具体可以参考 ObservableTriggerExtensions.Component.cs,只要能想到的 基本上 UniRx 都支持。
忘了说一点, 要使用 各种 Trigger 类型,就要导入命名空间: using UniRx.Triggers;

六、Coroutine的操作

UniRx 对 Unity 的 Coroutine 也提供支持,可以将一个 Coroutine 转化为事件源(Observable)。

using System.Collections;
using UnityEngine;
using UniRx;

public class RxCoroutineTest : MonoBehaviour
{
    IEnumerator CoroutineA()
    {
        yield return new WaitForSeconds(1f);
        Debug.Log("A");
    }

    void Start()
    {
        Observable.FromCoroutine(_ => CoroutineA())
            .Subscribe(_ =>
            {
                //do something
            }).AddTo(this);
    }
}

一秒之后,输出结果为:
A
非常简单。
当然也支持将 Observable 转化为一个 Coroutine 中的 yield 对象。
比如:

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

public class Rx2YieldTest : MonoBehaviour
{
    IEnumerator Delay1Second()
    {
        yield return Observable.Timer(TimeSpan.FromSeconds(1f)).ToYieldInstruction();
        Debug.Log("B");
    }

    void Start()
    {
        StartCoroutine(Delay1Second());
    }
}

一秒之后,输出结果为:
B
FromCoroutine 和 ToYieldInstruction 实现了 Observable 与 Coroutine 之间的互相转化。
而在之前说过,Observable 是一条事件流。UniRx 的操作符,比如 Merge 可以处理多个流,可以将流进行合并。
除了合并也支持别的操作,比如 顺序 (依赖) 执行 Coroutine,并行执行 Coroutine 等等。
在之后,通过学习新的操作符,可以让 Coroutine 更加强大。

七、WhenAll:Coroutine 的并行操作

WhenAll 意思是,当所有的。
当所有的什么呢?
就是当所有的事件流都结束,就会触发 Subscribe 注册的回调。
使用 WhenAll 可以实现 Coroutine 的并行操作。

using System.Collections;
using UnityEngine;
using UniRx;

public class WhenAllCoroutineTest : MonoBehaviour
{
    IEnumerator A()
    {
        yield return new WaitForSeconds(1f);
        Debug.Log("A");
    }

    IEnumerator B()
    {
        yield return new WaitForSeconds(2f);
        Debug.Log("B");
    }

    void Start()
    {
        var aStream = Observable.FromCoroutine(_ => A());
        var bStream = Observable.FromCoroutine(_ => B());
        Observable.WhenAll(aStream, bStream)
            .Subscribe(_ =>
            {

            }).AddTo(this);
    }
}

一秒后输出结果为:
A
两秒后输出结果为:
A
B
WhenAll 和 Merge 是同类型的,是处理多个流的操作符。
理解起来非常简单。
除了并行实现 Coroutine 之外,还可以实现,当所有的按钮都点击过一次的逻辑。

using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class ButtonAllClickedOnce : MonoBehaviour
{
    [SerializeField] Button mButtonA;
    [SerializeField] Button mButtonB;
    [SerializeField] Button mButtonC;

    void Start()
    {
        var aStream = mButtonA.OnClickAsObservable().First();
        var bStream = mButtonB.OnClickAsObservable().First();
        var cStream = mButtonC.OnClickAsObservable().First();

        Observable.WhenAll(aStream, bStream, cStream)
            .Subscribe(_ =>
            {
                Debug.Log("三个按钮都点击了");
            }).AddTo(this);
    }
}

当点击完,A、B、C 按钮之后就会输出:
三个按钮都点击了
WhenAll 可以配合非常多的操作符使用。理解也非常简单。

八、事件流的结束OnCompleted

UniRx的结束事件。

有的事件流是有结束事件的,比如 Timer、First、Coroutine 等。
有的则没有,比如 EveryUpdate 等。
使用 Subscribe API 进行订阅的时候,第一个参数是 OnNext 回调的注册,这也是我们大部分情况下使用的回调。第二个蚕⻝则是 OnComplete
代码如下:

using System.Collections;
using UnityEngine;
using UniRx;

public class OnCompletedExample : MonoBehaviour
{
    void Start()
    {
        Observable.FromCoroutine(A)
            .Subscribe(_ =>
            {
                Debug.Log("接下来");
            }, () =>
             {
                 Debug.Log("完成");
             });
    }

    IEnumerator A()
    {
        yield return new WaitForSeconds(2f);
    }
}

2 秒后输出结果为:

接下来

完成

九、Start:让多线程更简单

多线程,是作为高级开发者必须具备的一种技术。了解了多线程可以让我们充分利用多核移动端的计算优势,也可以让我们的游戏体验更平滑。
在 Unity 中我们一般用 Thread.Start 开启一个线程。当逻辑非常复杂的时候多线程非常难以管理理。
而 UniRx 改善了这一种状况。
一个”当所有线程运行完成后,在主线程执行某个任务” 这个功能,使用 UniRx 实现如下:

using System;
using UnityEngine;
using UniRx;

public class ThreadTest : MonoBehaviour
{
    void Start()
    {
        var threadAStream = Observable.Start(() =>
          {
              System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
              return 10;
          });

        var threadBStream = Observable.Start(() =>
          {
              System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
              return 10;
          });

        Observable.WhenAll(threadAStream, threadBStream)
            .ObserveOnMainThread()
            .Subscribe(xs =>
            {
                Debug.Log(xs[0] + ":" + xs[1]);
            });
    }
}

3 秒后,输出的结果如下:
10:10
这里有两个新的 API,一个是 Observable.Start,这个 API 意思开启一个线程流。
ObserveOnMainThread,意思是把 WhellAll 结果转到主线程上。这样 Subscribe 里的回调就可以使用Unity 的 API 了(Unity 的很多 API 不可以在其他线程中使用 )。
使用 UniRx 来处理线程逻辑非常简单。
线程和 Coroutine (协程)都可以使用 WhenAll 这种操作符。
除了WhenAll, 还有很多其他的操作符,我们在之后慢慢学习。

十、ObservableWWW 优雅的网络请求操作

以往我们不管使用 WWW 还是 UnityWebRequest 都要使用 Coroutine 去驱动。
但是使用协程写出来的代码,需要一堆判断,导致代码非常混乱。
而 UniRx 则是以往一样简练的风格提供了对网络请求的支持。
代码如下:

ObservableWWW.Get("http://sikiedu.com")
            .Subscribe(_ =>
            {
                //todo something
            }).AddTo(this);

非常简单。
当然,ObservableWWW 同样支持 WhenAll 操作符。
代码如下:

var aStream = ObservableWWW.Get("http://sikiedu.com");
        var bStream = ObservableWWW.Get("http://qframework.io");
        Observable.WhenAll(aStream,bStream)
            .Subscribe(_ =>
            {
                //todo something
            }).AddTo(this);

除了 Get 也支持了Post,还有 GetWWW 和 PostWWW 这种的辅助封装,还有 GetAndGetBytes 和PostAndGetBytes。
列出 QFramework 中一段下载文件的代码:

// http://liangxiegame.com/media/QFramework_v0.0.9.unitypackage
protected override void OnBegin()
{
...
var progressListener = new ScheduledNotifier<float>();
ObservableWWW.GetAndGetBytes(mRequestPackageData.DownloadUrl, null,
progressListener)
.Subscribe(bytes =>
{
...
});
progressListener.Subscribe(OnProgressChanged);
}
private void OnProgressChanged(float progress)
{
EditorUtility.DisplayProgressBar("插件更更新",
"插件下载中 {0:P2}".FillFormat(progress), progress);
}

ObservableWWW 的 API 都可以传进去一个 ScheduledNotifier<T>() ,用来监听下载进度的。
Subscribe 之后传回来的值则是,当前的进度。
而且 ObservableWWW 的 Get 和 Post 请求都可以自己传对应的 header 和 WWWForm。
除了常用的 Get 和 Post 请求,也对 AssetBundle 的加载也做了简单的封装。
提供了诸如 ObservableWWW.LoadFromCacheOrDownload 这样的 API。

如果想深⼊了解,可以参考 ObservableWWW.cs
总之对 WWW 提供的 API 非常简练,也足够使用。

十一、ReactiveCommand

我们先来看下 ReactiveCommand 定义

public interface IReactiveCommand<T> : IObservable<T>
{
IReadOnlyReactiveProperty<bool> CanExecute { get; }
bool Execute(T parameter);
}

它提供了两个 API:
•  CanExecte
•  Execute
Execute 方法是被外部调用的。也就是这个 Command 的执行。这个很容易理解,只要外部调用的
Execute 就会执行。
而 CanExecute 则是内部使用的,并且对外部提供了只读访问。
当 CanExecute 为 false 时,在外部调用 Execute 则该 Command 不会被执行。
当 CanExecute 为 true 时,在外部调用 Execute 则该 Command 会被执行。
是什么决定 CanExecute 为 false 或 true 呢?
答案是其他的 Observable。
新创建的 ReactiveCommand 默认 CanExecute 为 true。
我们看下代码就好了。

using UnityEngine;
using UniRx;

public class ReactiveCommandExample : MonoBehaviour
{
    void Start()
    {
        ReactiveCommand command = new ReactiveCommand();
        command.Subscribe(_ =>
        {
            Debug.Log("command executed");
        });

        command.Execute();
        command.Execute();
        command.Execute();
    }
}

输出结果为:
command executed
command executed
command executed
非常地简单,只要调用 Execute。command 就会通知 Subscribe 的回调(因为 CanExecute 为 true)。
CanExecute 的开启关闭是由 Observable (事件源)决定的。

示例代码如下:

using UnityEngine;
using UniRx;

public class MouseUpExample : MonoBehaviour
{
    void Start()
    {
        var leftMouseClickStream = Observable.EveryUpdate().Where(_ =>
          Input.GetMouseButtonDown(0)).Select(_ => true);
        var rightMouseClickStream = Observable.EveryUpdate().Where(_ =>
          Input.GetMouseButtonUp(0)).Select(_ => false);
        var mouseUp = Observable.Merge(leftMouseClickStream, rightMouseClickStream);
        var reactiveCommand = new ReactiveCommand(mouseUp, false);
        reactiveCommand.Subscribe(x =>
        {
            Debug.Log(x);
        });
        Observable.EveryUpdate()
            .Subscribe(_ =>
            {
                reactiveCommand.Execute();
            });
    }
}

当按下鼠标时持续输出 ”()”,当抬起鼠标时,则停止输出。
非常容易理解。
当然 ReactiveCommand 也是可以被订阅(Subscribe) 的,在订阅之前呢,也可以使用 Where 等操作符进行事件操作。
示例代码如下:

using UnityEngine;
using UniRx;

public class OperatorExample : MonoBehaviour
{
    void Start()
    {
        var reactiveCommand = new ReactiveCommand<int>();
        reactiveCommand.Where(x => (x % 2 == 0)).Subscribe(x =>
              Debug.LogFormat("{0} is Even numbers",x));
        reactiveCommand.Where(x => (x % 2 != 0)).Timestamp().Subscribe(x =>
              Debug.LogFormat("{0} is Odd,{1}", x.Value, x.Timestamp));
        reactiveCommand.Execute(2);
        reactiveCommand.Execute(3);
    }
}

输出结果为:

2 is Even numbers

3 is Odd,2021/10/20 6:33:57 +00:00
到此,ReactiveCommand 的基本用法,大家应该掌握了。
我们通过以下两图,简单去理解下 ReactiveCommand 执行原理。

ReactiveCommand 除了能做以上一些简单的事情外,其实可以做非常多强大的功能。
但是要介绍非常强大的功能之前呢,我们要先学好 UniRx 的入门基础及一点点原理。
所以今天呢,只是对 ReactiveCommand 进行了一个简介,在之后呢会对 ReactiveCommand 进行一个深⼊地了解。

十二、ReactiveCollection 与 ReactiveDictionary

ReactiveCollection 类似于 List。
我们可以使用如下的操作符:
ObserverAdd // 当 新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、Remove)
ReactiveCollection 示例代码:

using UnityEngine;
using UniRx;

public class ReactiveCollectionExample : MonoBehaviour
{
    ReactiveCollection<int> mAges = new ReactiveCollection<int>
    {
        1,2,3,4,5
    };

    void Start()
    {
        mAges.ObserveAdd()
            .Subscribe(addAge =>
            {
                Debug.LogFormat("add:{0}", addAge);
            });
        mAges.ObserveRemove()
            .Subscribe(removedAge => 
            { 
                Debug.LogFormat("remove:{0}", removedAge); 
            });
        mAges.ObserveCountChanged()
            .Subscribe(count =>
            {
                Debug.LogFormat("count:{0}", count);
            });
        foreach(var age in mAges)
        {
            Debug.Log(age);
        }

        mAges.Add(6);
        mAges.Remove(2);
    }
}

输出结果为
1
2
3
4
5
add:Index:5 Value:6
count:6
remove:Index:1 Value:2
count:5

ReactiveDictionary 功能与 Dictionary 一样。
同样地,它支持了几个操作符:
ObserverAdd // 当新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、Remove)
示例代码如下:

using UnityEngine;
using UniRx;

public class ReactiveDictionaryExample : MonoBehaviour
{
    private ReactiveDictionary<string, string> mLanguageCode = new ReactiveDictionary<string, string>()
    {
        { "en","英语"},
        { "cn","中文"}
    };

    void Start()
    {
        mLanguageCode.ObserveAdd()
            .Subscribe(addedLanguage =>
            {
                Debug.LogFormat("add:{0}", addedLanguage.Value);
            });
        mLanguageCode.ObserveRemove()
             .Subscribe(removedLanguage =>
             {
                 Debug.LogFormat("remove:{0}", removedLanguage.Value);
             }); 
        mLanguageCode.ObserveCountChanged()
              .Subscribe(count =>
              {
                  Debug.LogFormat("count:{0}", count);
              });
        mLanguageCode.Add("jp", "日语");
        mLanguageCode.Remove("en");
    }
}

输出结果为
add:日语
count:3
remove:英语
count:2

十三、加载场景 AsyncOperation

我们在异步加载资源或者异步加载场景的时候往往会用到 AsyncOperation。
UniRx 对 AsyncOperation 做了支持。使得加载操作可以很容易地监听加载进度。
示例代码如下:

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

public class AsyncOperationExample : MonoBehaviour
{
    void Start()
    {
        var progressObservable = new ScheduledNotifier<float>();
        SceneManager.LoadSceneAsync(0).AsAsyncOperationObservable(progressObservable)
            .Subscribe(ssyncOperation =>
            {
                Debug.Log("load done");
            });
        Resources.LoadAsync<GameObject>("TestCanvas").AsAsyncOperationObservable()
            .Subscribe(resourceRequest =>
            {
                Instantiate(resourceRequest.asset);
            });
        progressObservable.Subscribe(progress =>
        {
            Debug.LogFormat("加载了:{0}", progress);
        });
    }
}

输出结果为:
加载了:0.9
加载了:0.9
加载了:1
load done

[TodoList实战]

1.TodoList功能定义

TodoList App是一个待办事项应用。

功能如下:

(1)待办清单可以添加、更改、删除待办事项;

(2)待办事项可以完成,其内容可以编辑;

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林枫依依

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值