UniRx - Unity Responsive Programming Plugin

This article was first published on the official account of "Hongliu School".
Hongliu Academy will make you a few steps faster! Hello, I am Zheng Hongzhi, your technology pathfinder, you can call me Dazhi (vx: zhz11235).

Translator of this article: Zheng Hongzhi - Your technical pathfinder Translation date
February 1, 2018

UniRx - Unity Responsive Programming Plugin


Plug-in author Yoshifumi Kawai (neuecc)

grid

What is UniRx?

UniRx (Unity Responsive Programming Plugin) rewrites the Responsive Extension of .Net. .Net's official Rx is great, but it doesn't work in Unity and has compatibility issues with IL2CPP for IOS. This library solves these problems and adds some Unity-specific tool classes. Supported platforms are: PC/Mac/Android/iOS/WP8/WindowsStore/etc., and support all versions after Unity4.6.

Address of UniRx in Unity Asset Store (free) - http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT

Lecture PPT - http://www.slideshare.net/neuecc/unirx-reactive-extensions-for-unityen

Update Blog - https://medium.com/@neuecc

Support in the Unity Forum - http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity

ChangelogUniRx /releases

UniRx 包含 Core Library (Port of Rx) + Platform Adaptor (MainThreadScheduler/FromCoroutine/etc) + Framework (ObservableTriggers/ReactiveProeperty/etc)

Why use Rx?

In general, network operations require the use of WWWand Coroutine. But using is not a good choice Coroutinefor asynchronous operations for the following reasons:

  1. The coroutine cannot have a return value, because its return type must be IEnumerator
  2. The coroutine cannot handle exceptions, because the yield return statement cannot be tried-catch

It will cause a large area of ​​strong coupling in the code.

Rx is here to solve asynchronous problems. Rx can make asynchronous operations more elegant, use event-driven programming, and use LINQ operations.

Game loops (every Update, OnCollisionEnter, etc.), sensor data (Kinect, Leap Motion, VR Input, etc.) are events. Rx converts events into responsive sequences, which can be easily combined through LINQ operations, and also supports time operations.

Unity is usually single threaded, but UniRx makes multithreading easier.

UniRx can simplify uGUI programming, and all UI events (clicked, valuechanged, etc) can be converted into UniRx event flow.

Introduction

Great article introducing Rx: The introduction to Reactive Programming you've been missing .

The following code implements double-click detection:

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

This example shows the following features in just 5 lines of code:

  • Turn the game loop as (Update) into a stream of events
  • combined event stream
  • Merge own event stream
  • Time-based operations are very simple

network operation

Use ObservableWWW for asynchronous network operations. Its Get/Post methods return Subscribeable IObservables:

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

Rx can be combined and canceled. You can also query via LINQ expressions:

// composing asynchronous sequence with LINQ query expressions
var query = from google in ObservableWWW.Get("http://google.com/")
            from bing in ObservableWWW.Get("http://bing.com/")
            from unknown in ObservableWWW.Get(google + bing)
            select new {
    
     google, bing, unknown };

var cancel = query.Subscribe(x => Debug.Log(x));

// Call Dispose is cancel.
cancel.Dispose();

Parallel requests use Observable.WhenAll:

// Observable.WhenAll is for parallel asynchronous operation
// (It's like Observable.Zip but specialized for single async operations like Task.WhenAll)
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
});

Progress information can also be obtained:

// notifier for progress use ScheudledNotifier or new Progress<float>(/* action */)
var progressNotifier = new ScheduledNotifier<float>();
progressNotifier.Subscribe(x => Debug.Log(x)); // write www.progress

// pass notifier to WWW.Get/Post
ObservableWWW.Get("http://google.com/", progress: progressNotifier).Subscribe();

Error handling:

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

Working with IEnumerators (coroutines)

IEnumerator (coroutine) is Unity's main asynchronous tool. UniRx integrates coroutines and IObservables. You can write asynchronous code using coroutines, and then use UniRx to organize them. This is the best way to control asynchronous flow.

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

In Unity 5.3 or newer, you can use ToYieldInstruction to convert Observable to 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();
}

