UniRx之基础入门

news2024/10/7 12:19:54

什么是Rx

官方ReactiveX简介:
An API for asynchronous programming with observable streams。

通过这句话我们可以得到:

1.首先Rx是个编程接口,不同语言提供不同实现。例如JVM语言中的RxJava。
2.使用场景,异步编程中。
3.基于可观察的事件流。

我们主要解释下事件流:
Rx中把所有的事件都通过流的形式来处理,不通的操作符,相当于不通的流处理方式。最终筛选出我们需要的流。

下面的动态图很形象地解释了Reactive:
在这里插入图片描述
在这里插入图片描述

什么是UniRx?

UniRx(Unity的响应式编程框架)是.Net响应式编程框架的重新实现版本。官方的Rx的实现方式是非常棒的。但是,在Unity中使用会有一些问题;在IOS的IL2CPP中有兼容性的问题。UniRx修复这些问题,并针对Unity添加了一些特别的工具。支持的平台包括PC/Mac/Android/iOS/WebGL/WindowsStore/等等。

UniRx可在Asset Store(免费)中下载: http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT

博客信息更新:https://medium.com/@neuecc

Unity Forums 在线支持,有问题随时向我提问:http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity

发现说明: UniRx/releases

UniRx 作为核心库+平台适配器(MainThreadScheduler/FromCoroutinue/etf)+框架(ObservableTriggers/ReactiveProperty/etc)

注意:async/await 集成(UniRx.Async)被分离到Cysharp/UniTask 7.0之后的版本

为什么使用Rx?

通常,在Unity对网络操作要求使用WWW和Coroutine.但是出于以下几点原因(或者其它原因)使用协程来进行异步操作并不顺手:

1.虽然协程的返回类型必须是IEnumerator,但是协程不能返回任何值。
2.因为yield return 语句不能被try-catch结构体包裹,协程中不能处理异常。

这种缺乏可组合性导致程序的紧耦合,往往造成IEnumators中逻辑过于复杂。

Rx可以解决异步调用的“伤痛”,Rx 是一个使用可观察集合和LINQ风格查询运算符组合成的基于异步和基于事件的可编程库。

游戏循环(every Update,OnCollisionEnter),传感器数据(Kinect,Leap Motion,VR Input 等等)这些类型的事件。Rx将事件表示为响应式序列。通过使用LINQ查询运算符,Rx变得容易组合且支持基于时间的操作。

Unity通常是单线程的,但是UniRx促进了多线程joins、cancel 访问GameObject,等等。

UniRx为UGUI提供了UI编程。所有的UI事件(clicked,valuechanged,等)均可以被转化为UniRx的事件流。

Unity 在2017之后支持C# 中的astnc/await。UniRx 为Unity提供了更轻量、强大的async/await集成。请看: Cysharp/UniTask.

介绍

非常棒的介绍Rx的文章:The introduction to Reactive Programming you’ve been missing.

以下代码使用UniRx实现了文章中的双击检测事例:

var clickStream=Observable.EveryUpdate()
.Where(_=>Input.GetMouseButtonDown(0));

clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
    .Where(xs => xs.Count >= 2)
    .Subscribe(xs => Debug.Log("DoubleClick Detected! Count:" + xs.Count));

本事例演示了以下功能(仅仅使用5行代码):

游戏循环(Update)作为事件流
可组合事件流
合并自身流
易于处理基于时间的操作

网络操作

使用ObservableWWW 进行一步网络操作。它的Get/Post函数返回可订阅的IObservables.

ObservableWWW.Get("http://google.co.jp/")
.Subscribe(
    x=Debug.Log(x.Substring(0,100)),
    ex=Debug.LogExecption(ex)
);

Rx是可组合也是可以取消的,你可以使用LINQ 查询表达式:

var query=from google in ObservableWWW.Get("http://google.com/")
          from bing in ObservableWWW.Get("http://bing.com/")
          from unknow in ObservableWWW(goole+bing)
          select new {google,bing,unknow};
var cancel=query.Subscribe(x=>Debug.Log(x));

cancel.Dispose();

使用Observable.WhenAll 执行并行请求(parallel):

var parallel=Observable.WhenAll(
    ObservableWWW.Get("http://google.com/"),
    ObservableWWW.Get("http://bing.com/"),
    ObservableWWW.Get("http://unity3d.com/")
);
parallel.Subscribe(xs=>{
    Debug.Log(xs[0].Substring(0,100));// google
    Debug.Log(xs[1].Substring(0,100));// bing
    Debug.Log(xs[2].Substring(0,100));// unity
});

提供进度信息:

// notifier for progress use ScheduledNotifier or new Progress<float>(/* action */)
var progressNotifier=new ScheduledNotifier<float>();
// pass notifier to WWW.Get/Post
progressNotifier.Subscribe(x=>Debug.Log(x));

错误处理:

// If WWW has .error, ObservableWWW throws WWWErrorException to onError pipeline.
// WWWErrorException has RawErrorMessage, HasResponse, StatusCode, ResponseHeaders
ObservableWWW.Get("http://www.google.com/404")
    .CatchIgnore((WWWErrorException ex) =>
    {
        Debug.Log(ex.RawErrorMessage);
        if (ex.HasResponse)
        {
            Debug.Log(ex.StatusCode);
        }
        foreach (var item in ex.ResponseHeaders)
        {
            Debug.Log(item.Key + ":" + item.Value);
        }
    })
    .Subscribe();

