UniRx - 뭉태이의 까칠한 생활 - moongtaeng
E D R , A S I H C RSS

UniRx

UniRx - Reactive Extensions for Unity


https://github.com/neuecc/UniRx
Created by Yoshifumi Kawai(neuecc)

중요 : 번역은 구글의 힘을 사용하였습니다.

What is UniRx?

UniRx (Reactive Extensions for Unity) 는 .NET Reactive Extensions 을 재구현한것입니다. 공식 Rx는 훌륭하지만, Unity에서 동작하지 않으며 iOS IL2CPP 호환성 문제가 있습니다. 이 라이브러리는 이러한 문제를 수정하고 Unity에 대한 특정 유틸리티를 추가합니다. 지원되는 플랫폼은 PC/Mac/Android/iOS/WP8/WindowsStore/etc 이고 라이브러리는 Unity 5와 4.6에서 완벽하게 지원됩니다.

UniRx는 Unity 에셋 스토어에서 사용할 수 있습니다. (무료) - UniRx - Reactive Extensions for Unity


업데이트를 위한 블로그 입니다. - https://medium.com/@neuecc


릴리즈 노트는, UniRx/releases 을 봐주세요.

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


Why Rx?

보통, Unity에서 Network 작업에는 WWWCoroutine 이 필요합니다. 즉, Coroutine을 사용하는 것은 다음과 같은 이유 때문에 비동기 작업에 적합하지 않습니다.
  1. Coroutine은 반환 형식이 IEnumerator 여야 하므로 값을 반환할 수 없습니다.
  2. Coroutine은 예외 처리를 할 수 없습니다. yield return 문은 try-catch 구조로 둘러 쌀 수 없기 때문입니다.

이러한 종류의 구성 가능성 부족으로 인해 작업이 밀접하게 결합되어 거대한 monolithic IEnumerator가 됩니다.

Rx는 그런 종류의 "asychronous blues" 를 고칩니다. Rx는 observable 컬렉션과 LINQ 스타일 쿼리 연산자를 사용하여 비동기 및 이벤티기반 프로그램을 작성하기 위한 라이브러리 입니다.

game loop(모든 Update, OnCollisionEnter, 등), sensor data (Kinect, Leap Motion, VR Input, 등)는 모든 유형의 이벤트 입니다. Rx는 LINQ 쿼리 연산자를 사용하여 support time-base 작업을 지원하는 reactive sequences로 이벤트를 대신합니다.

Unity는 일반적으로 단일 스레드이지만, UniRx는 join, cancels, accessing GameObject 등을 위해 멀티 스레딩을 가능하게 합니다.

UniRx는 uGUI로 UI 프로그래밍을 도와줍니다. 모든 UI 이벤트(clicked, value changed, etc)는 UniRx event stream 으로 변환 될 수 있습니다.


Introduction

훌륭한 Rx 기사를 소개합니다 : The introduction to Reactive Programming you've been missing.

다음 코드는 UniRx의 기사에서 double click 감지를 구현한것입니다.

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

이 예제는 다음과 같은 기능을 보여줍니다(오직 다섯줄로!):
  • event stream 으로 game loop (Update)
  • 작성가능한 event stream
  • 병합하는 self stream
  • time base 작업의 쉬운 처리

Network operations

비동기 네트워크 작업에는 ObservableWWW를 사용하십시오. Get/Post 함수는 구독가능한 IObservables를 반환합니다.
ObservableWWW.Get("http://google.co.jp/")
    .Subscribe(
        x => Debug.Log(x.Substring(0, 100)), // onSuccess
        ex => Debug.LogException(ex)); // onError

Rx는 작성가능하고 취소가능 합니다. LINQ 표현식으로 쿼리할 수도 있습니다.
// 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();

병렬 request에는 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
});

진행 정보를 사용할 수 있습니다.:
// 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();

에러 핸들링:
// 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();


Using with IEnumerators (Coroutines)

IEnumerator(Coroutine)는 Unity의 기본 비동기 툴입니다. UniRx는 coroutines과 IObservables을 통합시킵니다. coroutines에서 비동기 코드를 작성하고, 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();

Unity 5.3인경우, Observable에서 Coroutine 으로 ToYieldInstruction을 사용할 수 있습니다.
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();
} 

일반적으로 coroutine이 값을 반환해야 할 때 callback 을 사용해야합니다. Observable.FromCoroutine 은 coroutine을 취소 가능한 IObservableT로 변환 할 수 있습니다.
// 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!
    }
} 

여기에 몇 가지 예가 있습니다. 다음은 multiple 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();


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

DefaultScheduler

UniRx의 기본 시간 기반 작업(Interval, Timer, Buffer(timeSpan), etc)은 Scheduler.MainThread 를 스케줄러로 사용합니다. 이는 대부분의 연산자가 (Observable.Start를 제외한) 단일 스레드에서 동작하므로, ObserverOn 이 필요하지 않으며 스레드 안전 조치를 무시할 수 있습니다. 이는 표준 RxNet구현과 다르지만 Unity 환경에 더 적합합니다.

Scheduler.MainThread는 Time.timeScale 의 영향하에 실행됩니다. time scale을 무시하려면 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);
    }
}

지원되는 트리거는 UniRx.wiki#UniRx.Triggers에 나열되어 있습니다.

이런 것들은 Component/GameObject의 확장 메서드에 의해 리턴된 observables에 직접 등록함으로써 보다 쉽게 처리될 수 있습니다. 이러한 메서드는 ObservableTrigger를 자동으로 삽입합니다. (ObservableEventTrigger, 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));
    }
}

UniRx의 이전 버전은 ObservableMonoBehaviour을 제공했습니다. 더 이상 지원되지 않는 레거시 인터페이스입니다. UniRx.Triggers를 대신 사용하십시오.

Creating custom triggers

Observable로 변환하는것은 Unity 이벤트를 처리하는 가장 좋은 방법입니다. UniRx에서 제공하는 표준 트리거가 충분하지 않다면, custom 트리거를 생성할 수 있습니다. 여기에 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();
        }
    }
}

표준 트리거만큼 쉽게 사용할 수 있습니다.
var trigger = button.AddComponent<ObservableLongPointerDownTrigger>();

trigger.OnLongPointerDownAsObservable().Subscribe();

Observable Lifecycle Management

OnCompleted 는 언제 불려지나요? UniRx를 사용할때 subscription 라이프 사이클 관리는 매우 중요하게 고려해야한다. ObservableTriggers 는 연결된 GameObject가 destroy 될 때 OnCompleted를 호출합니다. 다른 static generator methods (Observable.Timer, Observable.EveryUpdate, 등...)은 자동으로 중지되지 않고, subscription 을 수동으로 관리해야합니다.

Rx는 IDisposable.AddTo 처럼 한번에 여러 subscription을 dispose 하는 몇가지 helper method를 제공합니다.
// 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가 destory 되었을때 자동으로 Dispose 하려면, AddTo(GameObject/Component)를 사용하십시오.
void Start()
{
    Observable.IntervalFrame(30).Subscribe(x => Debug.Log(x)).AddTo(this);
}

AddTo call은 자동 Dispose 을 용이하게합니다. 특별한 OnCompleted 처리가 필요하다면 대신에 TakeWhile, TakeUntil, TakeUntilDestory 그리고 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())
            .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는 추가적인 안전한 Repeat 메서드를 제공합니다. RepeatSafe : 연속적인 "OnComplete"가 반복 중지로 불리는 경우. RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component)} 는 target GameObject 가 destroy 되었을때 멈추게 합니다.
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 hot observable(FromEvent/Subject/ReactiveProperty/UnityUI.AsObservable..., there are like event) have unhandled exception durability. What is it? If subscribe in subscribe, does not detach event.
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인 경우, 대상이 destroy되면 관찰을 중지하고, OnComplete를 호출합니다. 감시 대상이 일반 C# 개체인경우, OnComplete가 GC에서 호출됩니다.

Converting Unity callbacks to 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.RegisterLogCallbackApplication.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());
}

shows sequence element on OnNext, OnError, OnCompleted, OnCancel, OnSubscribe timing to Debug.Log. It enables only #if DEBUG.
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 time operators

UniRx는 몇가지 프레임 기반 시간 명령어를 제공합니다.
Method
EveryUpdate
EveryFixedUpdate
EveryEndOfFrame
EveryGameObjectUpdate
EveryLateUpdate
ObserveOnMainThread
NextFrame
IntervalFrame
TimerFrame
DelayFrame
SampleFrame
ThrottleFrame
ThrottleFirstFrame
TimeoutFrame
DelayFrameSubscription
FrameInterval
FrameTimeInterval
BatchFrame

예를들어, delayed invoke once:
Observable.TimerFrame(100).Subscribe(_ => Debug.Log("after 100 frame"));