Usually, a coroutine that wants to return a value needs to use a callback callback. Observable.FromCoroutine can convert a coroutine into a cancelable IObservable[T].

// public method
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!
    }
}

Here are some examples. Next is a multi-OnNext pattern.

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

for multithreading

// 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];
    }); 

default scheduler

Time-based operations in UniRx (Interval, Timer, Buffer(timeSpan), etc) use Scheduler.MainThreadas their default scheduler. This means that most operators (except Observable.Start) work on a single thread, so ObserverOn is not necessary and does not need to deal with thread safety. This is not the same as standard RxNet, but more in line with the Unity environment.

Scheduler.MainThreadWill be affected by Time.timeScale. If you want to ignore the time scale, use Scheduler.MainThreadIgnoreTimeScale.

Triggers for MonoBehaviour

Use UniRx.Triggerscan handle Monobehaviour's events:

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

List of supported triggers UniRx.wiki#UniRx.Triggers .

Controlling these triggers can be made simpler by subscribing to extension methods of the Component/GameObject. These methods will automatically add ObservableTrigger to the object (except ObservableEventTriggerand ObservableStateMachineTrigger):

using UniRx;
using UniRx.Triggers; // need UniRx.Triggers namespace for extend gameObejct

public class DragAndDropOnce : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        // All events can subscribe by ***AsObservable
        this.OnMouseDownAsObservable()
            .SelectMany(_ => this.UpdateAsObservable())
            .TakeUntil(this.OnMouseUpAsObservable())
            .Select(_ => Input.mousePosition)
            .Subscribe(x => Debug.Log(x));
    }
}

Previously UniRx provided ObservableMonoBehaviour. This is an obsolete interface that is no longer supported. Please use UniRx.Triggers instead.

Create custom triggers

The best way to handle Unity's events is to convert them to Observable. If the triggers that come with UniRx are not enough, you can also create your own triggers. The following is a UGUI long press trigger:

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

Using it is as easy as bringing your own triggers:

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

trigger.OnLongPointerDownAsObservable().Subscribe();

Observable lifecycle management

When will OnCompleted be called? Subscription lifecycle management is very important when using UniRx. ObservableTriggersCalled when the attached object is destroyed. Other static methods ( Observable.Timer, Observable.EveryUpdate, etc…) will not stop automatically and need to be managed manually.

Rx provides some methods, such as which IDisposable.AddToallow you to release multiple subscriptions at once:

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

If you want to automatically release when the object is destroyed, you can use AddTo(GameObject/Component):

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

The AddTo method can bring about autorelease. If you need some special OnCompleted handling, use TakeWhile, TakeUntil, TakeUntilDestroyor TakeUntilDisable:

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

RepeatAn important but dangerous approach when handling events . It may cause an infinite loop, so be careful:

using UniRx;
using UniRx.Triggers;

public class DangerousDragAndDrop : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        this.gameObject.OnMouseDownAsObservable()
            .SelectMany(_ => this.gameObject.UpdateAsObservable())
            .TakeUntil(this.gameObject.OnMouseUpAsObservable())
            .Select(_ => Input.mousePosition)
            .Repeat() // dangerous!!! Repeat cause infinite repeat subscribe at GameObject was destroyed.(If in UnityEditor, Editor is freezed)
            .Subscribe(x => Debug.Log(x));
    }
}

UniRx additionally provides a safe Repeat method. RepeatSafe: If "OnComplete" is called continuously, Repeat will stop automatically. RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component)you can stop Repeat when the gameobject is destroyed:

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

UniRx guarantees that dynamic observables (FromEvent/Subject/ReactiveProperty/UnityUI.AsObservable…, there are like event) can continue to work when there are unhandled exceptions. What does it mean? If you subscribe within Subscribe, the exception of subscribe inside will not cause the failure of external 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);
    });
});

This is useful when handling user events.

Instances of all classes provide a ObserveEveryValueChangedmethod that listens for value changes each frame:

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

This is very useful. If the monitored target is a GameObject, it will stop and call OnCompleted when the target is destroyed. If the monitoring target is a C# object, OnCompleted will be called during GC.