使用IEnumators (Coroutines)

IEnumator(Coroutine)是Unity的基本异步工具,UniRx集成了协程和IObservables,你可以在协程中写异步代码,并使用UniRx编排他们。这是控制异步流最好的方式。

// two coroutines
IEnumerator AsyncA()
{
    Debug.Log("a start");
    yield return new WaitForSeconds(1);
    Debug.Log("a end");
}

IEnumerator AsyncB()
{
    Debug.Log("b start");
    yield return new WaitForEndOfFrame();
    Debug.Log("b end");
}
// main code
// Observable.FromCoroutine converts IEnumerator to Observable<Unit>.
// You can also use the shorthand, AsyncA().ToObservable()
        
// after AsyncA completes, run AsyncB as a continuous routine.
// UniRx expands SelectMany(IEnumerator) as SelectMany(IEnumerator.ToObservable())
var cancel = Observable.FromCoroutine(AsyncA)
    .SelectMany(AsyncB)
    .Subscribe();

// you can stop a coroutine by calling your subscription's Dispose.
cancel.Dispose();

在Unity5.3中,你可以使用ToYieldInstruction将Observable转化为Coroutine:

IEnumerator TestNewCustomYieldInstruction()
{
    // wait Rx Observable.
    yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();

    // you can change the scheduler(this is ignore Time.scale)
    yield return Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.MainThreadIgnoreTimeScale).ToYieldInstruction();

    // get return value from ObservableYieldInstruction
    var o = ObservableWWW.Get("http://unity3d.com/").ToYieldInstruction(throwOnError: false);
    yield return o;

    if (o.HasError) { Debug.Log(o.Error.ToString()); }
    if (o.HasResult) { Debug.Log(o.Result); }

    // other sample(wait until transform.position.y >= 100) 
    yield return this.transform.ObserveEveryValueChanged(x => x.position).FirstOrDefault(p => p.y >= 100).ToYieldInstruction();
}

通常情况下,当我们想要协程返回一个值时,我们必须使用回调。Observable.FromCoroutine 可以将协程转化为可取消的IObservable[T]。

public static IObservable<string> GetWWW(string url)
{
    // convert coroutine to IObservable
    return Observable.FromCoroutine<string>((observer, cancellationToken) => GetWWWCore(url, observer, cancellationToken));
}

// IObserver is a callback publisher
// Note: IObserver's basic scheme is "OnNext* (OnError | Oncompleted)?" 
static IEnumerator GetWWWCore(string url, IObserver<string> observer, CancellationToken cancellationToken)
{
    var www = new UnityEngine.WWW(url);
    while (!www.isDone && !cancellationToken.IsCancellationRequested)
    {
        yield return null;
    }

    if (cancellationToken.IsCancellationRequested) yield break;

    if (www.error != null)
    {
        observer.OnError(new Exception(www.error));
    }
    else
    {
        observer.OnNext(www.text);
        observer.OnCompleted(); // IObserver needs OnCompleted after OnNext!
    }
}

这还有更多的示例,下方展示多个OnNext的形式:

public static IObservable<float> ToObservable(this UnityEngine.AsyncOperation asyncOperation)
{
    if (asyncOperation == null) throw new ArgumentNullException("asyncOperation");

    return Observable.FromCoroutine<float>((observer, cancellationToken) => RunAsyncOperation(asyncOperation, observer, cancellationToken));
}

static IEnumerator RunAsyncOperation(UnityEngine.AsyncOperation asyncOperation, IObserver<float> observer, CancellationToken cancellationToken)
{
    while (!asyncOperation.isDone && !cancellationToken.IsCancellationRequested)
    {
        observer.OnNext(asyncOperation.progress);
        yield return null;
    }
    if (!cancellationToken.IsCancellationRequested)
    {
        observer.OnNext(asyncOperation.progress); // push 100%
        observer.OnCompleted();
    }
}

// usecase
Application.LoadLevelAsync("testscene")
    .ToObservable()
    .Do(x => Debug.Log(x)) // output progress
    .Last() // last sequence is load completed
    .Subscribe();

多线程的使用

// Observable.Start is start factory methods on specified scheduler
// default is on ThreadPool
var heavyMethod = Observable.Start(() =>
{
    // heavy method...
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
    return 10;
});

var heavyMethod2 = Observable.Start(() =>
{
    // heavy method...
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
    return 10;
});

// Join and await two other thread values
Observable.WhenAll(heavyMethod, heavyMethod2)
    .ObserveOnMainThread() // return to main thread
    .Subscribe(xs =>
    {
        // Unity can't touch GameObject from other thread
        // but use ObserveOnMainThread, you can touch GameObject naturally.
        (GameObject.Find("myGuiText")).guiText.text = xs[0] + ":" + xs[1];
    }); 

DefaultScheduler(默认调度器)

UniRx默认是基于时间操作的(Interval、Timer、Buffer(timeSpan)等等),使用Scheduler.MainThread作为它们的调度器。UniRx中的大多数运算符(Observable.Start除外)都是在单个线程上执行的;因此不需要ObserverOn,并且可以忽略线程安全问题。虽然和标准 .NET 中的Rx实现不同,但是这更符合Unity的环境。
Scheduler.Mainthread的执行受Time.timeScale的影响,如果你想要在执行时忽略TimeScale,你可以使用Scheduler.MainThreadIgnoreTimeScale代替。

MonoBehaviour triggers

