R3 の Observable と Signal
R3 の Observable<T> と FlowTask の Signal<T> は、FlowTask.R3 パッケージで互いにつなげます。このページでは、Observable をフローから待つ ToSignal と、Signal を Observable として購読する ToObservable を説明します。
NuGet の FlowTask.R3 を参照します。R3 も依存として入ります。名前空間は本体と同じ Katout.FlowTask です。
UPM のパッケージはありません。Unity では、本体の FlowBridge.FromCallback で Observable の値をシグナルにできます。
// damaged is an Observable<int>using var hits = FlowBridge.FromCallback<int>(emit => damaged.Subscribe(emit).Dispose);var damage = await hits.Next();この形では、Observable の完了と失敗はシグナルに届きません。シグナルは、作ったスコープの終わりで閉じます(シグナル)。
Observable をフローで待つ:ToSignal
Section titled “Observable をフローで待つ:ToSignal”async FlowTask WaitForClick(Observable<Unit> clicks){ using var click = clicks.ToSignal(); // subscribed now, disposed when this scope ends var r = await FlowTask.Race(click.Next(), FlowTask.WaitForSeconds(1)); if (r.Index == 0) await OnClicked();}ToSignal() は、今のスコープが所有する EventSignal<T> を返します。
- 呼んだ時点で Observable を購読します。スコープが終わると(または
Disposeすると)R3 の購読を解除し、シグナルを閉じます。 OnNextの値はシグナルに Emit されます。別のスレッドからのOnNextは、World の受信箱を通って次の Tick か Flush で届きます。- フローは
OnNextの中で同期には再開しません。次のフラッシュで再開します。 name引数で名前を付けると、ダンプの待ちの表示に出ます(clicks.ToSignal("Clicks"))。- 購読したときにすぐ流れる値(
ReactivePropertyやBehaviorSubjectの今の値)は、まだ待ち手がいないので受け取れません。今の値は、先にCurrentValue(BehaviorSubjectはValue)で読みます。
Next() は呼んだ後の次の値だけを待ちます(エッジ)。待っていない間の値は失われます。取りこぼしたくないなら購読します。
using var hits = damage.ToSignal();using var queue = hits.Subscribe(BufferPolicy.Queue(16, BufferOverflow.DropOldest));while (true){ var hit = await queue.Next(); await HitStun(hit); // hits during the stun are kept in the queue}購読とバッファの方針は シグナルにあります。
Observable が完了すると、シグナルは閉じます。別のスレッドでの完了も、その前に送られた値の後で閉じます。
- 閉じたシグナルを
Next()で待つとSignalClosedExceptionが投げられます。NextOrClosed()なら(false, default)が返ります。 - Observable が失敗で終わると、シグナルはその失敗を持って閉じます。
Next()で待っていたフローには、InnerExceptionが元の例外のSignalClosedExceptionが届きます。 - 失敗は R3 の未処理例外のハンドラ(
ObservableSystem.RegisterUnhandledExceptionHandlerで設定するもの)にも報告されます。待っているフローがないときは、ここで失敗が分かります。 OnErrorResumeで流れるエラーは、シグナルに届かず、R3 の未処理例外のハンドラだけに行きます。シグナルは閉じず、待っているフローは待ち続けます。フローで受けるなら、ToSignal()の前にOnErrorResumeAsFailure()を付けて、失敗での完了に変えます(source.OnErrorResumeAsFailure().ToSignal())。
using var feed = source.ToSignal("Feed");using var items = feed.Subscribe(BufferPolicy.Queue(8, BufferOverflow.Fail));while (true){ var (received, item) = await items.NextOrClosed(); if (!received) break; // the observable completed or failed Show(item);}NextOrClosed() は、完了でも失敗でも (false, default) を返します。失敗を見分けるなら、Next() で待って SignalClosedException の InnerException を見ます。
Signal を Observable として購読する:ToObservable
Section titled “Signal を Observable として購読する:ToObservable”var hits = new Signal<int>(world, "Hits");using var log = hits.ToObservable(world) .Where(damage => damage >= 10) .Subscribe(damage => Console.WriteLine($"Big hit: {damage}"));ToObservable(world) は、Signal の Emit を流す Observable を返します。
- R3 の購読 1 つごとに、World で転送のフローを 1 つ始めます。このフローは
R3.ToObservable(…)という名前でダンプに出ます。 - 値は Emit の中では流れず、World のフラッシュで Emit の順に流れます。
- 購読は、購読した時点からの Emit を受け取ります。
- World のスレッドで購読してください。
- Signal が閉じると、たまった値を流し終えてから
OnCompletedを呼びます。Close(error)で失敗として閉じたときは、そのエラーを持った失敗(Result.Failure)で呼びます。 - R3 の購読を
Disposeすると、転送のフローが取り消されます。Disposeはどのスレッドからでも呼べます。別のスレッドからは次の Tick か Flush で効きますが、Disposeの後の値は流れません。購読者がDisposeしたときはOnCompletedは呼ばれません。 - Signal が閉じる前に転送のフローが終わったとき(World の Dispose、例外)は、
OperationCanceledExceptionを持った失敗でOnCompletedを呼びます。
バッファの上限
Section titled “バッファの上限”2 回のフラッシュの間に届いた Emit は、購読のバッファにたまります。既定では 256 個までで、それを超えると未処理の例外になります(値を黙って捨てません)。方針は引数で変えられます。
var latest = position.ToObservable(world, BufferPolicy.Latest); // only the newest value per flush