Convert Unity callbacks to IObservable

Use Subject (or AsyncSubject for asynchronous operations):

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

Removed in Unity5 Application.RegisterLogCallbackand Application.logMessageReceivedreplaced . So it can be used more simply 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 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"));

Debug Debugging

UniRx.DiagnosticsThe Debugoperator can help with debugging.

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

OnNextThe calls of , OnError, OnCompleted, OnCancel, will be displayed in order of events OnSubscribeand printed out through Debug.Log. Only takes effect #if DEBUGat .

Unity-specific Extra Gems (Cool extra features for Unity)

Translator's Note: SubscribeOnMainThread() has no effect after testing and has a bug (January 30, 2018, UniRx version 5.5)

// 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 time operators (time operations based on the number of frames)

UniRx provides some time operations based on frame number:

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

For example, a delayed call:

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

The execution order of the Every* methods is

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

EveryGameObjectUpdate is called in the same frame, called from MainThreadDispatcher.Update (the author recommends that the MainThreadDispatcher script be executed before other scripts (ScriptExecutionOrder is set to -32000)
EveryLateUpdate, EveryEndOfFrame is called in the same frame.
Then call EveryGameObjectUpdate in the next frame, and so on

MicroCoroutine (micro coroutine)

MicroCoroutines are more memory efficient and faster. This is based on Unity's blog "10000 UPDATE() CALLS" (http://blogs.unity3d.com/2015/12/23/1k-update-calls/), which can be dozens of times faster. MicroCoroutine is automatically used for frame-based time manipulation and ObserveEveryValueChanged.

If you want to use MicroCoroutine instead of unity's built-in coroutine, use MainThreadDispatcher.StartUpdateMicroCoroutineor 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());
    }
}

image

The limitation of MicroCoroutine is: it only supports yield return nulland the calling time is determined according to the calling method ( StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine).

If used with other IObservables, you can check for done properties such as 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 processing UnityEventis simple. Use UnityEvent.AsObservablesubscribe events.

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

Turn events into Observables so you can program with declarative 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());
}

For more reactive UI programming, see the examples in the project Sample12, Sample13 and the ReactiveProperty section below.

ReactiveProperty, ReactiveCollection

Other classes usually need to be notified when game data changes. Should we use properties and events (callbacks)? This is often too cumbersome. UniRx provides the ReactiveProperty class, a lightweight property proxy.

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

You can combine ReactiveProperties, ReactiveCollections and observables with UnityEvent.AsObservable. All UI components are observable.

Generally speaking, ReactiveProperties are not serializable or cannot be seen in the Inspector panel of the Unity editor, but UniRx provides special subclasses to implement this function. Including Int/LongReactiveProperty, Float/DoubleReactiveProperty, StringReactiveProperty, BoolReactiveProperty, and more see: InspectableReactiveProperty.cs(https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/InspectableReactiveProperty.cs) ). Both can be edited in the Inspector. For custom enumerated ReactiveProperty, it is also easy to write an inspectable ReactiveProperty[T].

If you need [Multiline]or [Range]add to ReactiveProperty, you can replaceMultilineReactivePropertyAttribute and with and .RangeReactivePropertyAttributeMultilineRange

These InpsectableReactiveProperties can be displayed in the inspector panel and be notified when their values ​​change, even in the editor.

This functionality is implemented at InspectorDisplayDrawer(https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/InspectorDisplayDrawer.cs). You can implement the drawing of your custom ReactiveProperties in the inspector panel by inheriting this class:

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
{
    
    
}

If the value of the ReactiveProperty is only updated in the stream, you can use ReadOnlyReactivePropertyto make the property read-only.

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

MVP Design Pattern Model-View-(Reactive) Presenter Pattern

The MVP (MVRP) design pattern can be implemented with UniRx.

Why should you use the MVP pattern instead of the MVVM pattern? Unity does not provide a UI binding mechanism, creating a binding layer is too complicated and will affect performance. Still, the view needs to be updated. The Presenters layer knows about the view's components and can update them. Although there is no real binding, Observables can notify subscribers, and the function is similar. This pattern is called 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();
    }
}

