UniRx/UniTask  Observable.FromCoroutineをUniTaskで書く

# まえおき：Observable.FromCoroutine

[UniRx](https://github.com/neuecc/UniRx)には`Observable.FromCoroutine`という、コルーチンから`Observable`を生成する機能があります。
これを使うとそれなりに複雑な`Observable`をコルーチンを用いて手続き的に記述可能です。

UniRxというとどうしてもオペレータに意識をとらわれがちですが、実際はオペレータを使わずに実装した方が簡単に終わるケースもそれなりにあります。
そういった場面で活躍するのがこの`Observable.FromCoroutine`です。

## 例:長押しイベントを発行するObservable

たとえば長押しイベントを発行する`Observable`を実装してみます。

### オペレータで実装

```cs
/*
 * 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();
```

### コルーチンで実装

```cs

/*
 * 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`は作りにくく、できたとしてもかなり複雑怪奇になるでしょう。

一方、コルーチンの場合は数行修正する程度で対応が可能です。

```cs:仕様変更後
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](https://github.com/Cysharp/UniTask)を使うことで、コルーチンと同等の処理を`async/await`を使って記述できるようになります。

じゃあ`Observable.FromCoroutine`も`UniTask`で書くならどうしたらいいんだろう、というのが今回の話です。

## 書き方1: Observable.Create + CancellationDisposableを使う

`Observable.Create`を使えば`IObserver<T>`が取得できるので、それを非同期メソッドに渡してしまえばOKです。
キャンセル対応は`CancellationDisposable`を使います。

```cs
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`を組み合わせるとメソッド内で処理を完結できます。
非同期メソッドの中身が小規模ならおすすめ。

```cs
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`](https://qiita.com/toRisouP/items/8f66fd952eaffeaf3107#unitaskasyncenumerableiunitaskasyncenumerable)を使って記述してから`Observable`に変換してしまう方法もあります。
[`UniTaskAsyncEnumerable.Create`](https://qiita.com/toRisouP/items/8f66fd952eaffeaf3107#unitaskasyncenumerablecreate)が使えます。


```cs
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)](https://www.amazon.co.jp/dp/B08MNQT86F/ref=dp_kinw_strp_1)」という本がまもなく発売されます。
電子書籍版も出ますのでUniRxやUniTaskについて深く知りたいという方におすすめです。