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()); } }

Unity Coroutine

當然微協程存在一些限制,經支持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中更改值時也會發出通知。
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)模式的實現成為可能。
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的編程環境,並提供了高性能和簡潔的體系結構。
Presenter View

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的新想法。

案例

UniRx/Examples

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 中的編譯符號各不相同。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM