この記事は最初に「Hongliu School」の公式アカウントで公開されました。
紅流アカデミーはあなたを数歩速くします!こんにちは、私は鄭紅志、テクノロジーの開拓者です。Dazhi (vx: zhz11235) と呼んでください。
この記事の翻訳者: Zheng Honzhi - Your Technical Pathfinder翻訳日2018 年
2 月 1 日
UniRx - Unity レスポンシブ プログラミング プラグイン
プラグイン作者河合善文 (neuecc)
UniRxとは何ですか?
UniRx (Unity Responsive Programming Plugin) は、.Net の Responsive Extension を書き換えます。.Net の公式 Rx は優れていますが、Unity では動作せず、IOS 用の IL2CPP との互換性の問題があります。このライブラリはこれらの問題を解決し、Unity 固有のツール クラスをいくつか追加します。対応プラットフォームはPC/Mac/Android/iOS/WP8/WindowsStore/などで、Unity4.6以降の全バージョンに対応しています。
Unity Asset Store の UniRx のアドレス (無料) - http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT
講義 PPT - http://www.slideshare.net/neuecc/unirx-reactive-extensions-for-unityen
ブログを更新 - https://medium.com/@neuecc
Unity フォーラムでのサポート - http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity
変更ログUniRx /リリース
UniRx にはコア ライブラリ (Rx のポート) + プラットフォーム アダプター (MainThreadScheduler/FromCoroutine/etc) + フレームワーク (ObservableTriggers/ReactiveProeperty/etc) が含まれます
Rx を使用する理由
一般に、ネットワーク操作にはWWW
とCoroutine
。ただし、次の理由により、非同期操作Coroutine
には。
- コルーチンは戻り値を持つことができません。戻り値の型は IEnumerator である必要があるためです。
- yield return ステートメントは Try-Catch できないため、コルーチンは例外を処理できません。
コード内に広い範囲の強い結合が発生します。
Rx は非同期の問題を解決するためにここにあります。Rx は、非同期操作をより洗練させ、イベント駆動型プログラミングを使用し、LINQ 操作を使用できます。
ゲーム ループ (すべての Update、OnCollisionEnter など)、センサー データ (Kinect、Leap Motion、VR 入力など) がイベントです。Rx はイベントを応答シーケンスに変換します。これは LINQ 操作を通じて簡単に組み合わせることができ、時間操作もサポートします。
Unity は通常シングルスレッドですが、UniRx を使用するとマルチスレッドが容易になります。
UniRx は uGUI プログラミングを簡素化し、すべての UI イベント (クリック、値の変更など) を UniRx イベント フローに変換できます。
序章
Rx を紹介する素晴らしい記事:あなたが見逃していたリアクティブ プログラミングの概要。
次のコードはダブルクリック検出を実装します。
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 行のコードで次の機能を示しています。
- ゲーム ループを (更新) としてイベントのストリームに変える
- 結合されたイベント ストリーム
- 独自のイベント ストリームをマージする
- 時間ベースの操作は非常に簡単です
ネットワーク運用
非同期ネットワーク操作には ObservableWWW を使用します。その Get/Post メソッドは、Subscribeable IObservable を返します。
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();
並列リクエストでは 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();
IEnumerators (コルーチン) の操作
IEnumerator (コルーチン) は、Unity の主要な非同期ツールです。UniRx はコルーチンと IObservable を統合します。コルーチンを使用して非同期コードを作成し、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 以降では、ToYieldstruct を使用して 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 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!
}
}
ここではいくつかの例を示します。次は複数の 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];
});
デフォルトのスケジューラ
UniRx の時間ベースの操作 (Interval、Timer、Buffer(timeSpan) など) は、デフォルトのスケジューラScheduler.MainThread
として。これは、ほとんどの演算子 ( を除くObservable.Start
) が単一のスレッドで動作するため、ObserverOn は必要なく、スレッド セーフに対処する必要もないことを意味します。これは標準の RxNet と同じではありませんが、より Unity 環境に準拠しています。
Scheduler.MainThread
Time.timeScale の影響を受けます。時間スケールを無視したい場合は、 を使用しますScheduler.MainThreadIgnoreTimeScale
。
MonoBehaviour のトリガー
Monobehaviour のイベントを処理できるUniRx.Triggers
ようになります。
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。
これらのトリガーの制御は、コンポーネント/ゲームオブジェクトの拡張メソッドをサブスクライブすることで簡単に行うことができます。これらのメソッドは、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 を使用してください。
カスタムトリガーを作成する
Unity のイベントを処理する最良の方法は、イベントを Observable に変換することです。UniRx に付属のトリガーでは不十分な場合は、独自のトリガーを作成することもできます。以下は、UGUI 長押しトリガーです。
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();
観察可能なライフサイクル管理
OnCompleted はいつ呼び出されますか? UniRx を使用する場合、サブスクリプションのライフサイクル管理は非常に重要です。ObservableTriggers
アタッチされたオブジェクトが破棄されるときに呼び出されます。他の静的メソッド ( 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();
}
オブジェクトが破棄されたときに自動的に解放したい場合は、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())
.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 はさらに、安全な繰り返しメソッドを提供します。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 は、未処理の例外がある場合でも、動的オブザーバブル (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));
これはとても便利です。監視対象のターゲットがゲームオブジェクトの場合、ターゲットが破棄されると停止して OnCompleted を呼び出します。監視対象がC#オブジェクトの場合、GC時にOnCompletedが呼び出されます。
Unity コールバックを IObservable に変換する
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);
}
ストリームロガー ストリームロガー
// 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"));
デバッグ デバッグ
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 固有の Extra Gems (Unity のクールな追加機能)
翻訳者注: SubscribeOnMainThread() はテスト後に効果がなく、バグがあります (2018 年 1 月 30 日、UniRx バージョン 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();
フレームカウントベースの時間演算子 (フレーム数に基づく時間演算)
UniRx は、フレーム番号に基づいていくつかの時間操作を提供します。
方法 |
---|
更新ごと |
すべての固定更新 |
すべてのフレームの終わり |
すべてのゲームオブジェクトの更新 |
毎遅更新 |
ObserveOnMainThread |
次のフレーム |
インターバルフレーム |
タイマーフレーム |
遅延フレーム |
サンプルフレーム |
スロットルフレーム |
スロットルファーストフレーム |
タイムアウトフレーム |
遅延フレームサブスクリプション |
フレーム間隔 |
フレーム時間間隔 |
バッチフレーム |
たとえば、遅延通話の場合は次のようになります。
Observable.TimerFrame(100).Subscribe(_ => Debug.Log("after 100 frame"));
Every* メソッドの実行順序は次のとおりです。
EveryGameObjectUpdate(in MainThreadDispatcher's Execution Order) ->
EveryUpdate ->
EveryLateUpdate ->
EveryEndOfFrame
EveryGameObjectUpdate は同じフレームで呼び出され、MainThreadDispatcher.Update から呼び出されます (作成者は、MainThreadDispatcher スクリプトを他のスクリプトの前に実行することを推奨しています (ScriptExecutionOrder は -32000 に設定されています)) EveryLateUpdate、EveryEndOfFrame は同じフレームで呼び出されます。次に、EveryGameObjectUpdate を次のスクリプトで呼び出し
ます
。フレームなど
MicroCoroutine (マイクロコルーチン)
MicroCoroutines はメモリ効率が高く、高速です。これは Unity のブログ「10000 UPDATE() CALLS」(http://blogs.unity3d.com/2015/12/23/1k-update-calls/) に基づいており、数十倍高速化できます。MicroCoroutine は、フレームベースの時間操作と ObserveEveryValueChanged に自動的に使用されます。
Unity の組み込みコルーチンの代わりに 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());
}
}
MicroCoroutine の制限は、サポートのみでありyield return null
、呼び出し時刻は呼び出しメソッド ( StartUpdateMicroCoroutine
、StartFixedUpdateMicroCoroutine
、StartEndOfFrameMicroCoroutine
) に従って決定されることです。
他の IObservable と一緒に使用すると、isDone などの Done プロパティを確認できます。
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"));
イベントを Observable に変換すると、宣言型 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;
});
ReactiveProperties、ReactiveCollections、およびオブザーバブルを UnityEvent.AsObservable と組み合わせることができます。すべての UI コンポーネントが監視可能です。
一般に、ReactiveProperties はシリアル化できないか、Unity エディターのインスペクター パネルに表示されませんが、UniRx はこの機能を実装するための特別なサブクラスを提供します。Int/LongReactiveProperty、Float/DoubleReactiveProperty、StringReactiveProperty、BoolReactiveProperty などについては、InspectableReactiveProperty.cs
(https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/InspectableReactiveProperty.cs) ) を参照してください。どちらもインスペクターで編集できます。カスタム列挙型 ReactiveProperty の場合、検査可能な ReactiveProperty[T] を記述するのも簡単です。
ReactivePropertyが必要な場合、[Multiline]
または ReactiveProperty に[Range]
追加する場合は、 とを とMultilineReactivePropertyAttribute
にRangeReactivePropertyAttribute
置き換えることができます。Multiline
Range
これらの InpsectableReactiveProperties はインスペクター パネルに表示でき、エディターでも値が変更されたときに通知を受け取ることができます。
この機能はInspectorDisplayDrawer
(https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/InspectorDisplayDrawer.cs) で実装されています。このクラスを継承することで、インスペクター パネルにカスタム ReactiveProperties の描画を実装できます。
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();
}
}
MVP デザイン パターン Model-View-(Reactive) Presenter パターン
MVP (MVRP) デザイン パターンは UniRx で実装できます。
MVVM パターンの代わりに MVP パターンを使用する必要があるのはなぜですか? Unity は UI バインディング メカニズムを提供していないため、バインディング レイヤーの作成は複雑すぎるため、パフォーマンスに影響します。ただし、ビューを更新する必要があります。Presenters レイヤーはビューのコンポーネントを認識しており、それらを更新できます。実際のバインディングはありませんが、Observables はサブスクライバーに通知することができ、機能は似ています。このパターンはリアクティブ プレゼンターと呼ばれます。
// 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 の階層によって定義されます。プレゼンテーション層は、Unity の初期化時にビュー層をバインドします。XxxAsObservable メソッドを使用すると、オーバーヘッドなしでイベント信号を簡単に作成できます。SubscribeToText と SubscribeToInteractable は両方とも、簡潔なバインディングのようなヘルパー関数です。これらのツールはシンプルですが、非常に便利です。Unity での使用はスムーズで、パフォーマンスも良く、コードをクリーンに保ちます。
V -> RP -> M -> RP -> V はリアクティブ方式で完全に接続されています。UniRx はすべての適応メソッドとクラスを提供しますが、他の MVVM (または MV*) フレームワークも使用できます。UniRx/ReactiveProperty は単なる単純なツールキットです。
GUI プログラミングでも、ObservableTriggers から大きな恩恵を受けることができます。ObservableTriggers は Unity イベントを Observable に変換するため、それらを使用して MV®P パターンを構成できます。たとえば、uGUI イベントを Observable にObservableEventTrigger
変換します。
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));
(廃止) PresenterBase
備考:
PresenterBase は便利ですが、複雑すぎます。
使用できるのInitialize
はメソッド、ほとんどの状況ではサブクラスで親クラスを呼び出すだけで十分です。
したがって、推奨されなくなりましたPresenterBase
。
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
非同期操作が完了すると (通常はボタンの対話可能にバインドされ) false になります。
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
コンストラクターは 3 つあります。
()
- 非同期実行が完了するまで、CanExecute は false に変更されます。(IObservable<bool> canExecuteSource)
- canExecute は、canExecuteSource が true になり、実行されていないときに true になります。(IReactiveProperty<bool> sharedCanExecute)
- 実行状態は複数の AsyncReactiveCommand 間で共有され、1 つの AsyncReactiveCommand が実行されている場合、非同期操作が完了するまで他の AsyncReactiveCommand (同じsharedCanExecute プロパティを持つ) の 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 は、タイプ フィルタリングに基づく 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 は、非同期の Publish 呼び出しを処理できる MessageBroker のバリアントです。
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.ツールキット
UniRx.Toolkit
いくつかの Rx スタイルのツールが含まれています。現在、エラーObjectPool
と がAsyncObjectPool
。はい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 プロファイラー
Visual Studio 2015 のユーザーは、アナライザー UniRxAnalyzer を使用できます。サブスクライブされていないストリームを検出できます。
ObservableWWW
サブスクライブされるまで実行されないため、アナライザーは警告を発行します。アナライザーは NuGet からダウンロードできます。
- インストールパッケージ UniRxAnalyzer(http://www.nuget.org/packages/UniRxAnalyzer)
アナライザーの新しいアイデアを GitHub Issues に投稿してください。
例
UniRx/Examples
(https://github.com/neuecc/UniRx/tree/master/Assets/Plugins/UniRx/Examples)を参照してください。
Sample09_EventHandling は、MainThreadDispatcher などのリソース管理を行う方法を示します。
Windows ストア/電話アプリ (NETFX_CORE)
UniRx.IObservable<T>
やなどの一部のインターフェイスは、System.IObservable<T>
Windows ストア アプリで使用すると競合が発生します。
したがって、NETFX_CORE を使用する場合は、UniRx.IObservable<T>
同様の、名前空間のない UniRx コンポーネントの使用を避けてください。これにより競合の問題が解決されます。
非同期/待機のサポート
「5.5.0b4 のエディターのアップグレードされた Mono/.Net」(https://forum.unity3d.com/threads/upgraded-mono-net-in-editor-on-5-5-0b4.433541/) に基づいて、 Unity .NET 4.6 および C# 6 がサポートされています。UniRx は、マルチスレッド タスクをメイン スレッドに返す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 は await コルーチンも直接サポートします。
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 セグメンテーション
UniRx をプリコンパイルしたい場合は、dll を自分でコンパイルできます。このプロジェクトを複製して開くと、完全に独立したプロジェクトであることがUniRx.sln
わかります。+UniRx
などのマクロ、またはその他のプラットフォーム マクロを定義する必要があります。Unity のバージョンごとにマクロが異なるため、コンパイル済み DLL は提供できません。UNITY;UNITY_5_4_OR_NEWER;UNITY_5_4_0;UNITY_5_4;UNITY_5;
UNITY_EDITOR
UNITY_IPHONE
.NET 3.5 CLR アプリケーションに UniRx を使用したい場合は、次のようにすることができますUniRx.Library
。UniRx.Library
UnityEngineに依存せず、コンパイル時UniRx.Library
にUniRxLibrary
マクロを定義する必要があります。追加のプリコンパイル済みUniRx.Library
ライブラリはNuGet で入手できます。
Install-Package UniRx
(https://www.nuget.org/packages/UniRx)
その他の参考文献
- UniRx/wiki(https://github.com/neuecc/UniRx/wiki)
UniRx API ドキュメント。
ReactiveX の本拠地。はじめに、すべての演算子はグラフィカルな大理石の図で示されており、理解しやすくなっています。UniRx は公式のReactiveX 言語です。
優れたオンライン チュートリアルと電子書籍。
Rx.NET に関する多数のビデオ、スライド、ドキュメント。
@torisoupによる紹介スライド
@Xeriosによる紹介スライドとサンプル ゲーム
PlayFab API と統合する方法
UniRx を使用しているゲームまたはライブラリは何ですか?
ゲーム
- [ファームアウェイ!][(http://www.farmawaygame.com/)
- ビルドアウェイ!
- アドベンチャーキャピタリスト
- 冒険共産主義者
図書館
- PhotonWire - Photon Server + Unity の型付き非同期 RPC レイヤー。
- uFrame ゲーム フレームワーク- Unity エンジン用に設計された MVVM/MV* フレームワーク。
- EcsRx - ECS パラダイムを使用する Unity のためのシンプルなフレームワークですが、完全にリアクティブなシステム用に unirx を使用します。
- ActionStreetMap Demo - ASM は、OSM データを使用して実際の都市環境を動的に構築するためのエンジンです。
- utymap - UtyMap は、さまざまなデータ ソース (主に OpenStreetMap と Natural Earth) を使用して実際の都市環境を動的に構築するためのライブラリです。
- Submarine - Unity3D と RoR、Go で書かれた WebSocket サーバーで作られたモバイル ゲーム。
UniRx を使用している場合は、UniRx/issues/152にコメントしてください。
助けて貢献する
Unity フォーラムのサポート スレッド。質問があれば聞いてください - http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity
バグレポート、リクエスト、プルリクエストなど、あらゆる貢献を歓迎します。
GitHub の問題に関するレポートやリクエストを相談して送信してください。
ソースコードは で入手できますAssets/Plugins/UniRx/Scripts
。
このプロジェクトは Visual Studio とUnityVSを使用しています。
著者の他の Unity + LINQ アセット
LINQ to GameObject はUnity の GameObject 拡張機能のグループで、LINQ to XML のように階層を横断して GameObject を追加できるようにします。これは GitHub 上で無料でオープンソースです。
著者情報
川合 善文 (別名 neuecc) は日本のソフトウェア開発者です。
Grani, Inc. の取締役兼 CTO です。Grani は
日本のトップ ソーシャル ゲーム デベロッパーです。
彼は 2011 年から Visual C# で Microsoft MVP を受賞しています。彼はlinq.js (LINQ to Objects for JavaScript)
の作成者として知られています。
ブログ: https://medium.com/@neuecc (英語)
ブログ: http://neue.cc/ (日本語)
Twitter: https://twitter.com/neuecc (日本語)
ライセンス
このライブラリは MIT ライセンスの下にあります。