zoukankan      html  css  js  c++  java
  • 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 中的编译符号各不相同。

  • 相关阅读:
    eventkeyboardmouse
    代理 IP
    网关 192.168.2.1 114.114.114.114 dns查询
    http ssl
    SSDP 抓包
    抓包登录信息提交
    危险的input 微博的过去
    firstChild.nodeValue
    浏览器控制台
    haproxy 中的http请求和https请求
  • 原文地址:https://www.cnblogs.com/tuyile006/p/12605384.html
Copyright © 2011-2022 走看看