# UniRx 初体验
# 链式编程
所谓链式编程 —— 使用. . . 像是链条一样进行编程。
🔖 下面是一个延迟调用的示例:
public class UniRxTest : MonoBehaviour | |
{ | |
private void Start() | |
{ | |
Debug.Log("开始时间:"+Time.realtimeSinceStartup); | |
Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe((x) => | |
{ | |
Debug.Log("现在时间:" + Time.realtimeSinceStartup); | |
}).AddTo(this); | |
Observable.EveryUpdate().Where(_ => Input.GetKeyDown(KeyCode.A)).Subscribe(_ => | |
{ | |
Debug.Log("点击了"); | |
}).AddTo(this); | |
} | |
} |
# Ui 数据绑定
原版本不支持绑定 TextMeshPro
using System; | |
using UniRx; | |
using UnityEngine; | |
using UnityEngine.UI; | |
/// <summary> | |
/// 数据绑定测试 | |
/// </summary> | |
public class RxBingData : MonoBehaviour | |
{ | |
public Text titleTex; | |
public Button clBtn; | |
public IModel model; | |
private void Awake() | |
{ | |
model = new GameData(); | |
model.Name.SubscribeToText(titleTex); | |
clBtn.OnClickAsObservable().Subscribe(_ => | |
{ | |
model.Name.Value = "以分行诉分行诉客户方"; | |
}); | |
Observable.EveryUpdate().Where(_ => Input.GetKeyDown(KeyCode.A)).Subscribe(_ => | |
{ | |
model.Name.Value = "1111"; | |
}).AddTo(this); | |
} | |
} | |
public interface IModel | |
{ | |
StringReactiveProperty Name { get; } | |
} | |
public class GameData : IModel | |
{ | |
public StringReactiveProperty Name { get; } = new StringReactiveProperty("校长"); | |
} |
# 绑定 TextMeshPro
通过扩展方法增加支持
public static class UnityUiExtensions | |
{ | |
public static IDisposable SubscribeToText(this IObservable<string> source, TextMeshProUGUI text) | |
{ | |
return source.SubscribeWithState(text, (x, t) => t.text = x); | |
} | |
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextMeshProUGUI text) | |
{ | |
return source.SubscribeWithState(text, (x, t) => t.text = x.ToString()); | |
} | |
} |
# 绑定 ScripTableObject
在此我开始思考是否可以绑定 ScripTableObject?💭
结果是:YES
[CreateAssetMenu] | |
public class GameSetting : ScriptableObject,IAudioSetting | |
{ | |
public StringReactiveProperty ProfileName => profileName; | |
public FloatReactiveProperty BgmVolume => bgmVolume; | |
public StringReactiveProperty profileName; | |
public FloatReactiveProperty bgmVolume; | |
} | |
public interface IAudioSetting | |
{ | |
StringReactiveProperty ProfileName { get; } | |
FloatReactiveProperty BgmVolume { get; } | |
} |
gameSetting.profileName.SubscribeToText(title); | |
gameSetting.bgmVolume.SubscribeToText(titleTex); | |
clBtn.OnClickAsObservable().Subscribe(_ => | |
{ | |
gameSetting.profileName.Value = "clicked Data"; | |
}); | |
Observable.EveryUpdate().Where(_ => Input.GetKeyDown(KeyCode.A)).Subscribe(_ => | |
{ | |
gameSetting.profileName.Value = "keyDown A"; | |
}).AddTo(this); |
🏷通过这种方式很快捷方便的就实现了数据的绑定,同时没有污染数据接口
# Subject 和 OnNext
Subject 既可以作为观察者也可以作为被观察对象
private void Start() | |
{ | |
var subject = new Subject<string>(); | |
// 注册处理方式 | |
subject.Subscribe(m => print($"接收到:{m}")); | |
// 传入参数进行处理 | |
subject.OnNext("你好"); | |
} |
Subject 实现了 ISubject 接口,其中 ISubject 接口声明如下:
public interface ISubject<TSource, TResult> : IObserver<TSource>, IObservable<TResult> | |
{ | |
} | |
public interface ISubject<T> : ISubject<T, T>, IObserver<T>, IObservable<T> | |
{ | |
} |
在其中实现了 IObserver
接口,这是一个微软官方的观察者模式的实现接口,可参考 参考资料2
namespace System | |
{ | |
/// <summary > 提供用于接收基于推送的通知的机制。</summary> | |
/// <typeparam name="T"> 提供通知信息的对象。</typeparam> | |
public interface IObserver<in T> | |
{ | |
/// <summary > 向观察者提供新数据。</summary> | |
/// <param name="value"> 当前通知信息。</param> | |
void OnNext(T value); | |
/// <summary > 通知观察者提供程序遇到错误情况。</summary> | |
/// <param name="error"> 提供有关错误的附加信息的对象。</param> | |
void OnError(Exception error); | |
/// <summary > 通知观察者提供程序已完成发送基于推送的通知。</summary> | |
void OnCompleted(); | |
} | |
} |
# 事件的过滤和筛选
private void Start() | |
{ | |
var subject = new Subject<string>(); | |
// 注册处理方式 | |
subject.Subscribe(m => print($"接收到:{m}")); | |
subject.Subscribe(m => print("处理方式二|" + m)); | |
subject.Where(x=>x=="你好").Subscribe(m => print($"接收到特殊:{m}")); | |
// 传入参数进行处理 | |
subject.OnNext("你好"); | |
subject.OnNext("1"); | |
subject.OnNext("2"); | |
subject.OnNext("3"); | |
} |
subject.Where(x=>x=="你好").Subscribe(m => print($"接收到特殊:{m}"));
使用 Where 操作符对输入参数进行了过滤,让其只处理特定的输入信息 😆 。
📖PS: 可以定义自定义的过滤运算符来指定过滤操作。
# UniRX 流
调用 OnError、OnCompleted
将会终止流,并销毁流对象。(后续监听被终止)
var subject = new Subject<string>(); | |
// 注册处理方式 | |
subject.Subscribe(m => print($"接收到:{m}")); | |
// 传入参数进行处理 | |
subject.OnNext("你好"); | |
subject.OnNext("1"); | |
subject.OnCompleted(); // 流被终止,后续不再执行 | |
subject.OnNext("2"); | |
subject.OnNext("3"); |
# 空参数
需要使用空参数的情况可以如下操作:
var sub = new Subject<Unit>(); | |
sub.Subscribe(_ => print("触发事件")); | |
sub.OnNext(Unit.Default); |
- 🔖对于不再使用的流要及时
OnCompleted
获得Dispose
来释放内存以免出现空引用或内存泄漏。- 🔖可以使用
AddTo()
将流的生命周期绑定到指定对象。对象被销毁,流也会被销毁。
# 流的来源
官方提供了创建流的几种方式:
- Subject
- ReactiveProperty
- ReactiveCollection
- ReactiveDictionary
- UniRx 提供的方法
- UniRx.Trigger
- UniRx 的协程
- UniRx 转换后的 UGUI 事件
# ReactiveCollection
ReactiveCollection 与 ReactiveProperty 类似,它是内置了一个通知状态变化功能的 List,ReactiveCollection 可以像 List 一样使用,更棒的是,ReactiveCollection 可以用来对 List 状态的变化进行 Subscribe,所支持的状态变化订阅如下:
- 添加元素时
- 删除元素时
- 集合数量变化时
- 集合元素变化时
- 元素移动时
- 清除集合时
var list = new ReactiveCollection<string>(){"a","b","c"}; | |
list.ObserveAdd().Subscribe(x => | |
{ | |
print($"新增元素:{x}"); | |
}); | |
list.ObserveRemove().Subscribe(x => | |
{ | |
print($"删除元素:{x}"); | |
}); | |
list.ObserveMove().Subscribe(x => | |
{ | |
print($"移动元素:{x}"); | |
}); | |
list.ObserveReset().Subscribe(_ => | |
{ | |
print($"重制"); | |
}); | |
//... | |
list.Add("AA"); | |
list.RemoveAt(0); | |
list.Clear(); |
# UniRx 的工厂方法
# UniRx.Trigger 系列
private void Start() | |
{ | |
this.OnTriggerEnterAsObservable().Subscribe(x => | |
{ | |
print($"{x.gameObject.name} 进入触发区域"); | |
}); | |
this.OnTriggerExitAsObservable().Subscribe(x => | |
{ | |
print($"{x.gameObject.name} 离开触发区域"); | |
}); | |
} |
# 在 Player 中的应用
private void Start() | |
{ | |
// 间隔 2S 攻击 | |
this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.A)).ThrottleFirst(TimeSpan.FromSeconds(2f)) | |
.Subscribe(_ => Attack()); | |
} | |
public void Attack() | |
{ | |
print("Attack"); | |
} |
# 使用 Unirx 设计 PlayerController
using UnityEngine; | |
using UniRx.Triggers; | |
using UniRx; | |
public class TestUniRX : MonoBehaviour | |
{ | |
private CharacterController characterController; | |
private BoolReactiveProperty isJumping = new BoolReactiveProperty(); | |
void Start() | |
{ | |
characterController = GetComponent<CharacterController>(); | |
this.UpdateAsObservable() | |
.Where(_ => !isJumping.Value) | |
.Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))) | |
.Where(x => x.magnitude > 0.1f) | |
.Subscribe(x => Move(x.normalized)); | |
this.UpdateAsObservable() | |
.Where(_ => Input.GetKeyDown(KeyCode.Space) | |
&& !isJumping.Value && characterController.isGrounded) | |
.Subscribe(_ => | |
{ | |
Jump(); | |
isJumping.Value = true; | |
}); | |
characterController | |
.ObserveEveryValueChanged(x => x.isGrounded) | |
.Where(x => x && isJumping.Value) | |
.Subscribe(_ => isJumping.Value = false) | |
.AddTo(gameObject); | |
isJumping.Where(x => !x) | |
.Subscribe(_ => PlaySoundEffect()); | |
} | |
private void PlaySoundEffect() | |
{ | |
Debug.Log("播放音效"); | |
} | |
private void Jump() | |
{ | |
Debug.Log("Jump"); | |
} | |
private void Move(Vector3 normalized) | |
{ | |
Debug.Log("Move"); | |
} | |
} |
# 针对协程流
UniRx 提供一种微协程的协程实现方式,性能比原生协程更佳。
# 串联协程
void Start() | |
{ | |
Observable.FromCoroutine(CoroutineA) | |
.SelectMany(CoroutineB) | |
.Subscribe(_=>Debug.Log("CoroutineA 和CoroutineB 执行完成")); | |
} | |
IEnumerator CoroutineA() | |
{ | |
Debug.Log("CoroutineA 开始"); | |
yield return new WaitForSeconds(3); | |
Debug.Log("CoroutineA 完成"); | |
} | |
IEnumerator CoroutineB() | |
{ | |
Debug.Log("CoroutineB 开始"); | |
yield return new WaitForSeconds(10); | |
Debug.Log("CoroutineB 完成"); | |
} |
# 并联协程汇总再处理
void Start() | |
{ | |
Observable.WhenAll( | |
Observable.FromCoroutine<string>(o => CoroutineA(o)), | |
Observable.FromCoroutine<string>(o => CoroutineB(o)) | |
).Subscribe(xs => | |
{ | |
foreach (var item in xs) | |
{ | |
Debug.Log("result:" + item); | |
} | |
}); | |
} | |
IEnumerator CoroutineA(IObserver<string> observer) | |
{ | |
Debug.Log("CoroutineA 开始"); | |
yield return new WaitForSeconds(3); | |
observer.OnNext("协程A 执行完成"); | |
Debug.Log("A 3秒等待结束"); | |
observer.OnCompleted(); | |
} | |
IEnumerator CoroutineB(IObserver<string> observer) | |
{ | |
Debug.Log("CoroutineB 开始"); | |
yield return new WaitForSeconds(1); | |
observer.OnNext("协程B 执行完成"); | |
Debug.Log("B 1秒等待结束"); | |
observer.OnCompleted(); | |
} |
# 其他使用
# 延迟和定时
// 延迟执行 | |
Observable.Timer(TimeSpan.FromSeconds(1)).Subscribe((x) => { print("执行"); }).AddTo(this); | |
// 循环执行 | |
Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ => { print("11"); }).AddTo(this); |
# 个人总结
unirx
提供了一种响应式的流编程模式,我觉得 UniRx 最强之处是以下几点:
- 万事万物皆可以为流
- 提供一种微协程方式,线程回溯
- 观察者模式 —— 数据绑定和监听(可用于 UI)
- 装饰者模式 —— 对操作和流进行灵活裁剪控制(Linq)
🔖 UniRx 是非常优秀的 Unity 框架!在之后我会在项目中尝试去使用它。
# 参考资料
- UniRx 入门系列一・开发文档集合 (ronpad.com)
- IObserver 接口 (System) | Microsoft Learn