【Unity】UniRxを使ったドラッグ移動のサンプル【UniRx】

# はじめに
uGUIで作ったUIをドラッグで動かしたくなったので書いてみた。

# コード

```Drag.cs
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class Drag : MonoBehaviour
{
    public RectTransform target;
    public bool horizontal = true;
    public bool vertical = true;

    public bool interactable {
        get { return this.graphic.raycastTarget; }
        set { this.graphic.raycastTarget = value; }
    }

    private Canvas _canvas;

    public Canvas canvas {
        get {
            if (_canvas == null) {
                _canvas = this.GetComponentInParent<Canvas>();
            }
            return _canvas;
        }
        set { _canvas = value; }
    }

    private Graphic _graphic;

    private Graphic graphic {
        get { return _graphic ?? (_graphic = this.GetComponent<Graphic>()); }
    }

    private ObservableEventTrigger _trigger;

    private ObservableEventTrigger trigger {
        get { return _trigger ?? (_trigger = this.gameObject.GetOrAddComponent<ObservableEventTrigger>()); }
    }

    void Awake()
    {
        if (this.graphic == null) {
            Debug.LogWarning("Graphic component is required.");
            this.gameObject.AddComponent<Image>().color = Color.clear;
        }

        if (this.target == null) {
            this.target = this.GetComponent<RectTransform>();
        }

        this.trigger
            .OnBeginDragAsObservable()
            .Where(e => this.interactable && this.target)
            .SelectMany(this.trigger.OnDragAsObservable())
            .TakeUntil(this.trigger.OnEndDragAsObservable())
            .TakeWhile(e => this.interactable && this.target)
            .Select(e => GetPosition(e))
            .Pairwise() // NOTE: Buffer(2,1)だと最後にペアでない値が来る
            .RepeatUntilDestroy(this)
            .Subscribe(OnDrag)
            .AddTo(this);
    }

    private Vector3 GetPosition(PointerEventData eventData)
    {
        var screenPosition = eventData.position;
        var result = Vector3.zero;

        switch (this.canvas.renderMode) {
        case RenderMode.ScreenSpaceOverlay:
        case RenderMode.ScreenSpaceCamera:
            RectTransformUtility.ScreenPointToWorldPointInRectangle(
                this.target,
                screenPosition,
                eventData.pressEventCamera,
                out result);
            break;
        case RenderMode.WorldSpace:
            // TODO: WorldSpaceのCanvas対応
            Debug.LogWarning("not supported RenderMode.WorldSpace.");
            break;
        }

        return result;
    }

    private void OnDrag(Pair<Vector3> positions)
    {
        var deltaX = this.horizontal ? positions.Current.x - positions.Previous.x : 0;
        var deltaY = this.vertical ? positions.Current.y - positions.Previous.y : 0;
        this.target.position += new Vector3(deltaX, deltaY, 0);
    }
}
```

# 特徴
* デフォルトで自分自身をドラッグで移動、`target`に自分以外を指定するとそのRectTransformを移動
* 親のCanvasの`RenderMode`の`ScreenSpaceOverlay`と`ScreenSpaceCamera`に対応
* 「横軸だけ」「縦軸だけ」の移動に対応（インスペクタから指定可能）
* 相対的な移動距離を使っているのでドラッグ開始時にドラッグ開始場所に対象の真ん中が急に動いてこない

# 感想
`RenderMode`で位置計算するあたりでかなり手間取ったが最終的には`transform.position`を使うことで`ScreenSpaceOverlay`と`ScreenSpaceCamera`での座標計算ロジックを同じにできた。

`interactable`によるフィルタは要らないかもしれない。