UniRx使用UniRx.Triggers处理MonoBehaviour事件:

using UniRx;
using UniRx.Triggers; // need UniRx.Triggers namespace

public class MyComponent : MonoBehaviour
{
    void Start()
    {
        // Get the plain object
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);

        // Add ObservableXxxTrigger for handle MonoBehaviour's event as Observable
        cube.AddComponent<ObservableUpdateTrigger>()
            .UpdateAsObservable()
            .SampleFrame(30)
            .Subscribe(x => Debug.Log("cube"), () => Debug.Log("destroy"));

        // destroy after 3 second:)
        GameObject.Destroy(cube, 3f);
    }
}

支持的triggers如列表所示:UniRx.wiki#UniRx.Triggers

通过直接订阅Component/GameObject上的扩展方法返回的Observables(可观察对象),可以更轻松的处理事件,这些方法被自动注入到ObservableTrigger中(除了ObservableEventTrigger和ObservableStateMachineTrigger):

using UniRx;
using UniRx.Triggers;
public class DragAndDropOnce:MonoBehaviour{
    void Start(){
        this.OnMouseDownAsObservable()
        .SelectMany(_=>this.UpdateAsObservable())
        .TakeUntil(this.OnMouseUpAsObservable())
        .Select(_=>Input.mousePosition)
        .Subscribe(x=>Debug.Log(x));
    }
}

之前版本中UniRx提供了ObservableMonoBehaiour.新版本中以不再对其提供支持,请使用UniRx.Triggers代替。

创建自定义Triggers

将Unity事件转化为Observable(可观察对象)是处理Unity事件最好的方式。如果UniRx提供的标准的triggers不够使用的话,你可以自定义triggers.为了演示,下方提供了一个基于UGUI的LongTap(长按)触发演示:

public class ObservableLongPointerDownTrigger : ObservableTriggerBase, IPointerDownHandler, IPointerUpHandler{
    public float IntervalSecond=1f;
    Subject<Unit> onLongPointerDown;
    float> raiseTime;

    void Update(){
        if (raiseTime!=null&&raiseTime<=Time.realtimeSinceStartup){
            if (onLongPointerDown!=null)onLongPointerDown.OnNext(Unit.Default);
            raiseTime=null;
        }
    }
     void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        raiseTime = Time.realtimeSinceStartup + IntervalSecond;
    }
    void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
    {
        raiseTime = null;
    }
    public IObservable<Unit> OnLongPointerDownAsObservable()
    {
        return onLongPointerDown ?? (onLongPointerDown = new Subject<Unit>());
    }
    protected override void RaiseOnCompletedOnDestroy()
    {
        if (onLongPointerDown != null)
        {
            onLongPointerDown.OnCompleted();
        }
    }
}

它的使用像标准triggers一样简单:

var trigger = button.AddComponent<ObservableLongPointerDownTrigger>();
trigger.OnLongPointerDownAsObservable().Subscribe();

Observable 生命周期管理

什么时候调用OnCompleted? 使用UniRx时,必须考虑订阅的生命周期管理。当与GameObject对象相连的游戏对象被销毁时,ObservableTriggers会调用OnCompleted.其它的静态生成器方法(Observable.Timer、Observable.EveryUpdate…等等,并不会自动停止,他们的订阅需要被手动管理。

Rx提供了一些辅助方法,比如,IDisposable.AddTo运行你一次释放多个订阅:

// CompositeDisposable is similar with List<IDisposable>, manage multiple IDisposable
CompositeDisposable disposables = new CompositeDisposable(); // field

void Start()
{
    Observable.EveryUpdate().Subscribe(x => Debug.Log(x)).AddTo(disposables);
}
void OnTriggerEnter(Collider other)
{
    // .Clear() => Dispose is called for all inner disposables, and the list is cleared.
    // .Dispose() => Dispose is called for all inner disposables, and Dispose is called immediately after additional Adds.
    disposables.Clear();
}

如果你想在GameObject被销毁时自动释放,你可以使用AddTo(GameObject/Component):

void Start(){
    Observable.IntervalFrame(30).Subscribe(x=Debug.Log(x)).AddTo(this);
}

AddTo可以促进流的自动释放,如果你需要在管道中队OnCompleted进行特殊处理,那么你可以使用TakeWhile、TakeUntil、TakeUntilDestroy和TakeUntilDisable代替:

Observable.IntervalFrame(30).TakeUntilDisable(this)
    .Subscribe(x => Debug.Log(x), () => Debug.Log("completed!"));

当你处理事件时,Repeat是一种重要但危险的方法,它可能会造成程序的无线循环,因此,请谨慎使用它:

using UniRx;
using UniRx.Triggers;
public class DangerousDragAndDrop:MonoBehaviour{
    void Start(){
        this.gameObject.OnMouseDownAsObservable()
        .SelectMany(_=>this.gameObject.UpdateAsObservable())
        .TakeUtil(this.gameObject.OnMouseUpAsObservable())
        .Select(_=>Input.mousePosition)
        .Repeat()
        .Subscribe(x=>Debug.Log(x));
    }
}

UniRx另外提供了一种安全使用Repeat的方法。RepeatSafe:
如果重复调用OnComplete,Repeat将会停止。RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component)允许在目标对象被销毁时停止。

this.gameObject.OnMouseDownAsObservable()
    .SelectMany(_ => this.gameObject.UpdateAsObservable())
    .TakeUntil(this.gameObject.OnMouseUpAsObservable())
    .Select(_ => Input.mousePosition)
    .RepeatUntilDestroy(this) // safety way
    .Subscribe(x => Debug.Log(x));  

