前提:本人用的Unity2019.3.0f3,从AssetStore上直接下的UniRx 7.1.0;(摘自凉鞋)
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 就是 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 秒时间,就搞定了一个实现起来比较麻烦的逻辑。
监听鼠标左右键按下:
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 的深入,也会渐渐淘汰,因为后边有更好的实现方式。
字⾯意思上理解为添加到。
添加到哪里呢?
其实就是 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 的使用更加安全。
Observable.XXX().Subscribe() 是非常典型的 UniRx 格式。
只要理解什么意思就可以看懂大部分的 UniRx 的用法了。
首先解决词汇问题:
Observable: 可观察的,形容词,形容后边的词(Timer) 是可观察的,我们可以粗暴地把 Observable 后边的理解成发布者。
Timer: 定时器,名词,被 Observable 描述,所以是发布者,是事件的发送⽅。
Subscribe: 订阅,动词,订阅谁呢?当然是前边的 Timer,这里可以理解成订阅者,也就是事件的接收⽅。
AddTo: 暂不用理解。
连起来则是:可被观察(监听)的.Timer().订阅()
顺下来应该是:订阅可被观察的定时器。
其概念关系很容易理解。
• Timer 是可观察的。
• 可观察的才能被订阅。
Observable.XXX().Subscribe();
可被观察(监听)的 XX,注册。
以上笔者从发布者和订阅者这个角度来进行的介绍,以便大家理解。
但是 UniRx 的侧重点,不是发布者和订阅者这两个概念如何使用,而是事件从发布者到订阅者之间的过程如何处理。
所以两个点不重要,重要的是两点之间的线,也就是事件的传递过程。
这里先不说得太深⼊,在入门之后,会用很大的篇幅去进行讲解。
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 吧。
很简单,就是获取第一个通过的事件。
using UnityEngine;
using UniRx;
public class FirstExample : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.First(_ => Input.GetMouseButtonDown(0))
.Subscribe(_ =>
{
Debug.Log("鼠标点击");
})
.AddTo(this);
}
}
UniRx 对 UGUI 进行了支持。
比如最常用的按钮点击事件注册。
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class UIExample : MonoBehaviour
{
void Start()
{
var button = GameObject.Find("Button").GetComponent
还支持 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();
image.OnBeginDragAsObservable().Subscribe(_ => Debug.Log("开始拖拽"));
image.OnDragAsObservable().Subscribe(_ => Debug.Log("正在拖拽"));
image.OnEndDragAsObservable().Subscribe(_ => Debug.Log("结束拖拽"));
}
}
UniRx 中有个一个非常强大的概念,叫做 ReactiveProperty。响应式属性。
强大在哪呢?它可以替代一切变量,给变量创造了很多功能。
假如我们想监听一个值是否发生了改变。
用通常的方法实现可能如下:
using UnityEngine;
using System;
public class ReactivePropertyExample : MonoBehaviour
{
public Action 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 Age = new ReactiveProperty(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 模式框架。
在上一讲有简单介绍过,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 CurrentHp{get;private set;}
public ReactiveProperty IsDead{get;private set;}
public Enemy(int initialHp)
{
CurrentHp=new ReactiveProperty(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
{
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 CurrentHp { get;private set;}
public ReactiveProperty IsDead { get;private set;}
public Enemy(int initialHp)
{
// Declarative Property
CurrentHp = new ReactiveProperty(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 意思是合并。
合并什么呢?
我们之前学习过,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
使用 Merge 将三个按钮的事件流合并。并通过 First 只处理第一次点击事件。
这样标题的所说的功能就完成了。
但是还有一点问题,就是当处理按钮事件的时候要知道是哪个按钮被点击了。
很简单,使用 Select 操作符就好了。
Select 是什么意思呢?
就是选择。
Select 本身是 Linq 的操作符。
一般是传⼊一个索引 i/index 然后根据索引返回具体的值。
对于一个 List,什么叫做 Selecte 呢?
看代码就懂了,如下:
var testNumbers = new List(){ 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();
var btnB = GameObject.Find("ButtonB").GetComponent();
var btnC = GameObject.Find("ButtonC").GetComponent();
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 命令系统。
• …
学习完以上的内容可以让我们的日常开发事半功倍。
作为初学者,在日常开发中接触得最多的就是 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();
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();
mRegisterBtn = transform.Find("Register").GetComponent();
mUsername = transform.Find("Username").GetComponent();
mPassword = transform.Find("Password").GetComponent();
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();
mBackBtn = transform.Find("ReturnLogin").GetComponent();
mUsername = transform.Find("Username").GetComponent();
mPassword1 = transform.Find("Password1").GetComponent();
mPassword2 = transform.Find("Password2").GetComponent();
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();
registerPanel = transform.Find("RegisterPanel").GetComponent();
loginPanel.gameObject.SetActive(true);
registerPanel.gameObject.SetActive(false);
}
}
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。
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;
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 意思是,当所有的。
当所有的什么呢?
就是当所有的事件流都结束,就会触发 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 可以配合非常多的操作符使用。理解也非常简单。
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 秒后输出结果为:
接下来
完成
多线程,是作为高级开发者必须具备的一种技术。了解了多线程可以让我们充分利用多核移动端的计算优势,也可以让我们的游戏体验更平滑。
在 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, 还有很多其他的操作符,我们在之后慢慢学习。
以往我们不管使用 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();
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
Subscribe 之后传回来的值则是,当前的进度。
而且 ObservableWWW 的 Get 和 Post 请求都可以自己传对应的 header 和 WWWForm。
除了常用的 Get 和 Post 请求,也对 AssetBundle 的加载也做了简单的封装。
提供了诸如 ObservableWWW.LoadFromCacheOrDownload 这样的 API。
如果想深⼊了解,可以参考 ObservableWWW.cs
总之对 WWW 提供的 API 非常简练,也足够使用。
我们先来看下 ReactiveCommand 定义
public interface IReactiveCommand : IObservable
{
IReadOnlyReactiveProperty 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();
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 类似于 List。
我们可以使用如下的操作符:
ObserverAdd // 当 新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、Remove)
ReactiveCollection 示例代码:
using UnityEngine;
using UniRx;
public class ReactiveCollectionExample : MonoBehaviour
{
ReactiveCollection mAges = new ReactiveCollection
{
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 mLanguageCode = new ReactiveDictionary()
{
{ "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。
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();
SceneManager.LoadSceneAsync(0).AsAsyncOperationObservable(progressObservable)
.Subscribe(ssyncOperation =>
{
Debug.Log("load done");
});
Resources.LoadAsync("TestCanvas").AsAsyncOperationObservable()
.Subscribe(resourceRequest =>
{
Instantiate(resourceRequest.asset);
});
progressObservable.Subscribe(progress =>
{
Debug.LogFormat("加载了:{0}", progress);
});
}
}
输出结果为:
加载了:0.9
加载了:0.9
加载了:1
load done
1.TodoList功能定义
TodoList App是一个待办事项应用。
功能如下:
(1)待办清单可以添加、更改、删除待办事项;
(2)待办事项可以完成,其内容可以编辑;