Every* 메서드의 실행 순서입니다.
EveryGameObjectUpdate(in MainThreadDispatcher's Execution Order) ->
EveryUpdate -> 
EveryLateUpdate -> 
EveryEndOfFrame

MainThreadDispatcher.Update 전에 호출자가 호출되면 EveryGameObjectUpdate가 동일한 프레임에서 호출됩니다. MainThreadDispatcher는 다른것보다 먼저 호출하는것을 권장합니다. (ScriptExecutionOrder makes -32000)
EveryLateUpdate, EveryEndOfFrame 은 같은 프레임에서 호출됩니다.
EveryUpdate는 다음 프레임에서 호출됩니다.


MicroCoroutine

MicroCoroutine 은 메모리 효율적이고 빠른 coroutine worker 입니다. 이 구현은 Unity blog's 10000 UPDATE() CALLS 를 기반으로 하고, 관리되지 않는 오버헤드를 피하므로 속도가 10배 빨라집니다. MicroCoroutine은 Framecount-based time operator와 ObserveEveryValueChanged에 자동으로 사용됩니다.

표준 Unity coroutine 대신 MicroCoroutine을 사용하려는 경우, 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());
    }
}

86e9ed5c-1a0c-11e6-8371-14b61a09c72c.png
[PNG image (162.88 KB)]



MicroCoroutine의 제한사항은 yield return null만 지원하며 update 타이밍은 start method (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 Integration

UniRx는 UnityEvent를 쉽게 처리할 수 있습니다. 이벤트를 구독(subscripbe) 하려면 UnityEvent.AsObservable를 사용하세요.
public Button MyButton;
// ---
MyButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));

이벤트를 Observables로 처리하면 선언적 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

게임 데이터는 종종 알림을 요청합니다. 속성(properties)과 이벤트(callback)을 사용해야하나요? 그것들은 보통 너무 복잡합니다. UniRx는 가벼운 속성(property) 브로커인 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;
    });

ReactiveProperties, ReactiveCollections 과 UnityEvent.AsObservable에서 반환된 observables를 조합할 수 있습니다. 모든 UI 속성은 관찰할수있습니다.

일반적인 ReactiveProperty들은 유니티 에디터에서 직렬화(serializable)하거나 검사(inspectable) 할 수 없으나, UniRx는 ReactiveProperty의 특별한 서브클래스를 제공합니다. 여기에는 Int/LongReactiveProperty, Float/DoubleReactiveProperty, StringReactiveProperty, BoolReactiveProperty 그리고 기타 등등(InspectableReactiveProperty.cs 를 확인해보세요) 과 같은 클래스들이 포함됩니다. 모두 인스펙터(Inspector) 에서 완전히 편집 할 수 있습니다. 커스텀 Enum ReactiveProperty, 검사가능한 ReactivePropertyT를 작성하기가 쉽습니다.

ReactiveProperty에 [Multiline]이나 [Range]가 필요하면, MultiLineRange대신에 MultilineReactivePropertyAttributeRangeReactivePropertyAttribute를 사용할 수 있습니다.


파생되어 제공된 InspectableReactiveProperty들은 Inspector에 자연스럽게 표시되고 Inspector에서 값이 변경될때와 마찬가지로 값이 변경될때 알려줍니다.

RxPropInspector.png
[PNG image (10.66 KB)]


이 기능성은 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 값이 스트림(stream) 안에서만 변경되는경우, 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_Pattern.png
[PNG image (11.08 KB)]

MVVM 대신에 MVP를 사용해야하는 이유는 무엇입니까? Unity는 UI 바인딩 매커니즘을 제공하지 않으며 바인딩 레이어를 만드는것은 너무 복잡하고 손실이며 성능에 영향을 줍니다. 그래도 View는 업데이트를 해야합니다. Presenter는 view의 구성요소를 인식하고 업데이트 할 수 있습니다. 실제 바인딩은 없지만, Observables는 notification에대한 구독(subscription)을 허용하고, 이는 실제와 매우 유사합니다. 이 패턴을 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();
    }
}

View는 scene이고, 이는 Unity 계층입니다. View는 초기화시에 Unity Engine을 통해 Presenter와 연결됩니다. XxxAsObservable 메서드를 사용하면 오버헤드 없이 이벤트 시그널을 쉽게 만들 수 있습니다. SubscribeToText와 SubscribeToInteractable은 간단한 바인딩과 같은 도우미입니다. 이것들은 간단한 도구 일 수도 있지만, 매우 강력합니다. 그것들은 Unity 환경에서 자연스럽고 높은 성능과 깨끗한 아키텍쳐를 제공합니다.

MVRP_Loop.png
[PNG image (31.54 KB)]


V -> RP -> M -> RP -> V 는 reactive 방식으로 완전히 연결됩니다. UniRx는 모든 어댑터 메소드와 클래스를 제공하지만, 다른 MVVM(또는 MV*) 프레임웍은 대신 사용할 수 있습니다. UniRx/ReactiveProperty는 단순한 툴킷일뿐입니다.

GUI 프로그래밍도 ObservableTriggers의 혜택을 받는다. ObservableTrigges는 Unity 이벤트를 Observable로 변환되고, MV(R)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));

(Obsolete)PresenterBase

