UniRx官方入门文档
UniRx 官方文档翻译
- UniRx官方入门文档
- UniRx入门系列一
- UniRx入门系列二
- UniRx入门系列三
- UniRx入门系列四
- 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));// googleDebug.Log(xs[1].Substring(0,100));// bingDebug.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 ObservableYieldInstructionvar 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 IObservablereturn 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 namespacepublic class MyComponent : MonoBehaviour
{void Start(){// Get the plain objectvar cube = GameObject.CreatePrimitive(PrimitiveType.Cube);// Add ObservableXxxTrigger for handle MonoBehaviour's event as Observablecube.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(); // fieldvoid 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 callbackUnityEngine.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 efficientMainThreadDispatcher.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 delayMyInput.OnValueChangedAsObservable().Where(x => x != null).Delay(TimeSpan.FromSeconds(1)).SubscribeToText(MyText); // SubscribeToText is helper for subscribe to text// Converting for human readabilityMySlider.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 PropertyCurrentHp = 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 ReactivePropertyEnemy 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 viewsenemy.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 PropertyCurrentHp = 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 poolpool.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 中的编译符号各不相同。
参考
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和文档
Reactive Programming, Unity 3D and you
- Repository of UniRxSimpleGame
PPT介绍 By @@torisoup
- Repository of UniRxSimpleGame
PPT介绍和游戏示例 By @Xerios
GDC 2016 Sessions of Adventure Capialist
更多内容,欢迎访问:
UniRx官方入门文档相关推荐
- 学习记录《Simulink 快速入门 —— 官方Help文档》
Simulink 快速入门 -- 官方Help文档 1. Simulink 模块图 2 创建简单模型 2.1 打开新模型 2.2 打开 Simulink库浏览器 2.2.1将模块添加到模型 2.2.2 ...
- GeoPandas官方快速入门文档(中文翻译版)
GeoPandas官方快速入门文档(中文翻译版) 原文链接>>Introduction to GeoPandas 点这里可以一键跑通>>GeoPandas 简介 GeoPand ...
- 【Auto.JS】Autojs官方提取文档使用说明函数 (2)
接上一篇文章:Autojs官方提取文档使用说明函数 (1) Images Stability: 2 - Stable images模块提供了一些手机设备中常见的图片处理函数,包括截图.读写图片.图片剪 ...
- CloudEvents 入门文档
CloudEvents 入门文档 - 1.0.3 版本 文档来自:GitHub CloudEvents 摘要 这份非技术规范文档用来为你提供关于 CloudEvents 规范的总体概览.它补充了 Cl ...
- 跟着官方帮助文档学ICEM网格划分(附视频教程)
作者 | 如鹰展翅上腾 导读:划分结构化网格是ICEM软件的一大特色,自学的话会比较耗时,如有人带的话,入门进阶都是比较快的,就像一层窗户纸一捅就破.软件是使我们研究的问题得以求解的工具,重点侧向于操 ...
- PyQt5随笔:PyQt5 官方网站文档、一些基础教程网站和书籍推荐
PyQt5随笔:PyQt5 官方网站文档.一些基础教程网站和书籍推荐 文章目录 PyQt5随笔:PyQt5 官方网站文档.一些基础教程网站和书籍推荐 1. 官方网站文档 2.基础教程网站(点击标题即可 ...
- Java日志框架-Logback手册中文版以及官方配置文档教程
Logback手册中文版:(链接: https://pan.baidu.com/s/1bpMyasR 密码: 6u5c),虽然版本有点旧,但是大体意思差不多,先用中文版了解个大概,然后一切最新的配置以 ...
- python语法手册chm-python3.5.2官方帮助文档 参考手册(CHM版)
python3.5.2官方帮助文档 参考手册(CHM版) python3.5.2官方帮助文档 参考手册(CHM版),欢迎下载! 新语法特性: PEP 492,具有async和await语法的协同程序. ...
- python帮助文档中文版下载-python3.5.2官方帮助文档 参考手册(CHM版)
python3.5.2官方帮助文档 参考手册(CHM版) python3.5.2官方帮助文档 参考手册(CHM版),欢迎下载! 新语法特性: PEP 492,具有async和await语法的协同程序. ...
最新文章
- HDU - 3911 Black And White 区间翻转+区间连续最长
- 给定n,a求最大的k,使n!可以被a^k整除但不能被a^(k+1)整除。
- 文件上传漏洞——DVWA练习
- NYOJ2—括号配对问题
- RabbitMQ实现RPC
- 关于数据库的水平切分和垂直切分的一些概念(转)
- Qt ToolBar工具栏里同时显示图标和文字
- GNU:GCC -O 优化等级
- [转载]c#委托事件简单例子
- 基于Netty自己动手实现Web框架
- Node.js:Express
- 智能优化算法:蜻蜓优化算法-附代码
- Pytorch安装步骤
- python车牌识别系统开源代码_python利用百度云接口实现车牌识别的示例
- 计算机专业动漫设计毕业论文,计算机动漫设计与制作专业毕业论文.doc
- 手指检测心跳设计——传感器制作篇
- 上顿号符号_顿号在键盘上怎么打 常见的电脑符号输入方法说明
- java关于地图经纬度的计算
- HM2022ssm-mp4.2【DML增删改编程控制(2)】
- 穷人冲冲冲:7个有钱人买,穷人不买的东西