UniRx确保hot Observable(FromEvent/Subject/ReactiveProperty/UnityUI.AsObservable…, 类似事件)可以持续的处理异常。什么意思?如果在Subscribe中订阅,这不分离事件。

button.OnClickAsObservable().Subscribe(_ =>
{
    // If throws error in inner subscribe, but doesn't detached OnClick event.
    ObservableWWW.Get("htttp://error/").Subscribe(x =>
    {
        Debug.Log(x);
    });
});

这种行为有时很有用,比如用户事件的处理。

每一个类的实例都提供了一个ObserveEveryValueChanged的方法。这个方法可以每一帧检测某个值发生的变化:

// watch position change
this.transform.ObserveEveryValueChanged(x => x.position).Subscribe(x => Debug.Log(x));

这是非常有用的,如果观察的目标是一个GameObject;当GameObject被销毁时,订阅将自动停止并调用OnCompleted.如果观察的对象是一个原生的C#对象,OnCompleted将在GC时被调用。

将Unity回调转化为IObservables(可观察对象)

使用Subject(或者AsyncSubject进行异步操作):

public class LogCallback
{
    public string Condition;
    public string StackTrace;
    public UnityEngine.LogType LogType;
}

public static class LogHelper
{
    static Subject<LogCallback> subject;

    public static IObservable<LogCallback> LogCallbackAsObservable()
    {
        if (subject == null)
        {
            subject = new Subject<LogCallback>();

            // Publish to Subject in callback
            UnityEngine.Application.RegisterLogCallback((condition, stackTrace, type) =>
            {
                subject.OnNext(new LogCallback { Condition = condition, StackTrace = stackTrace, LogType = type });
            });
        }

        return subject.AsObservable();
    }
}

// method is separatable and composable
LogHelper.LogCallbackAsObservable()
    .Where(x => x.LogType == LogType.Warning)
    .Subscribe();

LogHelper.LogCallbackAsObservable()
    .Where(x => x.LogType == LogType.Error)
    .Subscribe();

Unity5中,Application.RegisterLogCallback被移除了,转而提供Application.logMessageReceived的支持,因此,我们现在可以简单的使用Observable.FromEvent.

public static IObservable<LogCallback> LogCallbackAsObservable()
{
    return Observable.FromEvent<Application.LogCallback, LogCallback>(
        h => (condition, stackTrace, type) => h(new LogCallback { Condition = condition, StackTrace = stackTrace, LogType = type }),
        h => Application.logMessageReceived += h, h => Application.logMessageReceived -= h);
}

Stream Logger

// using UniRx.Diagnostics;

// logger is threadsafe, define per class with name.
static readonly Logger logger = new Logger("Sample11");

// call once at applicationinit
public static void ApplicationInitialize()
{
    // Log as Stream, UniRx.Diagnostics.ObservableLogger.Listener is IObservable<LogEntry>
    // You can subscribe and output to any place.
    ObservableLogger.Listener.LogToUnityDebug();

    // for example, filter only Exception and upload to web.
    // (make custom sink(IObserver<EventEntry>) is better to use)
    ObservableLogger.Listener
        .Where(x => x.LogType == LogType.Exception)
        .Subscribe(x =>
        {
            // ObservableWWW.Post("", null).Subscribe();
        });
}

// Debug is write only DebugBuild.
logger.Debug("Debug Message");

// or other logging methods
logger.Log("Message");
logger.Exception(new Exception("test exception"));

Debugging

UniRx.Diagnostics命名空间下的Debug运算符便于用于调试。

// needs Diagnostics using
using UniRx.Diagnostics;

---

// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
    var subject = new Subject<int>();

    subject.Debug("DebugDump, Normal").Subscribe();

    subject.OnNext(1);
    subject.OnNext(10);
    subject.OnCompleted();
}

// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
    var subject = new Subject<int>();

    var d = subject.Debug("DebugDump, Cancel").Subscribe();

    subject.OnNext(1);
    d.Dispose();
}

// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
    var subject = new Subject<int>();

    subject.Debug("DebugDump, Error").Subscribe();

    subject.OnNext(1);
    subject.OnError(new Exception());
}

在在OnNext,OnError,OnCompleted,OnCancel,OnSubscribe时序上显示序列元素以进行Debug.Log,仅当#if DEBUG时才被启用。

Unity-specific Extra Gems

// Unity's singleton UiThread Queue Scheduler
Scheduler.MainThreadScheduler 
ObserveOnMainThread()/SubscribeOnMainThread()

// Global StartCoroutine runner
MainThreadDispatcher.StartCoroutine(enumerator)

// convert Coroutine to IObservable
Observable.FromCoroutine((observer, token) => enumerator(observer, token)); 

// convert IObservable to Coroutine
yield return Observable.Range(1, 10).ToYieldInstruction(); // after Unity 5.3, before can use StartAsCoroutine()

// Lifetime hooks
Observable.EveryApplicationPause();
Observable.EveryApplicationFocus();
Observable.OnceApplicationQuit();

FrameCount-based timeoperators

UniRx 提供了一些继续帧数的时间运算符

