UniRx + MessagePipe を利用したフリック入力について


デモ

解説

  1. DragEvent(ドラッグされている座標+その時点の時間)をメッセージ化し購読し、別ストリームへと変換する

    
    [Inject] private ISubscriber<DragEvent> dragSub { get; set; }
    private ReactiveProperty<DragEvent> drag = new ReactiveProperty<DragEvent>(new DragEvent());
    
    dragSub.Subscribe(d => {
        this.transform.position = d.position;
        drag.Value = d; // 別ストリーム
    }).AddTo(this);
    
    // 購読されるクラス
    public class DragEvent : IMouseEvent {
        public Vector3 position { get; set; }
        public float time { get; set; }
    
        public DragEvent(IMouseEvent ime) {
            this.position = ime.position;
            this.time = ime.time;
        }
        public DragEvent() {
    
        }
    }
    
  2. DragEventを受け取るストリームを購読し、ドラッグ中の速度(draggingVerocity)を累積させる

    
    private Vector3 moveStartPosition;
    private ReactiveProperty<bool> isMoving = new ReactiveProperty<bool>(false); // 動作中の場合、true
    private ReactiveProperty<Vector3> draggingVerocity = new ReactiveProperty<Vector3>(Vector3.zero); // ドラッグ中に得た速度の合計
    
    // ReactiveProperty(DragEvent) から平均速度を算出する
    drag.Where(x => x != null)
        .Buffer(15).Where(d => d.Count >= 2)
        .Select(d => {
            var a = d.First();
            var b = d.Last();
            var dx = b.position - a.position;
            var dt = b.time - a.time;
            var verocity = dx / dt;
            if (!isMoving.Value) {
                moveStartPosition = a.position;// 動き出しのポジションをセット
            }
            return dx / dt;
        })
        .Subscribe(v => {
            draggingVerocity.Value = (draggingVerocity.Value + v) / 2;
    
            // 平均速度が閾値を超えている場合、isMovingをtrueに
            isMoving.Value = v.magnitude > thresholdMoving;
        }).AddTo(this);
    
            // 動いている間は マテリアルを赤くする
            isMoving.Subscribe(x => {
                if (x) {
                    material.color = Color.red;
                } else {
                    material.color = Color.white;
                    StopTransform();
                }
            }).AddTo(this);
    
  3. 累積したドラッグ速度が閾値を越えた場合にフリック処理を走らせる

    
    [SerializeField] private float thresholdMoving = 1; // TODO オブジェクトのスケールやフレームレートを考慮する必要があります
    [SerializeField] private float thresholdFlick = 50; // TODO 画面サイズに応じて計算する必要があります
    
    // 閾値を越えた場合にフリック処理を走らせる
    draggingVerocity
        .Where(v => v.magnitude > thresholdFlick && v != Vector3.zero)
        .Subscribe(_ => { Flick(); })
        .AddTo(this);
    
    private void Flick() {
        if (moveStartPosition == Vector3.zero) return;
    
        var positions = new Vector3[] {
            moveStartPosition,
            drag.Value.position
        };
        lineRenderer.SetPositions(positions); // フリック判定を視覚化しただけ
    
        // フリック後のポジションを次回フリック用に保存する
        moveStartPosition = drag.Value.position;
        draggingVerocity.Value = Vector3.zero; // TODO 無限ループしかねないのでフリック入力を実行するたびにSubscribeするべき
    }
    
    

全体のソース

public class DraggableObject : MonoBehaviour {

    [Inject] private ISubscriber<DragEvent> dragSub { get; set; }
    [Inject] private ISubscriber<MouseUpEvent> mUpSub { get; set; }

    private ReactiveProperty<DragEvent> drag = new ReactiveProperty<DragEvent>(new DragEvent());
    private ReactiveProperty<bool> isMoving = new ReactiveProperty<bool>(false); // 動作中の場合、true
    private ReactiveProperty<Vector3> draggingVerocity = new ReactiveProperty<Vector3>(Vector3.zero); // ドラッグ中に得た速度の合計
    private Vector3 moveStartPosition;

    [SerializeField] private float thresholdMoving = 1; // TODO オブジェクトのスケールやフレームレートを考慮する必要があります
    [SerializeField] private float thresholdFlick = 50; // TODO 画面サイズに応じて計算する必要があります

    // デバッグ用
    private LineRenderer lineRenderer;
    private Material material;

    private void Awake() {
        material = GetComponent<Renderer>().material;
        lineRenderer = GetComponent<LineRenderer>();
    }

    void Start() {

        dragSub.Subscribe(d => {
            this.transform.position = d.position;
            drag.Value = d;
        }).AddTo(this);

        mUpSub.Subscribe(mUp => {
            StopTransform();
        }).AddTo(this);

        // ReactiveProperty(DragEvent) から平均速度を算出する
        drag.Where(x => x != null)
            .Buffer(15).Where(d => d.Count >= 2)
            .Select(d => {
                var a = d.First();
                var b = d.Last();
                var dx = b.position - a.position;
                var dt = b.time - a.time;
                var verocity = dx / dt;
                if (!isMoving.Value) {
                    moveStartPosition = a.position;// 動き出しのポジションをセット
                }
                return dx / dt;
            })
            .Subscribe(v => {
                draggingVerocity.Value = (draggingVerocity.Value + v) / 2;

                // 平均速度が閾値を超えている場合、isMoving
                isMoving.Value = v.magnitude > thresholdMoving;
            }).AddTo(this);

        // 動いている間は マテリアルを赤くする
        isMoving.Subscribe(x => {
            if (x) {
                material.color = Color.red;
            } else {
                material.color = Color.white;
                StopTransform();
            }
        }).AddTo(this);

        draggingVerocity
            .Where(v => v.magnitude > thresholdFlick && v != Vector3.zero)
            .Subscribe(_ => { Flick(); })
            .AddTo(this);
    }


    private void Flick() {
        if (moveStartPosition == Vector3.zero) return;

        var positions = new Vector3[] {
            moveStartPosition,
            drag.Value.position
        };
        lineRenderer.SetPositions(positions);

        moveStartPosition = drag.Value.position;
        draggingVerocity.Value = Vector3.zero; // TODO 無限ループに気をつける必要がある
    }

    private void StopTransform() {
        draggingVerocity.Value = Vector3.zero; // TODO 無限ループに気をつける必要がある
        moveStartPosition = Vector3.zero;
    }
}

補足

ReactivePropertyからReactivePropertyへ値を伝搬させることは基本的に避けたほうが賢明だと思います。
必要があれば、状態を扱うためのコントローラを挟んだほうがいいでしょう。

値を通知せず、ReactivePropertyの値を変える方法も探しましたが私は見つけられませんでした。

フリック入力については、MouseUp とMouseDown を利用する実装では普遍的に使うことは難しいと思います。

ただしスマホの某パズルゲームのように動かす対象がオブジェクトとして定まり、その動作に影響するオブジェクトが詰まっている場合などは有効だと思います。

記事について

ソースに関しては、抜粋のためコピペでは動作しない可能性があります。
コメント、補足大歓迎です。

正常なフリック判定としては、まだ未完成なのでなぜこうなるかの解説と修正は次回に続きます