The view layer is a scene scene, which is defined by Unity's hierachy. The presentation layer binds the view layer when Unity initializes. The XxxAsObservable methods make it easy to create event signals without any overhead. SubscribeToText and SubscribeToInteractable are both concise binding-like helper functions. Although these tools are simple, they are very useful. It's smooth to use in Unity, performs well, and keeps your code cleaner.

V -> RP -> M -> RP -> V is fully connected in a reactive way. UniRx provides all adaptation methods and classes, but other MVVM (or MV*) frameworks can also be used. UniRx/ReactiveProperty is just a simple toolkit.

GUI programming can also benefit a lot from ObservableTriggers. ObservableTriggers turn Unity events into Observables, so the MV®P pattern can be composed with them. For example, ObservableEventTriggerconvert uGUI events to 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));

(Obsolete) PresenterBase

Remarks:
PresenterBase is useful, but too complicated.
You can only use Initializethe method , and calling the parent class in the subclass is enough for most situations.
So no longer recommended PresenterBase.

ReactiveCommand, AsyncReactiveCommand

ReactiveCommand abstracts the interactable property of buttons.

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 is a variant of ReactiveCommand whose CanExecute(usually bound to a button's interactable) becomes false after the asynchronous operation completes.

public class Presenter : MonoBehaviour		
{
    
    		
    public UnityEngine.UI.Button button;		
		
    void Start()
    {
    
    		
        var command = new AsyncReactiveCommand();		
		
        command.Subscribe(_ =>		
        {
    
    		
            // heavy, heavy, heavy method....		
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();		
        });		
		
        // after clicked, button shows disable for 3 seconds		
        command.BindTo(button);		
		
        // Note:shortcut extension, bind aync onclick directly		
        button.BindToOnClick(_ =>		
        {
    
    		
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();		
        });		
    }		
}		

AsyncReactiveCommandThere are three constructors.

  • () - CanExecute is changed to false until async execution finished
  • (IObservable<bool> canExecuteSource)- CanExecute becomes true when canExecuteSource becomes true and is not executing.
  • (IReactiveProperty<bool> sharedCanExecute)- The running state is shared between multiple AsyncReactiveCommands. If one AsyncReactiveCommand is executing, the CanExecute of other AsyncReactiveCommands (with the same sharedCanExecute property) becomes false until the asynchronous operation is completed.
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 (message broker, asynchronous message broker)

MessageBroker is an Rx-based in-memory publish-subscribe (pubsub) system based on type filtering.

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 is a variant of MessageBroker that can handle asynchronous Publish calls.

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.ToolkitContains some Rx-style tools. Currently reporting an error ObjectPooland AsyncObjectPool. Yes Rent, Returnand PreloadAsync(preload object pool before rent operation).

// 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 Profiler

Users of Visual Studio 2015 can use an analyzer UniRxAnalyzer. It can detect streams that are not subscribed.

ObservableWWWIt won't be executed until subscribed, so the analyzer will issue a warning. Analyzers can be downloaded from NuGet.

  • Install-Package UniRxAnalyzer(http://www.nuget.org/packages/UniRxAnalyzer)

Please submit new ideas for the analyzer on GitHub Issues!

example

See UniRx/Examples(https://github.com/neuecc/UniRx/tree/master/Assets/Plugins/UniRx/Examples)

Sample09_EventHandling shows how to do resource management, including MainThreadDispatcher and some other things.

Windows Store/Phone App (NETFX_CORE)

Some interfaces, such as UniRx.IObservable<T>and System.IObservable<T>will cause conflicts when used in Windows Store App.
Therefore, if using NETFX_CORE, avoid using UniRx.IObservable<T>similar constructors, and avoid using UniRx components without namespaces. This resolves conflict issues.

async/await Support

Based on "Upgraded Mono/.Net in Editor on 5.5.0b4" (https://forum.unity3d.com/threads/upgraded-mono-net-in-editor-on-5-5-0b4.433541/), Unity .NET 4.6 and C# 6 are supported. UniRx provides UniRxSynchronizationContextto return multi-threaded tasks to the main thread.

async Task UniRxSynchronizationContextSolves()
{
    
    
    Debug.Log("start delay");

    // UniRxSynchronizationContext is automatically used.
    await Task.Delay(TimeSpan.FromMilliseconds(300));

    Debug.Log("from another thread, but you can touch transform position.");
    Debug.Log(this.transform.position);
}

UniRx also directly supports await Coroutines.

async Task CoroutineBridge()
{
    
    
    Debug.Log("start www await");

    var www = await new WWW("https://unity3d.com");

    Debug.Log(www.text);
}

Of course, IObservable can be awaited.

async Task AwaitObservable()
{
    
    
    Debug.Log("start await observable");

    await Observable.NextFrame();   // like yield return null
    await Observable.TimerFrame(5); // await 5 frame

    Debug.Log("end await observable");
}

DLL segmentation

If you want to precompile UniRx, you can compile the dll yourself. Clone this project, open it UniRx.sln, and you can see UniRxthat it is a completely independent project. You should define some macros like UNITY;UNITY_5_4_OR_NEWER;UNITY_5_4_0;UNITY_5_4;UNITY_5;+ UNITY_EDITOR, UNITY_IPHONEor these other platform macros. We can't provide pre-compiled dll, because the macros of different versions of Unity are different.

If you want to use UniRx for .NET 3.5 CLR applications, you can UniRx.Library. UniRx.LibraryDoes not depend on UnityEngine, compilation UniRx.Libraryneeds to define UniRxLibrarymacros. Additional precompiled UniRx.Librarylibraries are available on NuGet.

Install-Package UniRx(https://www.nuget.org/packages/UniRx)

other references

  • UniRx/wiki(https://github.com/neuecc/UniRx/wiki)

UniRx API documentation.

The home of ReactiveX. Introduction, All operators are illustrated with graphical marble diagrams, there makes easy to understand. And UniRx is official ReactiveX Languages.

A great online tutorial and eBook.

Many videos, slides and documents for Rx.NET.

Intro slide by @torisoup

Intro slide and sample game by @Xerios

How to integrate with PlayFab API

What game or library is using UniRx?

Games

Libraries

  • PhotonWire - Typed Asynchronous RPC Layer for Photon Server + Unity.
  • uFrame Game Framework - MVVM/MV* framework designed for the Unity Engine.
  • EcsRx - A simple framework for unity using the ECS paradigm but with unirx for fully reactive systems.
  • ActionStreetMap Demo - ASM is an engine for building real city environment dynamically using OSM data.
  • utymap - UtyMap is library for building real city environment dynamically using various data sources (mostly, OpenStreetMap and Natural Earth).
  • Submarine - A mobile game that is made with Unity3D and RoR, WebSocket server written in Go.

If you use UniRx, please comment to UniRx/issues/152.

Help & Contribute

Support thread on the Unity forum. Ask me any question - http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity

We welcome any contributions, be they bug reports, requests or pull request.
Please consult and submit your reports or requests on GitHub issues.
Source code is available in Assets/Plugins/UniRx/Scripts.
This project is using Visual Studio with UnityVS.

Author’s other Unity + LINQ Assets

LINQ to GameObject is a group of GameObject extensions for Unity that allows traversing the hierarchy and appending GameObject to it like LINQ to XML. It’s free and opensource on GitHub.

Author Info

Yoshifumi Kawai(a.k.a. neuecc) is a software developer in Japan.
He is the Director/CTO at Grani, Inc.
Grani is a top social game developer in Japan.
He is awarding Microsoft MVP for Visual C# since 2011.
He is known as the creator of linq.js(LINQ to Objects for JavaScript)

Blog: https://medium.com/@neuecc (English)
Blog: http://neue.cc/ (Japanese)
Twitter: https://twitter.com/neuecc (Japanese)

License

This library is under the MIT License.

Some code is borrowed from Rx.NET and mono/mcs.

Guess you like

Origin blog.csdn.net/zhenghongzhi6/article/details/79229585