[Unity] uGUIとUniRxで四方向へのスワイプを実装する

こういうの作ります

![swipegesture_d.gif](https://qiita-image-store.s3.amazonaws.com/0/102664/f051d697-7899-a659-543e-0fec5ba70e2d.gif)

## 実行環境
- Unity5.5.2f1
- UniRx 5.5.0
- DOTween v1.1.310

## UniRx
- 言わずと知れた Unity で Reactive Extensions (Rx) が使えるようになるアセット
  - [AssetStore](https://www.assetstore.unity3d.com/jp/#!/content/17276)
  - [GitHub](https://github.com/neuecc/UniRx)
- 詳細は他の方が詳しくまとめているのでそちらを参照
  -  [@toRisouP](https://twitter.com/torisoup) さん連載の [UniRx入門](http://qiita.com/ShirakawaYoshimaru/items/c46f5f40f4c3e3d6651b)
  - [渋谷ほととぎす通信](http://www.shibuya24.info/) さんの [UniRxリンク集](http://www.shibuya24.info/entry/2015/07/08/135718)

## やりたいこと
- uGUI要素上におけるスワイプ/フリック処理の実装
  - e.g) 左右方向にスワイプしたら次の要素が表示される
- ~~[FingerGestures](https://www.assetstore.unity3d.com/jp/#!/content/1044) とか [Input.Touches](https://www.assetstore.unity3d.com/jp/#!/content/3283) 使えとか言わない~~

## コード
```c#:SwipeGesture
using System;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class SwipeGesture : MonoBehaviour
{
    public float ThresholdSenconds = 1.0f;
    public float ThresholdDistance = 100.0f;

    private Subject<Unit> onSwipeLeft = new Subject<Unit>();
    public IObservable<Unit> OnSwipeLeft {
        get { return onSwipeLeft; }
    }

    private Subject<Unit> onSwipeRight = new Subject<Unit>();
    public IObservable<Unit> OnSwipeRight {
        get { return onSwipeRight; }
    }

    private Subject<Unit> onSwipeDown = new Subject<Unit>();
    public IObservable<Unit> OnSwipeDown {
        get { return onSwipeDown; }
    }

    private Subject<Unit> onSwipeUp = new Subject<Unit>();
    public IObservable<Unit> OnSwipeUp {
        get { return onSwipeUp; }
    }

    private Vector2 beginPosition;
    private DateTime beginTime;

    void OnEnable()
    {
        var eventTrigger = this.gameObject.AddComponent<ObservableEventTrigger>();

        eventTrigger
            .OnBeginDragAsObservable()
            .TakeUntilDisable(this)
            .Where(eventData => eventData.pointerDrag.gameObject == this.gameObject)
            .Select(eventData => eventData.position)
            .Subscribe(position =>
            {
                this.beginPosition = position;
                this.beginTime = DateTime.Now;
            });

        var onEndDragObservable = eventTrigger
            .OnEndDragAsObservable()
            .TakeUntilDisable(this)
            .Where(eventData => (DateTime.Now - this.beginTime).TotalSeconds < ThresholdSenconds)
            .Select(eventData => eventData.position)
            .Share();

        // left
        onEndDragObservable
            .Where(position => beginPosition.x > position.x)
            .Where(position => Mathf.Abs(beginPosition.x - position.x) >= this.ThresholdDistance)
            .Subscribe(_ => onSwipeLeft.OnNext(Unit.Default));

        // right
        onEndDragObservable
            .Where(position => position.x > beginPosition.x)
            .Where(position => Mathf.Abs(position.x - beginPosition.x) >= this.ThresholdDistance)
            .Subscribe(_ => onSwipeRight.OnNext(Unit.Default));

        // down
        onEndDragObservable
            .Where(position => beginPosition.y > position.y)
            .Where(position => Mathf.Abs(beginPosition.y - position.y) >= this.ThresholdDistance)
            .Subscribe(_ => onSwipeDown.OnNext(Unit.Default));

        // up
        onEndDragObservable
            .Where(position => position.y > beginPosition.y)
            .Where(position => Mathf.Abs(position.y - beginPosition.y) >= this.ThresholdDistance)
            .Subscribe(_ => onSwipeUp.OnNext(Unit.Default));

    }
}
```

### 説明
- ObservableEventTrigger を使用することで uGUI における任意の [EventTrigger](https://docs.unity3d.com/jp/540/ScriptReference/EventSystems.EventTrigger.html) 発火時に Observable を生成することが出来ます
- 今回は以下の2つを利用しています
  - OnBeginDragAsObservable : ドラッグ開始時に Observable を発火する
  - OnEndDragAsObservable : ドラッグ終了時に Observable を発火する
- ドラッグ開始時にはスタート位置と開始時間を記録します
- ドラッグ終了時に一定時間 (ThresholdSenconds) 以内かつ閾値 (ThresholdDistance) 以上の任意方向の移動があれば、該当する方向の Subject.OnNext を実行します

## ページ送りを実装する
- サンプルとして上記のスワイプ処理を利用してページ送りを実装する
- モーションには [DOTween](https://www.assetstore.unity3d.com/jp/#!/content/27676) を使用

```c#:TurnPage
using DG.Tweening;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(SwipeGesture), typeof(HorizontalLayoutGroup))]
public class TurnPage : MonoBehaviour
{
    public float PageWidth = 1080;

    private RectTransform rectTransform;
    private SwipeGesture swipeGesture;
    private Tween moveAnimation;
    private int pageCount;
    private int currentPage = 1;

    void Awake()
    {
        DOTween.Init();
        DOTween.defaultAutoPlay = AutoPlay.None; // Tween生成時に自動再生させない
    }

	void OnEnable()
	{
	    this.rectTransform = this.GetComponent<RectTransform>();
        this.swipeGesture = this.GetComponent<SwipeGesture>();
        this.pageCount = this.transform.childCount;

	    // next
	    this.swipeGesture
	        .OnSwipeLeft
	        .Where(_ => currentPage < pageCount) // 最大ページ以前である場合のみ進める
	        .Where(_ => this.moveAnimation == null || !this.moveAnimation.IsPlaying()) // アニメーション実行中ではない
	        .Subscribe(_ =>
	        {
	            this.currentPage++;
	            this.moveAnimation = this.rectTransform
	                .DOAnchorPosX(rectTransform.anchoredPosition.x - this.PageWidth, 1.0f)
	                .SetEase(Ease.OutBounce)
	                .Play();
	        });

	    // back
	    this.swipeGesture
	        .OnSwipeRight
	        .Where(_ => currentPage > 1) // 1ページ目以降である場合のみ戻れる
	        .Where(_ => this.moveAnimation == null || !this.moveAnimation.IsPlaying()) // アニメーション実行中ではない
	        .Subscribe(_ =>
	        {
	            this.currentPage--;
	            this.moveAnimation = this.rectTransform
	                .DOAnchorPosX(rectTransform.anchoredPosition.x + this.PageWidth, 1.0f)
	                .SetEase(Ease.OutBounce)
	                .Play();
	        });
	}
}
```

### 使い方
- HorizontalLayoutGroup (横に要素を並べるリスト) を親にアタッチ
- 親に ContentSizeFitter をアタッチ
  - 子要素の数/大きさに合わせて親要素の大きさを変更する
  - 詳しくは [[Unity] ContentSizeFitterを使ってコンテンツサイズによって可変なUI要素を作る](http://qiita.com/lycoris102/items/2ca34aafa1144439324e) に記載
- 親の pivot を左端に合わせる (0, 0.5)
  - RectTransform の AnchorPreset 上で shift 押しながら左の要素選択すると早い
- 子要素に任意の大きさの Page を設置
- 親に TurnPage をアタッチ
  - RequireComponent によって自動的に SwipeGesture もアタッチ
- SwipeGestureコンポーネントの PageWidth に子要素の大きさを指定

![swipegesture.gif](https://qiita-image-store.s3.amazonaws.com/0/102664/74e4025b-bec0-5d5f-5479-e5d9e1e3cd30.gif)

### DEMO
![swipegesture_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/102664/15bd9017-fbed-c11c-1da8-104922473a83.gif)