LoginSignup
10
9

More than 3 years have passed since last update.

UniRx/UniTask Observable.FromCoroutineをUniTaskで書く

Last updated at Posted at 2020-10-10

まえおき: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.FromCoroutineUniTaskで書くならどうしたらいいんだろう、というのが今回の話です。

書き方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について深く知りたいという方におすすめです。

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9