e.blog

主にUnity/UE周りのことについてまとめていきます

逆引きUniRx - 使用例から見る使い方

概要

UniRx。最近よく目にする気がしますが基本的な概念は『Reactive Extensions』です。
似た用語として『関数型リアクティブプログラミング:Functional Reactive Programming(FRP』がありますが概念としては違うようです。

ざっくりとRx系ライブラリを説明すると、連続した値を『ストリーム』と捉え、それをどう扱うかに焦点を当てたものです。
ストリームのイメージは、『なにか』がパイプの中を流れていくイメージです。
この『なにか』はデータであるかもしれないし、時間かもしれません。とにかく抽象化されたものがパイプ内を流れ、それに加工を加えながら処理するもの、という感じです。

今回はUniRxの使い方の説明は割愛します。使い方や基礎的なところは@toRisouPさんがとても詳しく記事を書いてくださっているのでそちらを見るといいでしょう。

qiita.com



今回書くのは、ある程度基礎が分かっていて概念も把握したものの『で、実務でどう使ったらいいんだ?』となった人向けに、具体的な事例を交えて逆引き的に利用できるようにまとめたものです。
(というか、完全に自分のためのメモです・・・w なので、今後使いやすい・思い出しやすいように逆引きで書いてるというわけです)

なので多分、随時更新されていくと思います。

ドラッグ処理

最近実装したものでドラッグ処理です。
例で使われている_raycasterはレイが当たっているかどうかを判定するためのクラスで、_inputControllerはコントローラのトリガー状態を提供してくれるものです。

例がだいぶ偏ったものになっていますが、VRでコントローラからレイを飛ばしてなにかをドラッグして加速度を計算、対象オブジェクトを移動させる、みたいなシーンを思い浮かべてください。

var startStream = this.UpdateAsObservable()
                      .Where(_ => _raycaster.IsHit && _inputController.IsTriggerDown);

var stopStream = this.UpdateAsObservable()
                     .Where(_ => !_raycaster.IsHit || _inputController.IsTriggerUp);

startStream
    .SelectMany(x => this.UpdateAsObservable())
    .TakeUntil(stopStream)
    .Select(_ => _raycaster.ResultRaycastHit.point)
    .Pairwise()
    .RepeatUntilDestroy(this)
    .Subscribe(Dragging)
    .AddTo(this);


private void Dragging(Pair<Vector3> points)
{
    Vector3 cur = _anyObj.transform.worldToLocalMatrix.MultiplyPoint3x4(points.Current);
    Vector3 prev = _anyObj.transform.worldToLocalMatrix.MultiplyPoint3x4(points.Previous);

    float velocity = (cur.x - prev.x) / Time.deltaTime;
    _acceleration += velocity / Time.deltaTime;
    _acceleration *= _attenuateOfAcceleration;

    float relativeVelocity = (_acceleration * Time.deltaTime * _coefOfVelocity) - _anyObj.Velocity;

    _anyObj.AddVelocity(relativeVelocity);
}

キモは以下の部分です。

startStream
    .SelectMany(x => this.UpdateAsObservable())
    .TakeUntil(stopStream)
    .Select(_ => _raycaster.ResultRaycastHit.point)
    .Pairwise()
    .RepeatUntilDestroy(this)
    .Subscribe(Dragging)
    .AddTo(this);

ここで行っていることは、『なにがしかのスタートタイミング(startStream)』から開始され、指定の終了ストリーム(stopStream)に値が流れてくるまで継続する、というものです。

そしてストリームが継続している間はレイのヒット位置をストリームに流し(Select)、それをペアにし(Pairwise)、オブジェクトが破棄されるまで継続する、というものです。

値が閾値を越えたら処理をする

例えば、速度が一定速度以上になり、またそれが一定速度以下になった、という閾値またぎを検知したい場合があるかと思います。
そんなときに利用できるのがDistinctUntilChangedフィルタです。

これは、同じ値が連続している間はその値を流さない、という動作をします。
つまり、特定の値(今回の例では速度)が閾値をまたいだときにtrue/falseを返すようにしておき、それをフィルタすることで閾値をまたいだことを検知することができます。

ちなみに以下の例でSkip(1)が入っているのは初期化時など最初に発生してしまうイベントを無視するために入れています。

this.UpdateAsObservable()
    .Select(_ => Mathf.Abs(Velocity) <= _stopLimit)
    .DistinctUntilChanged()
    .Where(x => x)
    .Skip(1)
    .Subscribe(_ => DoAnything())
    .AddTo(this);

一定時間経過したら無効化する

次はボタンなど『開始イベント』と『終了イベント』があり、かつ制限時間を設けたい場合の処理です。

以下のサンプルではまず、開始イベントにスペースキーのDown、終了イベントにスペースキーのUpを設定しています。
そしてさらに、制限時間(例では3秒)が経過した場合も終了するようになっています。

var startStream = this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.Space));
var stopStream = this.UpdateAsObservable().Where(_ => Input.GetKeyUp(KeyCode.Space));
var timeOut = Observable.Timer(System.TimeSpan.FromMilliseconds(3000)).Select(_ => false);