Method
EveryUpdate
EevryFixedUpdate
EveryEndOfFrame
EveryGameObjectUpdate
EveryLateUpdate
ObserveOnMainThread
NextFrame
IntervalFrame
TimerFrame
DelayFrame
SampleFrame
ThrottleFrame
ThrottleFirstFrame
TimeoutFrame
DelayFrameSubscription
FrameInterval
FrameTimeInterval
BatchFrame

例如,一次延时调用:

Observable.TimerFrame(100).Subscribe(_ => Debug.Log("after 100 frame"));

Every* 方法的执行顺序如下:

EveryGameObjectUpdate(in MainThreadDispatcher's Execution Order) ->
EveryUpdate -> 
EveryLateUpdate -> 
EveryEndOfFrame

如果在MainThreadDispatcher之前调用了调用者,则从同一帧调用EveryGameObjectUpdate.(我建议对MainThreadDispatcher的调用在同一帧中优先于EveryLateUpdate、EveryEndOfFrame),EveryUpdate在下一帧中调用。

MicroCoroutine(微协程)

微协程的优点在于内存高效和执行快速。它的实现是基于Unity blog’s 10000 UPDATE() CALLS,避免了托管内存-非托管内存的开销,以致迭代速度提升了10倍。微协程自动用于基于帧数的时间运算符和ObserveEveryValueChanged.

如果你想使用微协程替代Unity自带的协程(Coroutine),使用MainThreadDispatcher.StartUpdateMicroCoroutine 或者Observable.FromMicroCoroutine.

int counter;

IEnumerator Worker()
{
    while(true)
    {
        counter++;
        yield return null;
    }
}

void Start()
{
    for(var i = 0; i < 10000; i++)
    {
        // fast, memory efficient
        MainThreadDispatcher.StartUpdateMicroCoroutine(Worker());

        // slow...
        // StartCoroutine(Worker());
    }
}

在这里插入图片描述

当然微协程存在一些限制,经支持yield return null 迭代,并且其更新时间取决于启动微协程的方法(StartUpdateMicroCoroutine,StartFixedUpdateMicroCoroutine,StartEndOfFrameMicroCoroutine)。

如果和其它IObservable结合起来,你可以检测已完成的属性,比如:isDone.

IEnumerator MicroCoroutineWithToYieldInstruction()
{
    var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
    while (!www.IsDone)
    {
        yield return null;
    }

    if (www.HasResult)
    {
        UnityEngine.Debug.Log(www.Result);
    }
}

UGUI 集成

UniRx可以很容易的处理UnityEvent,使用UnityEvent.AsObservable 订阅事件:

public Button MyButton;
// ---
MyButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));

将事件视为可观察对象可启用声明式UI编程。

public Toggle MyToggle;
public InputField MyInput;
public Text MyText;
public Slider MySlider;

// On Start, you can write reactive rules for declaretive/reactive ui programming
void Start()
{
    // Toggle, Input etc as Observable (OnValueChangedAsObservable is a helper providing isOn value on subscribe)
    // SubscribeToInteractable is an Extension Method, same as .interactable = x)
    MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
    
    // Input is displayed after a 1 second delay
    MyInput.OnValueChangedAsObservable()
        .Where(x => x != null)
        .Delay(TimeSpan.FromSeconds(1))
        .SubscribeToText(MyText); // SubscribeToText is helper for subscribe to text
    
    // Converting for human readability
    MySlider.OnValueChangedAsObservable()
        .SubscribeToText(MyText, x => Math.Round(x, 2).ToString());
}

更多响应式UI编程,请参考Sample12,Sample13和下面的ReactiveProperty部分。

ReactiveProperty,ReactiveCollection

游戏数据通常需要通知,我们应该使用属性和事件回调吗?这样的话,简直太麻烦了,还好UniRx为我们提供了ReactiveProperty,轻量级的属性代理人。

// Reactive Notification Model
public class Enemy
{
    public ReactiveProperty<long> CurrentHp { get; private set; }

    public ReactiveProperty<bool> IsDead { get; private set; }

    public Enemy(int initialHp)
    {
        // Declarative Property
        CurrentHp = new ReactiveProperty<long>(initialHp);
        IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
    }
}

// ---
// onclick, HP decrement
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
// subscribe from notification model.
enemy.CurrentHp.SubscribeToText(MyText);
enemy.IsDead.Where(isDead => isDead == true)
    .Subscribe(_ =>
    {
        MyButton.interactable = false;
    });

你可以组合使用UnityEvent.AsObservable返回的ReactiveProperties、ReactuveCollections和Observables.所有的UI元素都可作为可观察对象(Observable).

