UniRx 初体验

UniRx 初体验

查看源图像

链式编程

所谓链式编程——使用. . . 像是链条一样进行编程。

:bookmark: 下面是一个延迟调用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

UI数据绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

通过扩展方法增加支持

1
2
3
4
5
6
7
8
9
10
11
12
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?:thought_balloon:

结果是:YES

Asset设定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[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; }
}
bing数据
1
2
3
4
5
6
7
8
9
10
11
12
13
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);

动画

数据实时绑定

:label:通过这种方式很快捷方便的就实现了数据的绑定,同时没有污染数据接口

Subject 和OnNext

++Subject既可以作为观察者也可以作为被观察对象++

image-20220921103234529

1
2
3
4
5
6
7
8
9
private void Start()
{
var subject = new Subject<string>();

//注册处理方式
subject.Subscribe(m => print($"接收到:{m}"));
//传入参数进行处理
subject.OnNext("你好");
}

Subject实现了ISubject接口,其中ISubject接口声明如下:

1
2
3
4
5
6
7
public interface ISubject<TSource, TResult> : IObserver<TSource>, IObservable<TResult>
{
}

public interface ISubject<T> : ISubject<T, T>, IObserver<T>, IObservable<T>
{
}

在其中实现了IObserver接口,这是一个微软官方的观察者模式的实现接口,可参考参考资料2

IObserver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
}
}

事件的过滤和筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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操作符对输入参数进行了过滤,让其只处理特定的输入信息 :satisfied: 。

:book:PS: 可以定义自定义的过滤运算符来指定过滤操作。

UniRX 流

调用OnError、OnCompleted将会终止流,并销毁流对象。(后续监听被终止)

1
2
3
4
5
6
7
8
9
10
11
12
var subject = new Subject<string>();

//注册处理方式
subject.Subscribe(m => print($"接收到:{m}"));


//传入参数进行处理
subject.OnNext("你好");
subject.OnNext("1");
subject.OnCompleted(); //流被终止,后续不再执行
subject.OnNext("2");
subject.OnNext("3");

空参数

需要使用空参数的情况可以如下操作:

1
2
3
4
var sub = new Subject<Unit>();

sub.Subscribe(_ => print("触发事件"));
sub.OnNext(Unit.Default);
  • :bookmark:对于不再使用的流要及时OnCompleted获得Dispose来释放内存以免出现空引用或内存泄漏。
  • :bookmark:可以使用AddTo()将流的生命周期绑定到指定对象。对象被销毁,流也会被销毁。

流的来源

官方提供了创建流的几种方式:

  • Subject
  • ReactiveProperty
  • ReactiveCollection
  • ReactiveDictionary
  • UniRx提供的方法
  • UniRx.Trigger
  • UniRx的协程
  • UniRx转换后的UGUI事件

ReactiveCollection

ReactiveCollection与ReactiveProperty类似,++它是内置了一个通知状态变化功能的List++,ReactiveCollection可以像List一样使用,更棒的是,ReactiveCollection可以用来对List状态的变化进行Subscribe,所支持的状态变化订阅如下:

  • 添加元素时
  • 删除元素时
  • 集合数量变化时
  • 集合元素变化时
  • 元素移动时
  • 清除集合时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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系列
1
2
3
4
5
6
7
8
9
10
11
private void Start()
{
this.OnTriggerEnterAsObservable().Subscribe(x =>
{
print($"{x.gameObject.name} 进入触发区域");
});
this.OnTriggerExitAsObservable().Subscribe(x =>
{
print($"{x.gameObject.name} 离开触发区域");
});
}

动画

在Player中的应用

1
2
3
4
5
6
7
8
9
10
11
private void Start()
{
//间隔2S攻击
this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.A)).ThrottleFirst(TimeSpan.FromSeconds(2f))
.Subscribe(_ => Attack());
}

public void Attack()
{
print("Attack");
}

使用Unirx设计PlayerController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 提供一种微协程的协程实现方式,性能比原生协程更佳。

串联协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 完成");
}

并联协程汇总再处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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();
}

其他使用

延迟和定时

1
2
3
4
5
//延迟执行
Observable.Timer(TimeSpan.FromSeconds(1)).Subscribe((x) => { print("执行"); }).AddTo(this);

//循环执行
Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ => { print("11"); }).AddTo(this);

个人总结

unirx提供了一种响应式的流编程模式,我觉得UniRx最强之处是以下几点:

  • 万事万物皆可以为流
  • 提供一种微协程方式,线程回溯
  • 观察者模式——数据绑定和监听(可用于UI)
  • 装饰者模式——对操作和流进行灵活裁剪控制(Linq)

++:bookmark: UniRx是非常优秀的Unity框架!在之后我会在项目中尝试去使用它。++

参考资料

  1. UniRx入门系列一 · 开发文档集合 (ronpad.com)
  2. IObserver 接口 (System) | Microsoft Learn