startStream
    .SelectMany(stopStream.Select(_ => true).Amb(timeOut ).First())
    .Where(x => x)
    .Subscribe(_ => DoHoge())
    .AddTo(this);

ここでの大事な点はAmbです。
Ambはストリームを合成し、どちらかのストリームに流れたものをそのままひとつのストリームとして流してくれるオペレータです。

なのでここでは『スペースキーUp』と『制限時間経過』を『終了イベント』として捉え、そのどちらかが流れたら終了するようにしています。

ボタン長押しを検知

次はシンプルな『ボタン長押し』の処理です。

1秒後に発火

まず最初は指定時間押し続けていたら発火するもの。

var clickDownStream = this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.Space));
var clickUpStream = this.UpdateAsObservable().Where(_ => Input.GetKeyUp(KeyCode.Space));

clickDownStream
    .SelectMany(_ => Observable.Interval(System.TimeSpan.FromSeconds(1)))
    .TakeUntil(clickUpStream)
    .DoOnCompleted(() =>
    {
        Debug.Log("Completed!");
    })
    .RepeatUntilDestroy(this)
    .Subscribe(_ =>
    {
        Debug.Log("pressing...");
    });

押している間、押下を検知

上記は『長押し』だけを検知するものでしたが、こちらは『押している間』のイベントも受け取れるようにしたものです。
使用想定としては、押している間だけ常に何か処理をする、みたいなケースです。

var clickDownStream = this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.Space));
var clickUpStream = this.UpdateAsObservable().Where(_ => Input.GetKeyUp(KeyCode.Space));

clickDownStream
    .SelectMany(_ => this.UpdateAsObservable())
    .TakeUntil(clickUpStream)
    .DoOnCompleted(() =>
    {
        Debug.Log("Completed!");
    })
    .RepeatUntilDestroy(this)
    .Subscribe(_ =>
    {
        Debug.Log("pressing...");
    });

一定時間経つ前にボタンが離された場合も処理

こちらはUnity開発者ギルドの質問チャンネルで質問した際に教えていただいた方法です。
上記では『押している間』のイベントを捉えることができましたが、『終了判定』は取れませんでした。
『終了判定』を加えたものが以下のものです。

var startStream = this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.Space));
var stopStream = this.UpdateAsObservable().Where(_ => Input.GetKeyUp(KeyCode.Space));
var timeOut = Observable.Timer(System.TimeSpan.FromSeconds(5)).AsUnitObservable();

startStream
    .SelectMany(stopStream.Amb(timeOut))
    .First()
    .RepeatUntilDestroy(this)
    .Subscribe(_ =>
    {
        Debug.Log("hoge");
    });

値の変化監視(true / false)

値の変化監視はよくあるニーズだと思います。
例えば、前回はfalseだったものがtrueになったときだけ処理したい、などです。

using UniRx.Triggers; // ←UpdateAsObservableを使うにはこれが必要

this.UpdateAsObservable()
    .Select(_ => IsHoge())
    .DistinctUntilChanged()
    .Where(x => x)
    .Subscribe(_ =>
    {
        Debug.Log("Is Hoge");
    });

ObserveEveryValueChangedを使ったほうがもっとシンプルに書ける

this.ObserveEveryValueChanged(x => x.IsHoge())
    .Where(x => x)
    .Subscribe(_ =>
    {
        Debug.Log("Is Hoge");
    });

UniRxでコルーチンを使ったアニメーション

こちらはコルーチンを交えてUniRxでアニメーションを行う例です。
Observable.FromCoroutine<T>を使うことでコルーチンをストリームに変え、かつコルーチン内で計算した結果を受け取ることができます。

使い方は、`に渡すラムダの引数にObservableが渡ってくるのでそれをコルーチンに渡し、そのObservableを介してOnNext`を呼んでやることで実現しています。

private void DoAnimation()
{
    Observable.FromCoroutine<float>(o => Animation(o, duration))
        .SubscribeWithState3(transform, transform.position, position,
        (t, trans, start, goal) =>
        {
            trans.position = Vector3.Lerp(start, goal, t);
        })
        .AddTo(this);
}

private IEnumerator Animation(IObserver<float> observer, float duration)
{
    float timer = duration;

    while (timer >= 0)
    {
        timer -= Time.deltaTime;

        float t = 1f - (timer / duration);

        observer.OnNext(t);

        yield return null;
    }
}

参考記事

以下は参考にした記事のリンク集です。

qiita.com

noriok.hatenadiary.jp

developer.aiming-inc.com

qiita.com

qiita.com

www.slideshare.net

rxmarbles.com