泛型ReactiveProperties不能被序列化或者在Unity的Inspecatble中显示。但是UniRx为ReactivePropery提供了专门的子类,诸如 int/LongReactiveProperty,Float/DoubleReactiveProperty,StringReactiveProperty,BoolReactiveProperty 等等。(在这查看:(InspectableReactiveProperty.cs),这些属性都可以在Inspector中编辑,对于你自定义的Enum ReactiveProperty,编写一个自定义的inspectable ReactiveProperty[T]是很容易的。如果你需要为ReactiveProperty增加[Myltiline]或者[Range]之类的属性,你可以使用MultilineReactivePropertyAttribute和RangeReactivePropertyAttribute 替换Unity的Multiline和Range。

所提供的派生自InspecetableReactiveProperty显示在Inspector中,在值发生更改时发出通知,在Inspector中更改值时也会发出通知。

在这里插入图片描述

这个功能由 InspectorDisplayDrawer提供,通过继承你可以应用你自己自定义的ReactiveProperty:

public enum Fruit
{
    Apple, Grape
}

[Serializable]
public class FruitReactiveProperty : ReactiveProperty<Fruit>
{
    public FruitReactiveProperty()
    {
    }

    public FruitReactiveProperty(Fruit initialValue)
        :base(initialValue)
    {
    }
}

[UnityEditor.CustomPropertyDrawer(typeof(FruitReactiveProperty))]
[UnityEditor.CustomPropertyDrawer(typeof(YourSpecializedReactiveProperty2))] // and others...
public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer
{
}

如果ReactiveProperty仅在流中被更新,通过使用ReadOnlyReactiveProperty你可以将属性变为只读的。

public class Person
{
    public ReactiveProperty<string> GivenName { get; private set; }
    public ReactiveProperty<string> FamilyName { get; private set; }
    public ReadOnlyReactiveProperty<string> FullName { get; private set; }

    public Person(string givenName, string familyName)
    {
        GivenName = new ReactiveProperty<string>(givenName);
        FamilyName = new ReactiveProperty<string>(familyName);
        // If change the givenName or familyName, notify with fullName!
        FullName = GivenName.CombineLatest(FamilyName, (x, y) => x + " " + y).ToReadOnlyReactiveProperty();
    }
}

Model-View-(Reactive)Presenter Pattern

UniRx使得MVP(MVRP)模式的实现成为可能。
在这里插入图片描述

为什么我们需要使用MVP模式替换MVVM模式。Unity中没有提供UI绑定机制,创建绑定层太过于复杂且会影响性能。尽管如此,视图任然需要更新。Presenter持有视图的组件并能更新视图。虽然不是真正的绑定,但Observables 启用了对通知的订阅,它看起来更真实,这个模式被称为Reactive Presenter.

// Presenter for scene(canvas) root.
public class ReactivePresenter : MonoBehaviour
{
    // Presenter is aware of its View (binded in the inspector)
    public Button MyButton;
    public Toggle MyToggle;
    
    // State-Change-Events from Model by ReactiveProperty
    Enemy enemy = new Enemy(1000);

    void Start()
    {
        // Rx supplies user events from Views and Models in a reactive manner 
        MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
        MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);

        // Models notify Presenters via Rx, and Presenters update their views
        enemy.CurrentHp.SubscribeToText(MyText);
        enemy.IsDead.Where(isDead => isDead == true)
            .Subscribe(_ =>
            {
                MyToggle.interactable = MyButton.interactable = false;
            });
    }
}

// The Model. All property notify when their values change
public class Enemy
{
    public ReactiveProperty<long> CurrentHp { get; private set; }

    public ReactiveProperty<bool> IsDead { get; private set; }

    public Enemy(int initialHp)
    {
        // Declarative Property
        CurrentHp = new ReactiveProperty<long>(initialHp);
        IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
    }
}

在Unity的Hierarchy中,视图就是一个场景。视图在初始化时有UnityEngine和Presenters关联。xxxAsObservable方法使得创建事件信号变得简单,没有任何开销。SubscribeToText和SubscribeToInteractable是类似绑定的简单的工具。虽然简单,但是非常强大。他们很符合Unity的编程环境,并提供了高性能和简洁的体系结构。
在这里插入图片描述

V->RP->M->RP->V以响应式的方式完成的连接起来,UniRx提供了适配的方法和类。当然你也可以使用其它的MVVM(或者MV*) 框架代替,UniRx/ReactiveProperty仅仅是一个简单的工具集。

GUI编程也受益于ObservableTriggers.ObservableTriggers转化Unity事件为Observables(可观察对象),因此可以使用它们来组成MV®P模式。例如:ObservableEventTrigger 转化UGUI事件为Observable(可观察对象):

var eventTrigger = this.gameObject.AddComponent<ObservableEventTrigger>();
eventTrigger.OnBeginDragAsObservable()
    .SelectMany(_ => eventTrigger.OnDragAsObservable(), (start, current) => UniRx.Tuple.Create(start, current))
    .TakeUntil(eventTrigger.OnEndDragAsObservable())
    .RepeatUntilDestroy(this)
    .Subscribe(x => Debug.Log(x));

ReactiveCommand,AsyncReactiveCommand

ReactiveCommand作为可交互按钮命令的抽象。

public class Player
{		
   public ReactiveProperty<int> Hp;		
   public ReactiveCommand Resurrect;		
		
   public Player()
   {		
        Hp = new ReactiveProperty<int>(1000);		
        		
        // If dead, can not execute.		
        Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();		
        // Execute when clicked		
        Resurrect.Subscribe(_ =>		
        {		
             Hp.Value = 1000;		
        }); 		
    }		
}		
		
public class Presenter : MonoBehaviour		
{		
    public Button resurrectButton;		
		
    Player player;		
		
    void Start()
    {		
      player = new Player();		
		
      // If Hp <= 0, can't press button.		
      player.Resurrect.BindTo(resurrectButton);		
    }		
}

AsyncReactiveCommand 是ReactiveCommand的异步形式,将CanExecute(大多数情况下绑定到按钮的interactable)更改为false,直到异步操作执行完成。

