imog

主にUnityとかの事を書いています

UniRXでマウスのドラッグを実装する

オブジェクトのドラッグであれば OnDragAsObservable で一発なんだけど、オブジェクトを対象としない場合に簡単なメソッドがなさそうだったので書いてみた。

public class DragObserver : ObservableMonoBehaviour
{
    private Subject<Vector2> onDragStream = new Subject<Vector2>();

    public IObservable<Vector2> onDragAsObservable {
        get { return onDragStream.AsObservable(); }
    }

    public override void Start ()
    {
        var mousePositionAsObservable = UpdateAsObservable().Select(_ => Input.mousePosition);

        mousePositionAsObservable.Zip(mousePositionAsObservable.Skip(1), (preview, current) => preview - current).
            DistinctUntilChanged().
            Where(_ => Input.GetMouseButton(0)).
            Subscribe(delta => onDragStream.OnNext(delta));
    }
}

使うときはこんな感じで。

DragObserver onDrag = FindObjectOfType<DragObserver>();
onDrag.onDragAsObservable.Subscribe(delta => print("差分:" + delta.ToString()));

一番欲しかったのはマウスの移動量で、前回の値をどうやって持ってくるかでずっと悩んでいたが下記のやり方でなんとかなった。

mousePositionAsObservable.Zip(mousePositionAsObservable.Skip(1), (preview, current) => preview - current)

Zip複数のストリームがそろったら流し込む、今回は以下の二つのストリームを待つようにしている。

  • mousePositionAsObservable(以下A)
  • mousePositionAsObservable.Skip(1) (以下B)

Aは毎フレームのマウス座標を流している。Bも同じだが、Skip(1)により最初のメッセージだけ無視している。

図で表すとこんな感じ

(毎フレーム処理するので本当は隙間なくメッセージ流れるけど見づらいので隙間を開けています)

---1-2-3-4-5-(Frame)

A -o-o-o-o-o-
    \ \ \ \
B ---o-o-o-o-
     | | | |
R ---o-o-o-o-

1フレーム前の座標Aと現在の座標Bで固めてそれぞれを preview, current として取り出して、マウスの移動量deltaを求めている。 Zip便利。

Where(_ => Input.GetMouseButton(0)). のところは、 SkilUntil(onMouse).TakeUntil(onMouseUp).Repeat() という感じで「マウスが押されている間購読して、マウスが放されたら購読をやめる」という感じに厳密にしたほうが良いかもしれない。

UniRX、スライドとかテキスト読むとすぐに納得できるけどいざ書くとき全く応用できてないので、 もっと積極的に組み込んで慣れるしかなさそう・・。

参考にしたページ

Learn Reactive Extensions

rxmarbles.com

※ 追記

zipじゃなくてBufferで良いのではというご指摘いただいたので試したらうまくいきました。 ありがとうございます。

public class DragObserver : ObservableMonoBehaviour
{
    private Subject<Vector2> onDragStream = new Subject<Vector2>();

    public IObservable<Vector2> onDragAsObservable {
        get { return onDragStream.AsObservable(); }
    }

    public override void Start ()
    {
        var mousePositionAsObservable = UpdateAsObservable().Select(_ => Input.mousePosition);

        mousePositionAsObservable.Buffer(2,1).
            Select(mousePosition => mousePosition.First() - mousePosition.Last()).
            DistinctUntilChanged().
            Where(_ => Input.GetMouseButton(0)).
            Subscribe(delta => onDragStream.OnNext(delta));
    }
}

Zipほげほげ部分を Buffer(2,1).Select に書き直したらうまくいった。

Bufferにskipも指定できるオーバーロードがあったみたい。 https://github.com/neuecc/UniRx/blob/master/Assets/UniRx/Scripts/Observable.Paging.cs#L215-L258

そもそもBufferを使った場合、貯めた分(Skipしたやつを除く)のメッセージのListが返ってくる。 今回は2回分、前フレームと現フレームがリストでくるのでそれをFirst()とLast()で差分取って解決した。

Buffer、塞き止める方は考えてたけどまとめて取れるのは意識できてなかった・・。