【UniRx】第一季:入门精讲

2023-10-21 06:20
文章标签 入门 精讲 第一季 unirx

本文主要是介绍【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 ReactivePropertyEnemy enemy = new Enemy(1000);
void Start()
{// Rx supplies user events from Views and Models in a reactive mannermButton.OnClickAsObservable().Subscribe(_=>enemy.CurrentHp.Value -=
99);mToggle.OnValueChangedAsObservable().SubscribeToInteractable(mButton);// Models notify Presenters via Rx,and Presenters update their viewsenemy.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 PropertyCurrentHp = 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)待办事项可以完成,其内容可以编辑;

这篇关于【UniRx】第一季:入门精讲的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/252507

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

poj 2104 and hdu 2665 划分树模板入门题

题意: 给一个数组n(1e5)个数,给一个范围(fr, to, k),求这个范围中第k大的数。 解析: 划分树入门。 bing神的模板。 坑爹的地方是把-l 看成了-1........ 一直re。 代码: poj 2104: #include <iostream>#include <cstdio>#include <cstdlib>#include <al

MySQL-CRUD入门1

文章目录 认识配置文件client节点mysql节点mysqld节点 数据的添加(Create)添加一行数据添加多行数据两种添加数据的效率对比 数据的查询(Retrieve)全列查询指定列查询查询中带有表达式关于字面量关于as重命名 临时表引入distinct去重order by 排序关于NULL 认识配置文件 在我们的MySQL服务安装好了之后, 会有一个配置文件, 也就

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

C语言指针入门 《C语言非常道》

C语言指针入门 《C语言非常道》 作为一个程序员,我接触 C 语言有十年了。有的朋友让我推荐 C 语言的参考书,我不敢乱推荐,尤其是国内作者写的书,往往七拼八凑,漏洞百出。 但是,李忠老师的《C语言非常道》值得一读。对了,李老师有个官网,网址是: 李忠老师官网 最棒的是,有配套的教学视频,可以试看。 试看点这里 接下来言归正传,讲解指针。以下内容很多都参考了李忠老师的《C语言非

MySQL入门到精通

一、创建数据库 CREATE DATABASE 数据库名称; 如果数据库存在,则会提示报错。 二、选择数据库 USE 数据库名称; 三、创建数据表 CREATE TABLE 数据表名称; 四、MySQL数据类型 MySQL支持多种类型,大致可以分为三类:数值、日期/时间和字符串类型 4.1 数值类型 数值类型 类型大小用途INT4Bytes整数值FLOAT4By

【QT】基础入门学习

文章目录 浅析Qt应用程序的主函数使用qDebug()函数常用快捷键Qt 编码风格信号槽连接模型实现方案 信号和槽的工作机制Qt对象树机制 浅析Qt应用程序的主函数 #include "mywindow.h"#include <QApplication>// 程序的入口int main(int argc, char *argv[]){// argc是命令行参数个数,argv是