Note: PresenterBase는 충분히 작동하지만, 너무 복잡합니다.
간단한 Initialize 메서드를 사용할 수 있고 parent 에서 child 로 호출할 수 있습니다. 대부분의 시나리오에서 작동합니다.
따라서 PresenterBase를 사용하지 않는것이 좋습니다.

ReactiveCommand, AsyncReactiveCommand

ReactiveCommand boolean interactable를 사용하는 button 명령의 추상화입니다.
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는 비동기 실행이 끝날때까지 CanExecute(대부분 버튼의 상호작용 가능으로 바인딩)가 false로 변경된 ReactiveCommand 의 변형이다.
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();		
        });		
    }		
}	

AsyncReactiveCommand는 세개의 생성자가 있다.
  • () - 비동기 실행이 끝날때까지 CanExecute가 false로 변경됩니다.
  • (IObservable<bool> canExecuteSource) - 비어있는 상태로 혼합되면, canExecuteSource가 true로 보내고 실행되지 않을때 CanExecute 는 true가 됩니다.
  • (IReactiveProperty<bool> sharedCanExecute) - 여러개의 AsyncReactiveCommand 간에 실행 상태 공유, 하나의 AsyncReactiveCommand 가 실행중이면, 다른 AsyncReactiveCommand(동일한 sharedCanExecute property)가 비동기 실행이 끝날때까지 CanExecute는 false 가 됩니다.

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는 type별로 필터된 Rx기반 in-memory 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의 변경으로, Publish call을 기다릴 수 있습니다.
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 도구를 포함하고 있습니다. 현재 ObjectPoolAsyncObjectPool을 포함하고 있습니다. rent 명령 전에 pool을 채우기위해 Rent, Return 그리고 PreloadAsync 를 할 수 있습니다.
// 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 사용자에게 커스텀 analyzer인 UniRxAnalyzer를 제공합니다. 예를 들면 스트림 구독여부를 감지할 수 있습니다.
AnalyzerReference.jpg
[JPG image (18.61 KB)]

VSAnalyzer.jpg
[JPG image (25.55 KB)]

ObservableWWW는 구독 할 때까지 실행되지 않으므로, analyzer는 잘못된 사용법에 대해 경고합니다. 이것은 NuGet에서 다운로드 받을 수 있습니다.
GitHub Issues에 새로운 analyzer 아이디어를 등록해주세요.

Samples

UniRx/Examples을 참고해주세요.
샘플은 다른것들 중에서도 MainThreadDispatcher인 resource management (Sample09_EventHandling) 하는 방법을 보여줍니다.

Windows Store/Phone App (NETFX_CORE)

UniRx.IObservable<T>System.IObservable<T> 와 같은 일부 인터페이스는 Windows Store App 에 등록할때 충돌이 발생합니다. 따라서 NETFX_CORE를 사용할때, UniRx.IObservable<T>와 같은 구문을 사용하지말고 네임스페이스를 추가없이 짧은 이름으로 UniRx 구성요서를 참조해주세요. 이것이 충돌을 해결해줍니다.

async/await Support

Mono/.Net in Editor on 5.5.0b4 에서 Unity는 .NET 4.6 과 C# 6 언어를 지원합니다. UniRx는 Task multithreading 에서 MainThread 로 돌아갈수 있도록 UniRxSynchronizationContext를 제공합니다.
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는 yield return 대신에 await Coroutine 지원 형식을 직접 지원합니다.
async Task CoroutineBridge()
{
    Debug.Log("start www await");

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

    Debug.Log(www.text);
}

물론, IObservable 도 기다릴 수 있습니다.
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 Separation

UniRx를 미리 빌드하려면, 자신의 dll을 빌드하세요. 프로젝트를 복제하고 UniRx.sln 을 열면 UniRx가 보입니다. UniRx의 풀세트 프로젝트입니다. UNITY;UNITY_5_4_OR_NEWER;UNITY_5_4_0;UNITY_5_4;UNITY_5; + UNITY_EDITOR, UNITY_IPHONE 또는 다른 플랫폼 심볼과 같은 컴파일 심볼을 define 해야합니다. 컴파일 심볼이 서로 다르므로 사전 빌드 바이너리를 페이지, 에셋 스토어에 제공할 수 없습니다.

.NET 3.5 일반 CLR 어플리케이션에서 UniRx를 사용하려면, UniRx.Library를 사용하세요. UniRx.Library은 유니티 엔진 종속성이 분리되어있으며, UniRx.Library 빌드는 UniRxLibrary 심볼 define 이 필요합니다. pre-build UniRx.Library 바이너리는 NuGet 에서 사용할 수 있습니다.


Reference

UniRx API documents.

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.



GDC 2016 Sessions of Adventure Capialist
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.
Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2018-01-25 08:12:22
Processing time 0.0508 sec