public class Presenter:MonoBehaviour{
    public UnityEngine.UI.Button button;
    void Start(){
        var command=new AsyncReactiveCommand();

        command.Subscribe(_=>{
            return Observable.Timer(TimeSpan.FromSeconds(3)).asUnitObservable();
        });

        command.BindTo(button);

        button.BindToOnClick(_=>{
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
    }
}

AsyncReactiveCommand 有三个构造函数。

()CanExecute默认为false,直到异步执行完成
(IObservable canExecuteSource) 当canExecuteSource发送true并且不在执行时,与empty混合,CanExecute变为true.
(IReactiveProperty sharedCanExecute) 在多个AsyncReactiveCommands之间共享执行状态,如果其中一个AsyncReactiveCommand正在执行,其它AsyncReactiveCommands(拥有一个sharedCanExecute属性)的CanExecute变为false,直到这个AsyncCommand异步执行完成.

public class Presenter : MonoBehaviour
{
    public UnityEngine.UI.Button button1;
    public UnityEngine.UI.Button button2;

    void Start()
    {
        // share canExecute status.
        // when clicked button1, button1 and button2 was disabled for 3 seconds.

        var sharedCanExecute = new ReactiveProperty<bool>();

        button1.BindToOnClick(sharedCanExecute, _ =>
        {
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });

        button2.BindToOnClick(sharedCanExecute, _ =>
        {
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
    }
}

MessageBroker, AsyncMessageBroker

MessageBroker基于Rx的内存的pubsub系统的按类型过滤的。

public class TestArgs
{
    public int Value { get; set; }
}

---

// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));

// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });

AsyncMessageBroker是MessageBroker的异步形式,可以await发布调用

AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
    // show after 3 seconds.
    return Observable.Timer(TimeSpan.FromSeconds(3))
        .ForEachAsync(_ =>
        {
            UnityEngine.Debug.Log(x);
        });
});

AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
    .Subscribe(_ =>
    {
        UnityEngine.Debug.Log("called all subscriber completed");
    });

UniRx.ToolKit

UniRx.ToolKit 中包含一些Rx-ish工具。当前版本中包含 ObjectPool(对象池)和AsyncObjectPool(异步对象池),这个池子在租赁前可以租、回收和异步预加载。

// sample class
public class Foobar : MonoBehaviour
{
    public IObservable<Unit> ActionAsync()
    {
        // heavy, heavy, action...
        return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
    }
}

public class FoobarPool : ObjectPool<Foobar>
{
    readonly Foobar prefab;
    readonly Transform hierarchyParent;

    public FoobarPool(Foobar prefab, Transform hierarchyParent)
    {
        this.prefab = prefab;
        this.hierarchyParent = hierarchyParent;
    }

    protected override Foobar CreateInstance()
    {
        var foobar = GameObject.Instantiate<Foobar>(prefab);
        foobar.transform.SetParent(hierarchyParent);

        return foobar;
    }

    // You can overload OnBeforeRent, OnBeforeReturn, OnClear for customize action.
    // In default, OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false)

    // protected override void OnBeforeRent(Foobar instance)
    // protected override void OnBeforeReturn(Foobar instance)
    // protected override void OnClear(Foobar instance)
}

public class Presenter : MonoBehaviour
{
    FoobarPool pool = null;

    public Foobar prefab;
    public Button rentButton;

    void Start()
    {
        pool = new FoobarPool(prefab, this.transform);

        rentButton.OnClickAsObservable().Subscribe(_ =>
        {
            var foobar = pool.Rent();
            foobar.ActionAsync().Subscribe(__ =>
            {
                // if action completed, return to pool
                pool.Return(foobar);
            });
        });
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/195554.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

路由器 内核开发 流程

宽 带上网已经不是什么新鲜事情&#xff0c;人们对相关的网络器件已经不再陌生&#xff0c;比如说常见的路由器。对于一般的网络用户&#xff0c;他们能知道怎样使用路由器来上网、玩游戏等就 已经感到很满足了&#xff0c;通常情况下对路由器的深层技术很少去过问研究&#xf…

Matlab和PCL中的点云滤波

而在PCL中总结了几种需要进行点云滤波处理的情况&#xff0c;这几种情况分别是&#xff1a; (1)点云数据密度不规则需要平滑。 (2)因为遮挡等问题造成离群点需要去除。 (3)大量数据需要进行“下采样”(Downsample)。 (4)噪声数据需要去除。 对应的解决方法是&#xff1a; (1)按…

什么是ITIL中的变更管理

商业环境和客户期望在不断变化&#xff0c;数字化转型已成为各行各业企业成功的关键因素。数字化转型的关键在于利用可用 应对业务挑战和抓住机遇的技术。当你分解它时&#xff0c;数字化转型基本上是信息技术管理更好地消除有问题的领域&#xff0c;并使您的 IT 基础架构能够应…

WebView缓存机制

一 前言 由于H5具备 开发周期短、灵活性好 的特点&#xff0c;所以现在 Android App大多嵌入了 Android Webview 组件进行 Hybrid 开发 但我知道你一定在烦恼 Android Webview 的性能问题&#xff0c;特别突出的是&#xff1a;加载速度慢 & 消耗流量 今天&#xff0c;我将…

Django 模型的继承

Django 模型的继承项目管理模型关联关系模块的包管理模型的继承项目管理 到目前为止&#xff0c;都是属于httprunner中的用例部分&#xff0c;针对核心功能进行的开发工作&#xff0c;要把平台做成一个用户可以使用的程度还需要些额外的功能&#xff0c;比如项目管理&#xff…

13薪| 8k-14k Java开发工程师

"众推职聘”以交付结果为宗旨的全流程化招聘服务平台&#xff01;今日招聘信息↓工作内容1.参与软件项目和产品概要设计&#xff0c;负责详细功能设计、编码实现及相关文档编写&#xff1b;2.根据模块设计完成相应的模块编码及单元测试&#xff1b;3.对用户行为、需求及反…

zeek集群简述

Zeek不是多线程的&#xff0c;因此&#xff0c;一旦达到单处理器内核的限制&#xff0c;当前唯一的选择就是将工作负载分散到多个内核&#xff0c;甚至多个物理计算机上。Zeek的集群部署场景是构建这些大型系统的当前解决方案。Zeek附带的工具和脚本提供了一种结构&#xff0c;…

自注意力和位置编码(比较卷积神经网络、循环神经网络和自注意力)

在自注意力中&#xff0c;查询、键和值都来自同一组输入。 卷积神经网络和自注意力都拥有并行计算的优势&#xff0c;而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方&#xff0c;所以在很长的序列中计算会非常慢。 为了使用序列的顺序信息&…

Nostr with NIP-05 Verification Guide

What is a NIPNIPs (Nostr Implementation Possibilities) document what MUST, what SHOULD and what MAY be implemented by Nostr-compatible relay and client software. See a complete list of nips here.NIP-05 提案是针对用户 Nostr 帐户的验证方法&#xff0c;可以将其…

The update is not applicable to your computer

在安装windows CVE-2022-44698补丁的时候出现了报错&#xff0c;"Windows Update Standalone Installer The update is not applicable to your computer" 1.找到漏洞对应的官方文章 CVE-2022-44698 - Security Update Guide - Microsoft - Windows SmartScreen S…

学术科研无从下手?27 条机器学习避坑指南,让你的论文发表少走弯路

内容一览&#xff1a;如果你刚接触机器学习不久&#xff0c;并且未来希望在该领域开展学术研究&#xff0c;那么这份为你量身打造的「避坑指南」可千万不要错过了。 关键词&#xff1a;机器学习 科研规范 学术研究 机器学习学术小白&#xff0c;如何优雅避坑坑、让自己的论文顺…

力扣sql简单篇练习(九)

力扣sql简单篇练习(九) 1 合作过至少三次的演员和导演 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT actor_id,director_id FROM ActorDirector GROUP BY actor_id,director_id HAVING count(timestamp)>31.3 运行截图 2 患某种疾病的患…

结构体内存对齐;内存优化

结构体需要根据数据类型进行内存对齐。 所有数据类型占据空间的大小&#xff0c;一定是它基础类型的倍数。 首先按照最大的数据类型格式来作为最小分割单元。 最大整数倍 struct stu {char gender;unsigned int age; }student1;int main() {printf("sizeof this struct i…

SpringCloud Config分布式配置中心

目录 一、概述 二、Config服务端配置与测试 配置读取规则 三、Config客户端配置与测试 bootstrasp.yml 四、Config客户端之动态刷新 一、概述 官网&#xff1a;Spring Cloud Config 微服务意味着要将单体应用中的业务拆分成一个个子服务&#xff0c;每个服务的粒度相对…

【Java】IDEA调试线上服务

目录IEDA打开线上服务对应的代码&#xff0c;Edit Configuration创建与远程服务的连接复制黄匡生成的参数&#xff0c;添加到服务器启动命令中服务器的服务重新启动&#xff0c;并注意调试端口5005是否被防火墙拦截本地IDEA启动&#xff0c;控制台打印如图即成功代码上加断点&a…

Linux做选择题时的要点

1.线程独有&#xff1a;栈&#xff0c;寄存器&#xff0c;信号屏蔽字&#xff0c;errno...等信息&#xff0c;因此各个线程各自有各自的栈区&#xff0c;但是堆区共用。 2.用户态线程的切换在用户态实现&#xff0c;不需要内核支持。 3.每个线程在进程虚拟地址空间中会分配拥…

部署在Docker中的iServer进行服务迁移

目录前言一&#xff1a;备份与恢复1.备份2.恢复二&#xff1a;迁移配置文件作者&#xff1a;kxj 前言 Linux 容器虚拟技术&#xff08;LXC&#xff0c;Linux Container&#xff09;是一种轻量级的虚拟化手段&#xff0c;它利用内核虚拟化技术提供轻量级的虚拟化&#xff0c;来…

【八大数据排序法】希尔排序法的图形理解和案例实现 | C++

第十七章 希尔排序法 目录 第十七章 希尔排序法 ●前言 ●认识排序 ●一、希尔排序法是什么&#xff1f; 1.简要介绍 2.图形理解 3.算法分析 ●二、案例实现 1.案例一 ●总结 前言 排序算法是我们在程序设计中经常见到和使用的一种算法&#xff0c;它主要是将一…

C++引用(特性+使用场景+常引用)

文章目录1. 概念2. 关于别名的理解3. 引用的特性1.引用必须在定义时初始化2.一个变量可以有多个别名3.引用一旦引用一个实体&#xff0c;再不能引用其他实体4.使用场景1. 引用做参数2. 引用做返回值1. 传值返回是否为n直接返回临时变量作为返回值2. 传引用返回编译器傻瓜式判断…

11、循环语句

目录 一、while语句 二、do…while语句 三、for语句 一、while语句 使用while语句可以执行循环结构&#xff0c;其一般形式如下&#xff1a; while语句的执行流程图&#xff1a; while语句首先检验一个条件&#xff0c;也就是括号中的表达式。当条件为真时&#xff0c;就执…