まえおき:Observable.FromCoroutine
UniRxにはObservable.FromCoroutine
という、コルーチンからObservable
を生成する機能があります。
これを使うとそれなりに複雑なObservable
をコルーチンを用いて手続き的に記述可能です。
UniRxというとどうしてもオペレータに意識をとらわれがちですが、実際はオペレータを使わずに実装した方が簡単に終わるケースもそれなりにあります。
そういった場面で活躍するのがこのObservable.FromCoroutine
です。
例:長押しイベントを発行するObservable
たとえば長押しイベントを発行するObservable
を実装してみます。
オペレータで実装
/*
* 1秒以上キーを長押すると true を発行
* 長押しをやめたタイミングで false を発行
*/
var inputStream =
Observable.EveryUpdate()
.Select(_ => Input.GetKey(KeyCode.A));
var longPressObservable =
inputStream
.DistinctUntilChanged()
.Throttle(TimeSpan.FromSeconds(1))
.Where(x => x)
.Merge(inputStream.Where(x => !x).Skip(1))
.DistinctUntilChanged();
コルーチンで実装
/*
* 1秒以上キーを長押すると true を発行
* 長押しをやめたタイミングで false を発行
*/
public IObservable<bool> CreateLongPressObservable()
{
return Observable.FromCoroutine<bool>(LongPressCoroutine);
}
private IEnumerator LongPressCoroutine(IObserver<bool> observer)
{
var pressTime = 0.0f;
var lastValue = false;
while (true)
{
if (Input.GetKey(KeyCode.A))
{
pressTime += Time.deltaTime;
if (pressTime >= 1.0f)
{
if (!lastValue)
{
observer.OnNext(true);
lastValue = true;
}
}
}
else
{
pressTime = 0;
if (lastValue)
{
observer.OnNext(false);
lastValue = false;
}
}
yield return null;
}
}
オペレータ vs コルーチン
オペレータを用いた実装の方がコンパクトにまとまってはいます。
ですが初見でこれを読み解くのは難しく、UniRxにそれなりに慣れてないと若干難しいコードになっています。
コルーチンの方はTHE・手続き型という昔ながらのコードになっています。
煩雑さはありますが、基本的なC#の文法さえわかっていれば読めるし編集できるというメリットがあります。
正直、この程度の複雑さの例であればどっちで書いてもあまり変わらないとは思います。
複雑になってくるとコルーチンの方が書きやすい
ここに仕様が追加され「Bキーを押している間は長押しの判定間隔を0.5秒にしてほしい!」となったとします。
こうなってくるとオペレータの方ではかなり厳しいです。
UniRxは「後からObservable
の挙動を変える」というのが苦手です。そのためこの様な条件によって挙動が変わるObservable
は作りにくく、できたとしてもかなり複雑怪奇になるでしょう。
一方、コルーチンの場合は数行修正する程度で対応が可能です。
private IEnumerator LongPressCoroutine(IObserver<bool> observer)
{
var pressTime = 0.0f;
var lastValue = false;
while (true)
{
// Bが押されている間だけしきい値を変える
var thresholdTime = Input.GetKey(KeyCode.B) ? 0.5f : 1.0f;
if (Input.GetKey(KeyCode.A))
{
pressTime += Time.deltaTime;
if (pressTime >= thresholdTime)
{
if (!lastValue)
{
observer.OnNext(true);
lastValue = true;
}
}
}
else
{
pressTime = 0;
if (lastValue)
{
observer.OnNext(false);
lastValue = false;
}
}
yield return null;
}
}
宣言的な記述もいいですけど昔ながらの手続き的な記述方法も大事ですよ、という話ですね。
本題:コルーチンよりもUniTaskを使いたい
UniTaskを使うことで、コルーチンと同等の処理をasync/await
を使って記述できるようになります。
じゃあObservable.FromCoroutine
もUniTask
で書くならどうしたらいいんだろう、というのが今回の話です。
書き方1: Observable.Create + CancellationDisposableを使う
Observable.Create
を使えばIObserver<T>
が取得できるので、それを非同期メソッドに渡してしまえばOKです。
キャンセル対応はCancellationDisposable
を使います。
public IObservable<bool> CreateLongPressObservable()
{
return Observable.Create<bool>(observer =>
{
// CancellationDisposableは
// IDisposableと連動するCancellationTokenを生成する
var cd = new CancellationDisposable();
LongPressAsync(observer, cd.Token).Forget();
return cd;
});
}
private async UniTaskVoid LongPressAsync(IObserver<bool> observer, CancellationToken token)
{
var pressTime = 0.0f;
var lastValue = false;
while (true)
{
if (Input.GetKey(KeyCode.A))
{
pressTime += Time.deltaTime;
if (pressTime >= 1.0f)
{
if (!lastValue)
{
observer.OnNext(true);
lastValue = true;
}
}
}
else
{
pressTime = 0;
if (lastValue)
{
observer.OnNext(false);
lastValue = false;
}
}
await UniTask.Yield(PlayerLoopTiming.Update, token);
}
}
より簡素に書く:UniTask.Voidを組み合わせる
UniTask.Void
を組み合わせるとメソッド内で処理を完結できます。
非同期メソッドの中身が小規模ならおすすめ。
public IObservable<bool> CreateLongPressObservable()
{
return Observable.Create<bool>(observer =>
{
var cd = new CancellationDisposable();
UniTask.Void(async ct =>
{
var pressTime = 0.0f;
var lastValue = false;
while (true)
{
if (Input.GetKey(KeyCode.A))
{
pressTime += Time.deltaTime;
if (pressTime >= 1.0f)
{
if (!lastValue)
{
observer.OnNext(true);
lastValue = true;
}
}
}
else
{
pressTime = 0;
if (lastValue)
{
observer.OnNext(false);
lastValue = false;
}
}
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
}, cd.Token);
return cd;
});
}
書き方2:UniTaskAsyncEnumerable -> Observable
UniTaskAsyncEnumerable
を使って記述してからObservable
に変換してしまう方法もあります。
UniTaskAsyncEnumerable.Create
が使えます。
public IObservable<bool> CreateLongPressObservable()
{
return UniTaskAsyncEnumerable.Create<bool>(async (writer, ct) =>
{
var pressTime = 0.0f;
var lastValue = false;
while (true)
{
if (Input.GetKey(KeyCode.A))
{
pressTime += Time.deltaTime;
if (pressTime >= 1.0f)
{
if (!lastValue)
{
writer.YieldAsync(true);
lastValue = true;
}
}
}
else
{
pressTime = 0;
if (lastValue)
{
writer.YieldAsync(false);
lastValue = false;
}
}
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
}).ToObservable();
}
今回のケースでは最後にObservable
へ変換していますが、場合によってはUniTaskAsyncEnumerable
のまま扱ったほうが効率的なケースも存在します。
Observable
を用いるべきか、UniTaskAsyncEnumerable
を用いるべきかはよく吟味しましょう。
まとめ
-
Observable
の作り方はオペレータだけじゃない - 手続き的に書いたほうがトータルで楽な場合も存在する
- コルーチンを非同期メソッドに置き換えることも可能
-
UniTaskAsyncEnumerable
も便利なので使おう
最後に
UniRx,UniTaskについて機能を紹介した「UniRx/UniTask完全理解(Amazon)」という本がまもなく発売されます。
電子書籍版も出ますのでUniRxやUniTaskについて深く知りたいという方におすすめです。