在完善 Unity 开发的游戏框架时,看到框架 TinaX
使用了 TweenRx 插件 TweenRx
而这个插件,又使用到了一个名为 UniRx 的插件 UniRx
看到框架 QFramework 也用到了该插件 QFramework
于是了解了响应式编程这个概念,经过一番学习之后,个人理解是:
这是一种基于异步通信数据流的编程模式。
这个流的最大特点是:
每一个数据都可以转化为流;
对流的操作,产生新的流;
数据变化后,中间的“操作”是不变的,会使用同一个处理流程。
它是一种全新的编程理念,可以让我们更加关注于业务逻辑,而非程序逻辑,这大大启发了我。
这一篇主要记录下这个插件的基本使用方法。
学习的过程中,除了参考官方的 README.md 之外,也参考了这些资料:
Unity UniRX中文教程-Chinar
什么是响应式编程
The introduction to Reactive Programming you’ve been missing
官方 README :
UniRx (Reactive Extensions for Unity) is a reimplementation of the .NET Reactive Extensions. The Official Rx implementation is great but doesn’t work on Unity and has issues with iOS IL2CPP compatibility. This library fixes those issues and adds some specific utilities for Unity. Supported platforms are PC/Mac/Android/iOS/WebGL/WindowsStore/etc and the library.
UniRx 全称 Reactive Extensions for Unity,它重新实现了 .NET Reactive Extensions,官方的Rx (Reactive Extensions)虽然很给力,但是无法在 Unity 上正常的工作,而且在 iOS IL2CPP 的兼容性上有些问题。UniRx 修复了这些问题,并为 Unity 额外添加了一些特殊的工具。支持的平台包括 PC/Mac/Android/iOS/WebGL/WindowsStore 等等。
它不但在 Github 上开源,而且在 Unity 插件商店 也是免费的。
总结官方的话,就是这几个点:
1、Unity 中经常要使用的 Coroutine,对于异步操作不好用;
怎么不好用?
1.1、Coroutine 返回对是 IEnumerator 对象,而不是一些值对象(Coroutines can’t return any values, since its return type must be IEnumerator.)
1.2、异常处理不友好(Coroutines can’t handle exceptions, because yield return statements cannot be surrounded with a try-catch construction.)
那么 UniRx 的特点是什么?官方的原话如下:
UniRx is a library for composing asynchronous and event-based programs using observable collections and LINQ-style query operators.
The game loop (every Update, OnCollisionEnter, etc), sensor data (Kinect, Leap Motion, VR Input, etc.) are all types of events. Rx represents events as reactive sequences which are both easily composable and support time-based operations by using LINQ query operators.
Unity is generally single threaded but UniRx facilitates multithreading for joins, cancels, accessing GameObjects, etc.
UniRx helps UI programming with uGUI. All UI events (clicked, valuechanged, etc) can be converted to UniRx event streams.
Unity supports async/await from 2017 with C# upgrades, UniRx family prjects provides more lightweight, more powerful async/await integration with Unity. Please see CySharp/UniTask.
https://github.com/Cysharp/UniTask
以下可忽略,基本和官方一致。
UniRx 怎么用?
官方示例:捕获用户双击鼠标事件
using System;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
[SerializeField] Text m_Text = null;
void Start()
{
var clickStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0));
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Subscribe(xs => Debug.LogFormat("DoubleClick Detected! Count: {0}", xs.Count));
}
}
网络操作(官方不建议使用 ObservableWWW)
通用
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
void Start()
{
ObservableWWW.Get("http://google.co.jp/")
.Subscribe(
x => Debug.Log(x.Substring(0, 100)), // onSuccess
ex => Debug.LogException(ex)); // onError
}
}
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
[SerializeField] Text m_Text = null;
void Start()
{
// composing asynchronous sequence with LINQ query expressions
var query = from google in ObservableWWW.Get("http://google.com/")
from bing in ObservableWWW.Get("http://bing.com/")
from unknown in ObservableWWW.Get(google + bing)
select new { google, bing, unknown };
var cancel = query.Subscribe(x => Debug.Log(x));
// Call Dispose is cancel.
cancel.Dispose();
}
}
// Observable.WhenAll is for parallel asynchronous operation
// (It's like Observable.Zip but specialized for single async operations like Task.WhenAll)
var parallel = Observable.WhenAll(
ObservableWWW.Get("http://google.com/"),
ObservableWWW.Get("http://bing.com/"),
ObservableWWW.Get("http://unity3d.com/"));
parallel.Subscribe(xs =>
{
Debug.Log(xs[0].Substring(0, 100)); // google
Debug.Log(xs[1].Substring(0, 100)); // bing
Debug.Log(xs[2].Substring(0, 100)); // unity
});
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
[SerializeField] Text m_Text = null;
void Start()
{
var _progressNotifier = new ScheduledNotifier<float>();
ObservableWWW.GetAndGetBytes("https://github.com/neuecc/UniRx/releases/download/7.1.0/UniRx.unitypackage", progress: _progressNotifier).Subscribe();
_progressNotifier.Subscribe(progressFloat => {
Debug.Log("progress >> " + progressFloat);
});
_progressNotifier.SubscribeToText(m_Text);
}
}
// public method
public static IObservable<string> GetWWW(string url)
{
// convert coroutine to IObservable
return Observable.FromCoroutine<string>((observer, cancellationToken) => GetWWWCore(url, observer, cancellationToken));
}
// IObserver is a callback publisher
// Note: IObserver's basic scheme is "OnNext* (OnError | Oncompleted)?"
static IEnumerator GetWWWCore(string url, IObserver<string> observer, CancellationToken cancellationToken)
{
var www = new UnityEngine.WWW(url);
while (!www.isDone && !cancellationToken.IsCancellationRequested)
{
yield return null;
}
if (cancellationToken.IsCancellationRequested) yield break;
if (www.error != null)
{
observer.OnError(new Exception(www.error));
}
else
{
observer.OnNext(www.text);
observer.OnCompleted(); // IObserver needs OnCompleted after OnNext!
}
}
使用
void Start()
{
var req = GetWWW("https://www.baidu.com")
.Subscribe(
x => Debug.Log(x.Substring(0, 100)), // onSuccess
ex => Debug.LogException(ex)); // onError
}
public static IObservable<float> ToObservable(this UnityEngine.AsyncOperation asyncOperation)
{
if (asyncOperation == null) throw new ArgumentNullException("asyncOperation");
return Observable.FromCoroutine<float>((observer, cancellationToken) => RunAsyncOperation(asyncOperation, observer, cancellationToken));
}
static IEnumerator RunAsyncOperation(UnityEngine.AsyncOperation asyncOperation, IObserver<float> observer, CancellationToken cancellationToken)
{
while (!asyncOperation.isDone && !cancellationToken.IsCancellationRequested)
{
observer.OnNext(asyncOperation.progress);
yield return null;
}
if (!cancellationToken.IsCancellationRequested)
{
observer.OnNext(asyncOperation.progress); // push 100%
observer.OnCompleted();
}
}
// usecase
Application.LoadLevelAsync("testscene")
.ToObservable()
.Do(x => Debug.Log(x)) // output progress
.Last() // last sequence is load completed
.Subscribe();
如何每一帧调用某个函数? Observable.EveryUpdate()
using UnityEngine;
using UniRx;
public class MyUniRx_EveryUpdate : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Observable.EveryUpdate().Subscribe(_ =>
{
Debug.Log("EveryUpdate");
});
}
}
EveryUpdate().First() 第一帧执行
using UnityEngine;
using UniRx;
public class MyUniRx_EveryUpdate : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.First()
.Subscribe(_ =>
{
Debug.Log("EveryUpdate");
});
}
}
First(过滤条件),按下鼠标左键时执行
using UnityEngine;
using UniRx;
public class MyUniRx_EveryUpdate : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.First(_ => Input.GetMouseButtonUp(0))
.Subscribe(_ =>
{
Debug.Log("左键按下时执行,并且只执行一次");
});
}
}
AddTo 给事件流添加生命周期
using UnityEngine;
using UniRx;
public class MyUniRx_EveryUpdate : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.First(_ => Input.GetMouseButtonUp(0)).Subscribe(_ => {
Debug.Log("Call On Left MouseButtonUp");
})
.AddTo(this); // 生命周期和 this 保持一致
}
}
等价的写法
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class MyUniRx_EveryUpdate : MonoBehaviour
{
void Start()
{
this.UpdateAsObservable()
.First(_ => Input.GetMouseButtonUp(0)).Subscribe(_ => {
Debug.Log("Call On Left MouseButtonUp");
});
}
}
Interval
Observable.Interval(TimeSpan.FromSeconds(1))
.Subscribe(_ =>
{
Debug.Log("Interval");
})
.AddTo(this);
数据绑定绑定到 UI
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;
public class MyUniRx_EveryUpdate : MonoBehaviour
{
[SerializeField] Text m_Text = null;
private ReactiveProperty<int> m_IntRxProperty = new ReactiveProperty<int>(9);
void Start()
{
m_IntRxProperty.SubscribeToText(m_Text);
this.UpdateAsObservable()
.Subscribe(_ => {
if(Input.GetMouseButtonDown(0))
{
// 左键按下时,修改数据值后,查看当前文本
m_IntRxProperty.Value++;
}
if(Input.GetMouseButtonDown(1))
{
// 右键按下时,修改文本后,查看当前值
m_Text.text = "99999";
Debug.Log(m_IntRxProperty.Value);
// 值和文本不一样,说明只是数据绑定到 UI
}
});
}
}
按钮点击事件绑定
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class MyUniRx_ButtonClickBinding : MonoBehaviour
{
[SerializeField] Button m_Button = null;
private ReactiveProperty<int> m_IntRxProperty = new ReactiveProperty<int>(9);
void Start()
{
m_Button.OnClickAsObservable().Subscribe(_ =>
{
Debug.Log("Click");
});
}
}
给按钮添加两个点击事件,能够同时响应
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;
public class MyUniRx_ButtonClickBinding : MonoBehaviour
{
[SerializeField] Button m_Button = null;
private ReactiveProperty<int> m_IntRxProperty = new ReactiveProperty<int>(9);
void Start()
{
m_Button.OnClickAsObservable()
.Subscribe(_ =>
{
Debug.Log("Click");
})
.AddTo(this);
m_Button.OnClickAsObservable()
.Subscribe(_ =>
{
Debug.Log("Click 1");
})
.AddTo(this);
}
}
图片拖拽事件
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;
public class MyUniRxTest : MonoBehaviour
{
[SerializeField] Image m_Image = null;
void Start()
{
m_Image.OnBeginDragAsObservable()
.Subscribe(_ =>
{
Debug.Log("BeginDrag");
})
.AddTo(this);
m_Image.OnDragAsObservable()
.Subscribe(_ =>
{
m_Image.transform.position = Input.mousePosition;
})
.AddTo(this);
m_Image.OnEndDragAsObservable()
.Subscribe(_ =>
{
Debug.Log("EndDrag");
})
.AddTo(this);
}
}
Coroutine 转 Observable
using System.Collections;
using UnityEngine;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
void Start()
{
Observable.FromCoroutine(TestIEnumerator)
.Subscribe()
.AddTo(this);
}
private IEnumerator TestIEnumerator()
{
Debug.Log("开始");
yield return new WaitForSeconds(1.0f);
Debug.Log("结束");
}
}
Observale 转 IEnumerator
using System;
using System.Collections;
using UnityEngine;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
void Start()
{
Observable.FromCoroutine(TestIEnumerator)
.Subscribe()
.AddTo(this);
}
private IEnumerator TestIEnumerator()
{
Debug.Log("开始");
yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
Debug.Log("结束");
}
}
既然 Observable 可以转 IEnuerator,因此也就有了这样的使用方式:
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class MyUniRxTest : MonoBehaviour
{
void Start()
{
Observable.FromCoroutine(TestIEnumerator)
.Subscribe()
.AddTo(this);
}
private IEnumerator TestIEnumerator()
{
Debug.Log("开始");
yield return this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(0)).First().ToYieldInstruction();
Debug.Log("结束");
}
}
其根本原因,还是在于 Observable 的底层实现,是 Coroutine 的这套机制;
多个任务并行
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class MyUniRxTest : MonoBehaviour
{
void Start()
{
var _task1 = Observable.FromCoroutine(Task1);
var _task2 = Observable.FromCoroutine(Task2);
Observable.WhenAll(_task1, _task2)
.Subscribe(_=> {
Debug.Log("所有任务完成!");
})
.AddTo(this);
}
private IEnumerator Task1()
{
Debug.Log("开始 1");
yield return this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(0)).First().ToYieldInstruction();
Debug.Log("结束 1");
}
private IEnumerator Task2()
{
Debug.Log("开始 2");
yield return this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(1)).First().ToYieldInstruction();
Debug.Log("结束 2");
}
}
线程
using System.Threading;
using UnityEngine;
using UniRx;
public class MyUniRxTest : MonoBehaviour
{
void Start()
{
var _task1 = Observable.Start(() =>
{
Thread.Sleep(2);
Debug.Log("任务 1 Sleep 2 结束");
return 1;
});
var _task2 = Observable.Start(() =>
{
Thread.Sleep(1);
Debug.Log("任务 2 Sleep 1 结束");
return 2;
});
Observable.WhenAll(_task1, _task2).ObserveOnMainThread().Subscribe(ret =>
{
Debug.Log(ret[0]);
Debug.Log(ret[1]);
});
}
}
从结果看,完成的顺序是 任务2 先于任务1,结果 ret 保持了 WhenAll 中的任务顺序。
MicroCoroutine
限制:
只能由 StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine 这几个方法调用;
只支持 yield return null
UniRx makes it possible to implement the MVP(MVRP) Pattern.
UniRx 有助于实现 MVP 模式
ReactiveCommand
public class Player
{
public ReactiveProperty<int> HP;
public ReactiveCommand Resurrect;
public Player()
{
HP = new ReactiveProperty<int>(1000);
Resurrect = HP.Select(x => x < 0).ToReactiveCommand();
Resurrect.Subscribe(_ =>
{
Debug.Log("Add 2000");
HP.Value += 2000;
});
}
}
public class MyUniRxTest : MonoBehaviour
{
[SerializeField] Text m_Text = null;
[SerializeField] Button m_Button = null;
Player m_Player;
void Start()
{
m_Player = new Player();
m_Player.Resurrect.BindTo(m_Button);
m_Player.HP.SubscribeToText(m_Text);
Observable.Interval(TimeSpan.FromSeconds(1))
.Subscribe(_ =>
{
m_Player.HP.Value -= 500;
})
.AddTo(this);
}
}
一个 ReactiveCommand 可以绑定到一个 UGUI 按钮点击事件上
而 ReactiveCommand 本身可以通过 IObservable 对象创建,创建的过程可以附加执行的前置条件。
基于类型的订阅发布机制:MessageBroker, AsyncMessageBroker
public class TestArgs
{
public int Value { get; set; }
}
---
// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));
// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });
那么如果存在继承关系呢?经过测试,订阅的消息,不受继承关系的影响,
AsyncMessageBroker
AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
// show after 3 seconds.
return Observable.Timer(TimeSpan.FromSeconds(3))
.ForEachAsync(_ =>
{
UnityEngine.Debug.Log(x);
});
});
AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
.Subscribe(_ =>
{
UnityEngine.Debug.Log("called all subscriber completed");
});
上述机制,适合建立对象的异步回收策略;