UniRx 官方文檔翻譯
UniRx 基於Unity的響應式編程框架
什麼是UniRx?
UniRx(Unity的響應式編程框架)是.Net響應式編程框架的重新實現版本。官方的Rx的實現方式是非常棒的。但是,在Unity中使用會有一些問題;在IOS的IL2CPP中有兼容性的問題。UniRx修復這些問題,並針對Unity添加了一些特別的工具。支持的平臺包括PC/Mac/Android/iOS/WebGL/WindowsStore/等等。
UniRx可在Asset Store(免費)中下載: http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT
博客信息更新:https://medium.com/@neuecc
Unity Forums 在線支持,有問題隨時向我提問:http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity
發現說明: UniRx/releases
UniRx 作爲核心庫+平臺適配器(MainThreadScheduler/FromCoroutinue/etf)+框架(ObservableTriggers/ReactiveProperty/etc)
注意:async/await 集成(UniRx.Async)被分離到Cysharp/UniTask 7.0之後的版本
爲什麼使用Rx?
通常,在Unity對網絡操作要求使用WWW和Coroutine.但是出於以下幾點原因(或者其它原因)使用協程來進行異步操作並不順手:
1.雖然協程的返回類型必須是IEnumerator,但是協程不能返回任何值。
2.因爲yield return 語句不能被try-catch結構體包裹,協程中不能處理異常。
這種缺乏可組合性導致程序的緊耦合,往往造成IEnumators中邏輯過於複雜。
Rx可以解決異步調用的“傷痛”,Rx 是一個使用可觀察集合和LINQ風格查詢運算符組合成的基於異步和基於事件的可編程庫。
遊戲循環(every Update,OnCollisionEnter),傳感器數據(Kinect,Leap Motion,VR Input 等等)這些類型的事件。Rx將事件表示爲響應式序列。通過使用LINQ查詢運算符,Rx變得容易組合且支持基於時間的操作。
Unity通常是單線程的,但是UniRx促進了多線程joins、cancel 訪問GameObject,等等。
UniRx爲UGUI提供了UI編程。所有的UI事件(clicked,valuechanged,等)均可以被轉化爲UniRx的事件流。
Unity 在2017之後支持C# 中的astnc/await。UniRx 爲Unity提供了更輕量、強大的async/await集成。請看: Cysharp/UniTask.
介紹
非常棒的介紹Rx的文章:The introduction to Reactive Programming you’ve been missing.
以下代碼使用UniRx實現了文章中的雙擊檢測事例:
var clickStream=Observable.EveryUpdate()
.Where(_=>Input.GetMouseButtonDown(0));
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Subscribe(xs => Debug.Log("DoubleClick Detected! Count:" + xs.Count));
本事例演示了以下功能(僅僅使用5行代碼):
- 遊戲循環(Update)作爲事件流
- 可組合事件流
- 合併自身流
- 易於處理基於時間的操作
網絡操作
使用ObservableWWW 進行一步網絡操作。它的Get/Post函數返回可訂閱的IObservables.
ObservableWWW.Get("http://google.co.jp/")
.Subscribe(
x=Debug.Log(x.Substring(0,100)),
ex=Debug.LogExecption(ex)
);
Rx是可組合也是可以取消的,你可以使用LINQ 查詢表達式:
var query=from google in ObservableWWW.Get("http://google.com/")
from bing in ObservableWWW.Get("http://bing.com/")
from unknow in ObservableWWW(goole+bing)
select new {google,bing,unknow};
var cancel=query.Subscribe(x=>Debug.Log(x));
cancel.Dispose();
使用Observable.WhenAll 執行並行請求(parallel):
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
});
提供進度信息:
// notifier for progress use ScheduledNotifier or new Progress<float>(/* action */)
var progressNotifier=new ScheduledNotifier<float>();
// pass notifier to WWW.Get/Post
progressNotifier.Subscribe(x=>Debug.Log(x));
錯誤處理:
// If WWW has .error, ObservableWWW throws WWWErrorException to onError pipeline.
// WWWErrorException has RawErrorMessage, HasResponse, StatusCode, ResponseHeaders
ObservableWWW.Get("http://www.google.com/404")
.CatchIgnore((WWWErrorException ex) =>
{
Debug.Log(ex.RawErrorMessage);
if (ex.HasResponse)
{
Debug.Log(ex.StatusCode);
}
foreach (var item in ex.ResponseHeaders)
{
Debug.Log(item.Key + ":" + item.Value);
}
})
.Subscribe();
使用IEnumators (Coroutines)
IEnumator(Coroutine)是Unity的基本異步工具,UniRx集成了協程和IObservables,你可以在協程中寫異步代碼,並使用UniRx編排他們。這是控制異步流最好的方式。
// two coroutines
IEnumerator AsyncA()
{
Debug.Log("a start");
yield return new WaitForSeconds(1);
Debug.Log("a end");
}
IEnumerator AsyncB()
{
Debug.Log("b start");
yield return new WaitForEndOfFrame();
Debug.Log("b end");
}
// main code
// Observable.FromCoroutine converts IEnumerator to Observable<Unit>.
// You can also use the shorthand, AsyncA().ToObservable()
// after AsyncA completes, run AsyncB as a continuous routine.
// UniRx expands SelectMany(IEnumerator) as SelectMany(IEnumerator.ToObservable())
var cancel = Observable.FromCoroutine(AsyncA)
.SelectMany(AsyncB)
.Subscribe();
// you can stop a coroutine by calling your subscription's Dispose.
cancel.Dispose();
在Unity5.3中,你可以使用ToYieldInstruction將Observable轉化爲Coroutine:
IEnumerator TestNewCustomYieldInstruction()
{
// wait Rx Observable.
yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
// you can change the scheduler(this is ignore Time.scale)
yield return Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.MainThreadIgnoreTimeScale).ToYieldInstruction();
// get return value from ObservableYieldInstruction
var o = ObservableWWW.Get("http://unity3d.com/").ToYieldInstruction(throwOnError: false);
yield return o;
if (o.HasError) { Debug.Log(o.Error.ToString()); }
if (o.HasResult) { Debug.Log(o.Result); }
// other sample(wait until transform.position.y >= 100)
yield return this.transform.ObserveEveryValueChanged(x => x.position).FirstOrDefault(p => p.y >= 100).ToYieldInstruction();
}
通常情況下,當我們想要協程返回一個值時,我們必須使用回調。Observable.FromCoroutine 可以將協程轉化爲可取消的IObservable[T]。
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!
}
}
這還有更多的示例,下方展示多個OnNext的形式:
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.Start is start factory methods on specified scheduler
// default is on ThreadPool
var heavyMethod = Observable.Start(() =>
{
// heavy method...
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
return 10;
});
var heavyMethod2 = Observable.Start(() =>
{
// heavy method...
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
return 10;
});
// Join and await two other thread values
Observable.WhenAll(heavyMethod, heavyMethod2)
.ObserveOnMainThread() // return to main thread
.Subscribe(xs =>
{
// Unity can't touch GameObject from other thread
// but use ObserveOnMainThread, you can touch GameObject naturally.
(GameObject.Find("myGuiText")).guiText.text = xs[0] + ":" + xs[1];
});
DefaultScheduler(默認調度器)
UniRx默認是基於時間操作的(Interval、Timer、Buffer(timeSpan)等等),使用Scheduler.MainThread作爲它們的調度器。UniRx中的大多數運算符(Observable.Start除外)都是在單個線程上執行的;因此不需要ObserverOn,並且可以忽略線程安全問題。雖然和標準 .NET 中的Rx實現不同,但是這更符合Unity的環境。
Scheduler.Mainthread的執行受Time.timeScale的影響,如果你想要在執行時忽略TimeScale,你可以使用Scheduler.MainThreadIgnoreTimeScale代替。
MonoBehaviour triggers
UniRx使用UniRx.Triggers處理MonoBehaviour事件:
using UniRx;
using UniRx.Triggers; // need UniRx.Triggers namespace
public class MyComponent : MonoBehaviour
{
void Start()
{
// Get the plain object
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
// Add ObservableXxxTrigger for handle MonoBehaviour's event as Observable
cube.AddComponent<ObservableUpdateTrigger>()
.UpdateAsObservable()
.SampleFrame(30)
.Subscribe(x => Debug.Log("cube"), () => Debug.Log("destroy"));
// destroy after 3 second:)
GameObject.Destroy(cube, 3f);
}
}
支持的triggers如列表所示:UniRx.wiki#UniRx.Triggers
通過直接訂閱Component/GameObject上的擴展方法返回的Observables(可觀察對象),可以更輕鬆的處理事件,這些方法被自動注入到ObservableTrigger中(除了ObservableEventTrigger和ObservableStateMachineTrigger):
using UniRx;
using UniRx.Triggers;
public class DragAndDropOnce:MonoBehaviour{
void Start(){
this.OnMouseDownAsObservable()
.SelectMany(_=>this.UpdateAsObservable())
.TakeUntil(this.OnMouseUpAsObservable())
.Select(_=>Input.mousePosition)
.Subscribe(x=>Debug.Log(x));
}
}
之前版本中UniRx提供了ObservableMonoBehaiour.新版本中以不再對其提供支持,請使用UniRx.Triggers代替。
創建自定義Triggers
將Unity事件轉化爲Observable(可觀察對象)是處理Unity事件最好的方式。如果UniRx提供的標準的triggers不夠使用的話,你可以自定義triggers.爲了演示,下方提供了一個基於UGUI的LongTap(長按)觸發演示:
public class ObservableLongPointerDownTrigger : ObservableTriggerBase, IPointerDownHandler, IPointerUpHandler{
public float IntervalSecond=1f;
Subject<Unit> onLongPointerDown;
float> raiseTime;
void Update(){
if (raiseTime!=null&&raiseTime<=Time.realtimeSinceStartup){
if (onLongPointerDown!=null)onLongPointerDown.OnNext(Unit.Default);
raiseTime=null;
}
}
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{
raiseTime = Time.realtimeSinceStartup + IntervalSecond;
}
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
raiseTime = null;
}
public IObservable<Unit> OnLongPointerDownAsObservable()
{
return onLongPointerDown ?? (onLongPointerDown = new Subject<Unit>());
}
protected override void RaiseOnCompletedOnDestroy()
{
if (onLongPointerDown != null)
{
onLongPointerDown.OnCompleted();
}
}
}
它的使用像標準triggers一樣簡單:
var trigger = button.AddComponent<ObservableLongPointerDownTrigger>();
trigger.OnLongPointerDownAsObservable().Subscribe();
Observable 生命週期管理
什麼時候調用OnCompleted? 使用UniRx時,必須考慮訂閱的生命週期管理。當與GameObject對象相連的遊戲對象被銷燬時,ObservableTriggers會調用OnCompleted.其它的靜態生成器方法(Observable.Timer、Observable.EveryUpdate…等等,並不會自動停止,他們的訂閱需要被手動管理。
Rx提供了一些輔助方法,比如,IDisposable.AddTo運行你一次釋放多個訂閱:
// CompositeDisposable is similar with List<IDisposable>, manage multiple IDisposable
CompositeDisposable disposables = new CompositeDisposable(); // field
void Start()
{
Observable.EveryUpdate().Subscribe(x => Debug.Log(x)).AddTo(disposables);
}
void OnTriggerEnter(Collider other)
{
// .Clear() => Dispose is called for all inner disposables, and the list is cleared.
// .Dispose() => Dispose is called for all inner disposables, and Dispose is called immediately after additional Adds.
disposables.Clear();
}
如果你想在GameObject被銷燬時自動釋放,你可以使用AddTo(GameObject/Component):
void Start(){
Observable.IntervalFrame(30).Subscribe(x=Debug.Log(x)).AddTo(this);
}
AddTo可以促進流的自動釋放,如果你需要在管道中隊OnCompleted進行特殊處理,那麼你可以使用TakeWhile、TakeUntil、TakeUntilDestroy和TakeUntilDisable代替:
Observable.IntervalFrame(30).TakeUntilDisable(this)
.Subscribe(x => Debug.Log(x), () => Debug.Log("completed!"));
當你處理事件時,Repeat是一種重要但危險的方法,它可能會造成程序的無線循環,因此,請謹慎使用它:
using UniRx;
using UniRx.Triggers;
public class DangerousDragAndDrop:MonoBehaviour{
void Start(){
this.gameObject.OnMouseDownAsObservable()
.SelectMany(_=>this.gameObject.UpdateAsObservable())
.TakeUtil(this.gameObject.OnMouseUpAsObservable())
.Select(_=>Input.mousePosition)
.Repeat()
.Subscribe(x=>Debug.Log(x));
}
}
UniRx另外提供了一種安全使用Repeat的方法。RepeatSafe:
如果重複調用OnComplete,Repeat將會停止。RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component)允許在目標對象被銷燬時停止。
this.gameObject.OnMouseDownAsObservable()
.SelectMany(_ => this.gameObject.UpdateAsObservable())
.TakeUntil(this.gameObject.OnMouseUpAsObservable())
.Select(_ => Input.mousePosition)
.RepeatUntilDestroy(this) // safety way
.Subscribe(x => Debug.Log(x));
UniRx確保hot Observable(FromEvent/Subject/ReactiveProperty/UnityUI.AsObservable…, 類似事件)可以持續的處理異常。什麼意思?如果在Subscribe中訂閱,這不分離事件。
button.OnClickAsObservable().Subscribe(_ =>
{
// If throws error in inner subscribe, but doesn't detached OnClick event.
ObservableWWW.Get("htttp://error/").Subscribe(x =>
{
Debug.Log(x);
});
});
這種行爲有時很有用,比如用戶事件的處理。
每一個類的實例都提供了一個ObserveEveryValueChanged的方法。這個方法可以每一幀檢測某個值發生的變化:
// watch position change
this.transform.ObserveEveryValueChanged(x => x.position).Subscribe(x => Debug.Log(x));
這是非常有用的,如果觀察的目標是一個GameObject;當GameObject被銷燬時,訂閱將自動停止並調用OnCompleted.如果觀察的對象是一個原生的C#對象,OnCompleted將在GC時被調用。
將Unity回調轉化爲IObservables(可觀察對象)
使用Subject(或者AsyncSubject進行異步操作):
public class LogCallback
{
public string Condition;
public string StackTrace;
public UnityEngine.LogType LogType;
}
public static class LogHelper
{
static Subject<LogCallback> subject;
public static IObservable<LogCallback> LogCallbackAsObservable()
{
if (subject == null)
{
subject = new Subject<LogCallback>();
// Publish to Subject in callback
UnityEngine.Application.RegisterLogCallback((condition, stackTrace, type) =>
{
subject.OnNext(new LogCallback { Condition = condition, StackTrace = stackTrace, LogType = type });
});
}
return subject.AsObservable();
}
}
// method is separatable and composable
LogHelper.LogCallbackAsObservable()
.Where(x => x.LogType == LogType.Warning)
.Subscribe();
LogHelper.LogCallbackAsObservable()
.Where(x => x.LogType == LogType.Error)
.Subscribe();
Unity5中,Application.RegisterLogCallback被移除了,轉而提供Application.logMessageReceived的支持,因此,我們現在可以簡單的使用Observable.FromEvent.
public static IObservable<LogCallback> LogCallbackAsObservable()
{
return Observable.FromEvent<Application.LogCallback, LogCallback>(
h => (condition, stackTrace, type) => h(new LogCallback { Condition = condition, StackTrace = stackTrace, LogType = type }),
h => Application.logMessageReceived += h, h => Application.logMessageReceived -= h);
}
Stream Logger
// using UniRx.Diagnostics;
// logger is threadsafe, define per class with name.
static readonly Logger logger = new Logger("Sample11");
// call once at applicationinit
public static void ApplicationInitialize()
{
// Log as Stream, UniRx.Diagnostics.ObservableLogger.Listener is IObservable<LogEntry>
// You can subscribe and output to any place.
ObservableLogger.Listener.LogToUnityDebug();
// for example, filter only Exception and upload to web.
// (make custom sink(IObserver<EventEntry>) is better to use)
ObservableLogger.Listener
.Where(x => x.LogType == LogType.Exception)
.Subscribe(x =>
{
// ObservableWWW.Post("", null).Subscribe();
});
}
// Debug is write only DebugBuild.
logger.Debug("Debug Message");
// or other logging methods
logger.Log("Message");
logger.Exception(new Exception("test exception"));
Debugging
UniRx.Diagnostics命名空間下的Debug運算符便於用於調試。
// needs Diagnostics using
using UniRx.Diagnostics;
---
// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
var subject = new Subject<int>();
subject.Debug("DebugDump, Normal").Subscribe();
subject.OnNext(1);
subject.OnNext(10);
subject.OnCompleted();
}
// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
var subject = new Subject<int>();
var d = subject.Debug("DebugDump, Cancel").Subscribe();
subject.OnNext(1);
d.Dispose();
}
// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
var subject = new Subject<int>();
subject.Debug("DebugDump, Error").Subscribe();
subject.OnNext(1);
subject.OnError(new Exception());
}
在在OnNext,OnError,OnCompleted,OnCancel,OnSubscribe時序上顯示序列元素以進行Debug.Log,僅當#if DEBUG時才被啓用。
Unity-specific Extra Gems
// Unity's singleton UiThread Queue Scheduler
Scheduler.MainThreadScheduler
ObserveOnMainThread()/SubscribeOnMainThread()
// Global StartCoroutine runner
MainThreadDispatcher.StartCoroutine(enumerator)
// convert Coroutine to IObservable
Observable.FromCoroutine((observer, token) => enumerator(observer, token));
// convert IObservable to Coroutine
yield return Observable.Range(1, 10).ToYieldInstruction(); // after Unity 5.3, before can use StartAsCoroutine()
// Lifetime hooks
Observable.EveryApplicationPause();
Observable.EveryApplicationFocus();
Observable.OnceApplicationQuit();
FrameCount-based timeoperators
UniRx 提供了一些繼續幀數的時間運算符
Method |
---|
EveryUpdate |
EevryFixedUpdate |
EveryEndOfFrame |
EveryGameObjectUpdate |
EveryLateUpdate |
ObserveOnMainThread |
NextFrame |
IntervalFrame |
TimerFrame |
DelayFrame |
SampleFrame |
ThrottleFrame |
ThrottleFirstFrame |
TimeoutFrame |
DelayFrameSubscription |
FrameInterval |
FrameTimeInterval |
BatchFrame |
例如,一次延時調用:
Observable.TimerFrame(100).Subscribe(_ => Debug.Log("after 100 frame"));
Every* 方法的執行順序如下:
EveryGameObjectUpdate(in MainThreadDispatcher's Execution Order) ->
EveryUpdate ->
EveryLateUpdate ->
EveryEndOfFrame
如果在MainThreadDispatcher之前調用了調用者,則從同一幀調用EveryGameObjectUpdate.(我建議對MainThreadDispatcher的調用在同一幀中優先於EveryLateUpdate、EveryEndOfFrame),EveryUpdate在下一幀中調用。
MicroCoroutine(微協程)
微協程的優點在於內存高效和執行快速。它的實現是基於Unity blog’s 10000 UPDATE() CALLS,避免了託管內存-非託管內存的開銷,以致迭代速度提升了10倍。微協程自動用於基於幀數的時間運算符和ObserveEveryValueChanged.
如果你想使用微協程替代Unity自帶的協程(Coroutine),使用MainThreadDispatcher.StartUpdateMicroCoroutine 或者Observable.FromMicroCoroutine.
int counter;
IEnumerator Worker()
{
while(true)
{
counter++;
yield return null;
}
}
void Start()
{
for(var i = 0; i < 10000; i++)
{
// fast, memory efficient
MainThreadDispatcher.StartUpdateMicroCoroutine(Worker());
// slow...
// StartCoroutine(Worker());
}
}
當然微協程存在一些限制,經支持yield return null 迭代,並且其更新時間取決於啓動微協程的方法(StartUpdateMicroCoroutine,StartFixedUpdateMicroCoroutine,StartEndOfFrameMicroCoroutine)。
如果和其它IObservable結合起來,你可以檢測已完成的屬性,比如:isDone.
IEnumerator MicroCoroutineWithToYieldInstruction()
{
var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
while (!www.IsDone)
{
yield return null;
}
if (www.HasResult)
{
UnityEngine.Debug.Log(www.Result);
}
}
UGUI 集成
UniRx可以很容易的處理UnityEvent,使用UnityEvent.AsObservable 訂閱事件:
public Button MyButton;
// ---
MyButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));
將事件視爲可觀察對象可啓用聲明式UI編程。
public Toggle MyToggle;
public InputField MyInput;
public Text MyText;
public Slider MySlider;
// On Start, you can write reactive rules for declaretive/reactive ui programming
void Start()
{
// Toggle, Input etc as Observable (OnValueChangedAsObservable is a helper providing isOn value on subscribe)
// SubscribeToInteractable is an Extension Method, same as .interactable = x)
MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
// Input is displayed after a 1 second delay
MyInput.OnValueChangedAsObservable()
.Where(x => x != null)
.Delay(TimeSpan.FromSeconds(1))
.SubscribeToText(MyText); // SubscribeToText is helper for subscribe to text
// Converting for human readability
MySlider.OnValueChangedAsObservable()
.SubscribeToText(MyText, x => Math.Round(x, 2).ToString());
}
更多響應式UI編程,請參考Sample12,Sample13和下面的ReactiveProperty部分。
ReactiveProperty,ReactiveCollection
遊戲數據通常需要通知,我們應該使用屬性和事件回調嗎?這樣的話,簡直太麻煩了,還好UniRx爲我們提供了ReactiveProperty,輕量級的屬性代理人。
// Reactive Notification Model
public class Enemy
{
public ReactiveProperty<long> CurrentHp { get; private set; }
public ReactiveProperty<bool> IsDead { get; private set; }
public Enemy(int initialHp)
{
// Declarative Property
CurrentHp = new ReactiveProperty<long>(initialHp);
IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
}
}
// ---
// onclick, HP decrement
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
// subscribe from notification model.
enemy.CurrentHp.SubscribeToText(MyText);
enemy.IsDead.Where(isDead => isDead == true)
.Subscribe(_ =>
{
MyButton.interactable = false;
});
你可以組合使用UnityEvent.AsObservable返回的ReactiveProperties、ReactuveCollections和Observables.所有的UI元素都可作爲可觀察對象(Observable).
泛型ReactiveProperties不能被序列化或者在Unity的Inspecatble中顯示。但是UniRx爲ReactivePropery提供了專門的子類,諸如 int/LongReactiveProperty,Float/DoubleReactiveProperty,StringReactiveProperty,BoolReactiveProperty 等等。(在這查看:(InspectableReactiveProperty.cs),這些屬性都可以在Inspector中編輯,對於你自定義的Enum ReactiveProperty,編寫一個自定義的inspectable ReactiveProperty[T]是很容易的。如果你需要爲ReactiveProperty增加[Myltiline]或者[Range]之類的屬性,你可以使用MultilineReactivePropertyAttribute和RangeReactivePropertyAttribute 替換Unity的Multiline和Range。
所提供的派生自InspecetableReactiveProperty顯示在Inspector中,在值發生更改時發出通知,在Inspector中更改值時也會發出通知。
這個功能由 InspectorDisplayDrawer提供,通過繼承你可以應用你自己自定義的ReactiveProperty:
public enum Fruit
{
Apple, Grape
}
[Serializable]
public class FruitReactiveProperty : ReactiveProperty<Fruit>
{
public FruitReactiveProperty()
{
}
public FruitReactiveProperty(Fruit initialValue)
:base(initialValue)
{
}
}
[UnityEditor.CustomPropertyDrawer(typeof(FruitReactiveProperty))]
[UnityEditor.CustomPropertyDrawer(typeof(YourSpecializedReactiveProperty2))] // and others...
public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer
{
}
如果ReactiveProperty僅在流中被更新,通過使用ReadOnlyReactiveProperty你可以將屬性變爲只讀的。
public class Person
{
public ReactiveProperty<string> GivenName { get; private set; }
public ReactiveProperty<string> FamilyName { get; private set; }
public ReadOnlyReactiveProperty<string> FullName { get; private set; }
public Person(string givenName, string familyName)
{
GivenName = new ReactiveProperty<string>(givenName);
FamilyName = new ReactiveProperty<string>(familyName);
// If change the givenName or familyName, notify with fullName!
FullName = GivenName.CombineLatest(FamilyName, (x, y) => x + " " + y).ToReadOnlyReactiveProperty();
}
}
Model-View-(Reactive)Presenter Pattern
UniRx使得MVP(MVRP)模式的實現成爲可能。
爲什麼我們需要使用MVP模式替換MVVM模式。Unity中沒有提供UI綁定機制,創建綁定層太過於複雜且會影響性能。儘管如此,視圖任然需要更新。Presenter持有視圖的組件並能更新視圖。雖然不是真正的綁定,但Observables 啓用了對通知的訂閱,它看起來更真實,這個模式被稱爲Reactive Presenter.
// Presenter for scene(canvas) root.
public class ReactivePresenter : MonoBehaviour
{
// Presenter is aware of its View (binded in the inspector)
public Button MyButton;
public Toggle MyToggle;
// 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
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
// Models notify Presenters via Rx, and Presenters update their views
enemy.CurrentHp.SubscribeToText(MyText);
enemy.IsDead.Where(isDead => isDead == true)
.Subscribe(_ =>
{
MyToggle.interactable = MyButton.interactable = false;
});
}
}
// The Model. All property notify when their values change
public class Enemy
{
public ReactiveProperty<long> CurrentHp { get; private set; }
public ReactiveProperty<bool> IsDead { get; private set; }
public Enemy(int initialHp)
{
// Declarative Property
CurrentHp = new ReactiveProperty<long>(initialHp);
IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
}
}
在Unity的Hierarchy中,視圖就是一個場景。視圖在初始化時有UnityEngine和Presenters關聯。xxxAsObservable方法使得創建事件信號變得簡單,沒有任何開銷。SubscribeToText和SubscribeToInteractable是類似綁定的簡單的工具。雖然簡單,但是非常強大。他們很符合Unity的編程環境,並提供了高性能和簡潔的體系結構。
V->RP->M->RP->V以響應式的方式完成的連接起來,UniRx提供了適配的方法和類。當然你也可以使用其它的MVVM(或者MV*) 框架代替,UniRx/ReactiveProperty僅僅是一個簡單的工具集。
GUI編程也受益於ObservableTriggers.ObservableTriggers轉化Unity事件爲Observables(可觀察對象),因此可以使用它們來組成MV®P模式。例如:ObservableEventTrigger 轉化UGUI事件爲Observable(可觀察對象):
var eventTrigger = this.gameObject.AddComponent<ObservableEventTrigger>();
eventTrigger.OnBeginDragAsObservable()
.SelectMany(_ => eventTrigger.OnDragAsObservable(), (start, current) => UniRx.Tuple.Create(start, current))
.TakeUntil(eventTrigger.OnEndDragAsObservable())
.RepeatUntilDestroy(this)
.Subscribe(x => Debug.Log(x));
ReactiveCommand,AsyncReactiveCommand
ReactiveCommand作爲可交互按鈕命令的抽象。
public class Player
{
public ReactiveProperty<int> Hp;
public ReactiveCommand Resurrect;
public Player()
{
Hp = new ReactiveProperty<int>(1000);
// If dead, can not execute.
Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();
// Execute when clicked
Resurrect.Subscribe(_ =>
{
Hp.Value = 1000;
});
}
}
public class Presenter : MonoBehaviour
{
public Button resurrectButton;
Player player;
void Start()
{
player = new Player();
// If Hp <= 0, can't press button.
player.Resurrect.BindTo(resurrectButton);
}
}
AsyncReactiveCommand 是ReactiveCommand的異步形式,將CanExecute(大多數情況下綁定到按鈕的interactable)更改爲false,直到異步操作執行完成。
public class Presenter:MonoBehaviour{
public UnityEngine.UI.Button button;
void Start(){
var command=new AsyncReactiveCommand();
command.Subscribe(_=>{
return Observable.Timer(TimeSpan.FromSeconds(3)).asUnitObservable();
});
command.BindTo(button);
button.BindToOnClick(_=>{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
}
}
AsyncReactiveCommand 有三個構造函數。
- ()CanExecute默認爲false,直到異步執行完成
- (IObservable canExecuteSource) 當canExecuteSource發送true並且不在執行時,與empty混合,CanExecute變爲true.
- (IReactiveProperty sharedCanExecute) 在多個AsyncReactiveCommands之間共享執行狀態,如果其中一個AsyncReactiveCommand正在執行,其它AsyncReactiveCommands(擁有一個sharedCanExecute屬性)的CanExecute變爲false,直到這個AsyncCommand異步執行完成.
public class Presenter : MonoBehaviour
{
public UnityEngine.UI.Button button1;
public UnityEngine.UI.Button button2;
void Start()
{
// share canExecute status.
// when clicked button1, button1 and button2 was disabled for 3 seconds.
var sharedCanExecute = new ReactiveProperty<bool>();
button1.BindToOnClick(sharedCanExecute, _ =>
{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
button2.BindToOnClick(sharedCanExecute, _ =>
{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
}
}
MessageBroker, AsyncMessageBroker
MessageBroker基於Rx的內存的pubsub系統的按類型過濾的。
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是MessageBroker的異步形式,可以await發佈調用
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");
});
UniRx.ToolKit
UniRx.ToolKit 中包含一些Rx-ish工具。當前版本中包含 ObjectPool(對象池)和AsyncObjectPool(異步對象池),這個池子在租賃前可以租、回收和異步預加載。
// sample class
public class Foobar : MonoBehaviour
{
public IObservable<Unit> ActionAsync()
{
// heavy, heavy, action...
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
}
}
public class FoobarPool : ObjectPool<Foobar>
{
readonly Foobar prefab;
readonly Transform hierarchyParent;
public FoobarPool(Foobar prefab, Transform hierarchyParent)
{
this.prefab = prefab;
this.hierarchyParent = hierarchyParent;
}
protected override Foobar CreateInstance()
{
var foobar = GameObject.Instantiate<Foobar>(prefab);
foobar.transform.SetParent(hierarchyParent);
return foobar;
}
// You can overload OnBeforeRent, OnBeforeReturn, OnClear for customize action.
// In default, OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false)
// protected override void OnBeforeRent(Foobar instance)
// protected override void OnBeforeReturn(Foobar instance)
// protected override void OnClear(Foobar instance)
}
public class Presenter : MonoBehaviour
{
FoobarPool pool = null;
public Foobar prefab;
public Button rentButton;
void Start()
{
pool = new FoobarPool(prefab, this.transform);
rentButton.OnClickAsObservable().Subscribe(_ =>
{
var foobar = pool.Rent();
foobar.ActionAsync().Subscribe(__ =>
{
// if action completed, return to pool
pool.Return(foobar);
});
});
}
}
Visual Studio Analyzer
對Visual Studio 2015的用戶來說,UniRx提供了一個自定義的分析工具————UniRxAnalyzer。例如:檢測流什麼時候沒有被訂閱。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-35LUvwph-1570018427291)(https://github.com/neuecc/UniRx/raw/master/StoreDocument/VSAnalyzer.jpg)]
ObservableWWW 在訂閱前不會被觸發,分析器拋出使用不當的警告,你可以通過NuGet下載它。
- Install-Package UniRxAnalyzer
請在github的Issuse中提交你的關於Analyzer的新想法。
案例
Windows Store/Phone App (NETFX_CORE)
一些接口,例如UniRx.IObservable和System.IObservable ,當提交應用到Windows Store App時會引起衝突,因此,當時用NETFX_CORE時,請不要使用諸如UniRx.IObservable這樣的結構,使用其簡短的形式,不要添加命名空間,衝突就解決了。
分離DLL
如果你想使用預構建的UniRx,你可以構建自己的dll,克隆這個項目並打開UniRx.sln,你會看到這是一個完全分離的項目。你需要像這樣定義編譯宏定義UNITY;UNITY_5_4_OR_NEWER;UNITY_5_4_0;UNITY_5_4;UNITY_5; + UNITY_EDITOR, UNITY_IPHONE或者其它平臺宏定義,在發佈頁面我們不提供預編譯二進制文件,因爲在asset store 中的編譯符號各不相同。
參考
-
UniRx/wiki
UniRx API 文檔: -
ReactiveX
ReactiveX的主頁.介紹,[所有運算符(http://reactivex.io/documentation/operators.html)以圖形化的方式來說明和講解,使得理解起來更加容易。UniRxshReactiveX的官方語言。 -
Introduction to Rx
非常棒的在線教程和電子書
-
Beginner’s Guide to the Reactive Extensions
更多Rx的視頻、ppt和文檔
-
PPT介紹 By @@torisoup
PPT介紹和遊戲示例 By @Xerios
更多內容,歡迎訪問: