本文前提
UniRx 是已经19年就停更的产物,原作者选择在UniRx的基础上,推出适应更多平台和性能更好的R3。
二者不可兼容升级,由于国内没有对应R3的教程,所以本文为机翻搬运。原作者:@toRisouP,链接:https://qiita.com/toRisouP/items/4344fbcba7b7e8d8ce16
前提
「R3」是根据ReactiveExtensions
最新环境的c#重新构建的库。Unity中有一个名为“UniRx”的库,粗略地说,可以认为是“将UniRx
按照最新的环境进行重制”。
详细内容会在别的报道中总结。
另外,本文执笔时的环境如下所示。
- Unity - 2023.1.14f1
- R3 - 1.0.0
- ObservableCollections - 2.0.1
- NuGetForUnity - 4.0.2
这次的概要
介绍“UniRx”和“R3”的功能比较,R3的新功能和被废除的功能,从UniRx换成R3时的替代等。(细节部分不能全部挑出来,介绍遗漏请见谅。另外,我们省略了非面向Unity的功能。)
在这篇文章中出现的样本代码是CC0,除非另有记载。请自由复制粘贴使用。
(但是发生的纠纷和问题不负责)
另外,我们在GitHub上发布了样本项目。
操作环境等的差异
最低Unity版本
UniRx
UniRx没有特别的最低版本,Unity 2017的相当旧的Unity版本也能运行。
R3
R3至少需要Unity 2021.3以上。
补充:destroyCancellationToken
R3是通过CancellationToken
来管理取消的机制。
Unity 2022.2以后可以在MonoBehaviour
上使用destroyCancellationToken
,使用这个很方便。
在Unity 2022.2以下的情况下,使用R3提供的ObservableDestroyTrigger
,可以得到相同的功能。
(Unity2022.2以后使用ObservableDestroyTrigger
也没有问题)
using System;
using Cysharp.Threading.Tasks;
using R3;
using UnityEngine;
public class DestroySample : MonoBehaviour
{
private void Start()
{
// Unity 2022.2以后可以使用destroyCancellationToken
Observable
.Timer(TimeSpan.FromSeconds(3), destroyCancellationToken)
.Subscribe();
// 如果不使用destroyCancellationToken,GetCancellationTokenOnDestroy代替使用就OK
// (Unity2022.2以后的话只是内部返回destroyCancellationToken所以没有成本)
Observable
.Timer(TimeSpan.FromSeconds(3), this.GetCancellationTokenOnDestroy())
.Subscribe();
}
}
导入方法
UniRx
UniRx必须通过以下方法之一来导入。
- unitypackage
- UPM (Git)
- OpenUPM
R3
R3分为“核心模块”和“Unity插件”两部分,在Unity中运行全部功能时需要安装这两者。在R3的官方文档中记载了两者的安装方法。
因为核心模块需要通过Nuget安装,所以推荐使用NugetForUnity
。请通过UnityPackageManager通过Git安装Unity插件(R3.Unity
)。
详细内容请参考官方文档。
如果你在asmdef中进行模块管理,请添加R3.Unity
作为参考
行为上的根本差异
R3从根本上重新考虑了Observable
的概念,因此其行为与UniRx有很大的不同。
- OnError消息被改为
OnErrorResume
消息 - 发布
OnCompleted
消息时可以选择“正常结束”还是“异常结束(包含异常)” - 所有的
Observable
都可以最后发布OnCompleted
消息了 Scheduler
被废除了- 与
async/await
的合作变得容易了 CancellationToken
更易控制
关于这一点,在别的报道中已经解说完毕,请参考下面的报道。
大的改动点写在README里
R3的README中写了差异,所以先读那个吧
从UniRx转移到R3也能直接使用的功能
UniRx中经常使用的功能在R3中也存在。因此,在R3以后的功能中也可以同样使用。
Trigger (MonoBehaviour的事件转换)
将UniRx中存在的MonoBehaviour事件转换为Observable的功能(Trigger),在R3中也可以使用。
using R3;
using R3.Triggers;
using UnityEngine;
namespace Samples.R3Sample
{
public class TriggerSample : MonoBehaviour
{
private void Start()
{
// 可以获取与该GameObject相关联的OnCollisionEnter作为Observable
this.OnCollisionEnterAsObservable()
.Subscribe(collision =>
{
Debug.Log("OnCollisionEnter: " + collision.gameObject.name);
});
// 可以获取Update()作为Observable
this.UpdateAsObservable()
.Subscribe(_ =>
{
Debug.Log("Update!");
});
// 还有很多其他的
}
}
}
AddTo(MonoBehaviour)
将IDisposable联动到MonoBehaviour寿命的AddTo(this),也可以用在R3上。
using R3;
using R3.Triggers;
using UnityEngine;
namespace Samples.R3Sample
{
public class AddToSample : MonoBehaviour
{
[SerializeField] private GameObject _childObject;
private void Start()
{
// 获取与childObject相关联的OnCollisionEnter作为Observable
_childObject
.OnCollisionEnterAsObservable()
.Subscribe(collision =>
{
Debug.Log("OnCollisionEnter: " + collision.gameObject.name);
})
// 将Observable的寿命与这个MonoBehaviour挂钩
.AddTo(this);
}
}
}
uGUI组件的事件转换
UnityEngine.UI.Button
等所谓的“uGUI”事件转换为Observable
的功能可以继续从UniRx使用。
可以使用哪些活动请参照这里。
using R3;
using UnityEngine;
using UnityEngine.UI;
namespace Samples.R3Sample
{
public class GuiEventSample : MonoBehaviour
{
[SerializeField] private Button _button;
[SerializeField] private InputField _inputField;
[SerializeField] private Slider _slider;
[SerializeField] private Text _text;
private void Start()
{
// 按钮的点击
_button
.OnClickAsObservable()
.Subscribe(_ => Debug.Log("Button Clicked!"))
.AddTo(this);
// InputField的文本变更
_inputField.OnValueChangedAsObservable()
.Subscribe(txt => Debug.Log("InputField Text: " + txt))
.AddTo(this);
// Slider的值改变
_slider.OnValueChangedAsObservable()
.Subscribe(v => Debug.Log("Slider Value: " + v))
.AddTo(this);
// 在Text中反映InputField的文本
_inputField.OnValueChangedAsObservable()
.SubscribeToText(_text)
.AddTo(this);
}
}
}
R3的新功能(UniRx没有的功能)
[新功能] SubscribeAwait/SelectAwait/WhereAwait
在新一代Rx [R3]的解说中也有说明,追加了Subscribe
和Select/Where
可以同时使用async/await
的版本。在R3的情况下,async/await
的完成和消息处理控制的感觉很好。(与UniTask组合更方便!)
using Cysharp.Threading.Tasks;
using R3;
using UnityEngine;
using UnityEngine.UI;
namespace Samples.R3Sample
{
public class SubscribeAwaitSample1 : MonoBehaviour
{
[SerializeField] private Button _goButton;
private void Start()
{
// 按下按钮后前进一秒钟
// 被连击的情况下,按此数前进
_goButton.OnClickAsObservable()
.SubscribeAwait(async (_, ct) =>
{
var time = Time.time;
while (Time.time - time < 1f)
{
transform.position += Vector3.forward * Time.deltaTime;
await UniTask.Yield(ct);
}
},
AwaitOperation.Sequential,
// configureAwait建议从true开始不变
configureAwait: true)
.AddTo(this);
}
}
}
注意,configureAwait指定True。(默认为真)
如果指定为假,执行上下文可能会被无意中切换到线程池。
另外,通过指定AwaitOperation
这个参数,可以在异步处理执行中(await
处理结束之前)调整下一条消息到达时的行为。
AwaitOpenration | await 下一个事件发生时的举动 | 备考 |
---|---|---|
Sequential | 优先现在执行中的处理。把剩余的活动堆在队列里。异步处理结束后,取出下一个按顺序异步执行。 | |
Drop | 优先现在执行中的处理。剩余的活动就当作没有忽视。 | |
Switch | 取消现在执行中的异步处理。优先开始处理新到达的事件。 | 取消处理需要使用CancellationToken 自己实现。 |
Parallel | 立即处理新来的事件。处理结束后,以最快的速度输出。 | maxConcurrent 可以限制同时运行的数量。超过maxConcurrent 的数量的消息被堆积在队列中。 |
SequentialParallel※ | 立即处理新来的事件。不管处理的结束顺序如何,输出顺序被调整为与输入顺序相同。 | maxConcurrent 可以限制同时运行的数量。超过maxConcurrent 的数量的消息被堆积在队列中。 |
ThrottleFirstLast | 当不执行异步处理时,处理新到达的值。在执行异步处理期间,仅保存一个最新值,并在异步处理结束时取出该最新值进行处理。 | ThrottleFirst 和ThrottleLast 结合的行为 |
SequentialParallel只能在WhereAwait/SelectAwait中使用
(补充)UniRx是怎么做的
顺便说一下,在UniRx使用async/await的时候,全部都是用async void来操作的。因此,这并不是“与异步处理相协调的动作”。
[新功能]Debounce / ThrottleFirst / ThrottleLast异步的对应
Debounce
(旧名 Throttle
) / ThrottleFirst
/ ThrottleLast
(旧名 Sample
)是 UniRx也存在的操作人员, R3是异步处理应对了。也就是说可以和async/await
一起使用。
各个异步版的行为如下所示。
Debounce
:消息到达后执行异步处理。异步处理完成后发布那个消息。如果在异步处理中来了下一个消息,则在执行中取消异步处理,重新执行异步处理。ThrottleFirst
:消息到达后让它通过,然后执行异步处理,直到处理结束为止切断消息。ThrottleLast
:当消息到达时,执行异步处理来阻断消息,当该处理结束时,只发布一个最后到达的消息。
Debounce
和ThrottleLast
的区别在于异步处理是重新进行还是完成。Debounce
每次来消息都要重做异步处理。ThrottleLast
是一旦开始跑,就一直跑到跑完为止。
未完待续