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

こういうの作ります

swipegesture_d.gif

実行環境

  • Unity5.5.2f1
  • UniRx 5.5.0
  • DOTween v1.1.310

UniRx

やりたいこと

  • uGUI要素上におけるスワイプ/フリック処理の実装
    • e.g) 左右方向にスワイプしたら次の要素が表示される
  • FingerGestures とか Input.Touches 使えとか言わない

コード

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 発火時に Observable を生成することが出来ます
  • 今回は以下の2つを利用しています
    • OnBeginDragAsObservable : ドラッグ開始時に Observable を発火する
    • OnEndDragAsObservable : ドラッグ終了時に Observable を発火する
  • ドラッグ開始時にはスタート位置と開始時間を記録します
  • ドラッグ終了時に一定時間 (ThresholdSenconds) 以内かつ閾値 (ThresholdDistance) 以上の任意方向の移動があれば、該当する方向の Subject.OnNext を実行します

ページ送りを実装する

  • サンプルとして上記のスワイプ処理を利用してページ送りを実装する
  • モーションには DOTween を使用
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 をアタッチ
  • 親の pivot を左端に合わせる (0, 0.5)
    • RectTransform の AnchorPreset 上で shift 押しながら左の要素選択すると早い
  • 子要素に任意の大きさの Page を設置
  • 親に TurnPage をアタッチ
    • RequireComponent によって自動的に SwipeGesture もアタッチ
  • SwipeGestureコンポーネントの PageWidth に子要素の大きさを指定

swipegesture.gif

DEMO

swipegesture_demo.gif

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.