【Unity】タッチ処理の実装(エディタ、実機両対応)

【Unity】スマホとPCの両方のタッチに対応する

PCのクリックとスマホのタッチだと実は検出の実装方法が違うとのことで調べてみました。スマホ向けアプリを製作していてエディター上で動作確認しているシーンを想定しています。

// ★PC
// 左クリックが押し込まれたらtrue
bool isClick = Input.GetMouseButtonDown(0);

// ★スマホ
// タッチ開始(画面に触れたら)したらtrue
bool isTouch = Input.touchCount != 0 && Input.GetTouch(0).phase == TouchPhase.Began

既に似たような事を考えて既に管理クラスを実装している方もいるようなので、今回はそれらを参考にして EventTrigger や IPointerDownHandler、IPointerUpHandler、IPointerMoveHandler を使っ場合と似たような動作いなるように少し動きを調整したマネージャーを作成したいと思います。Unity でスマホの画面をタッチ(タップ)した時とデバッグ中に PC で画面をクリックしたときに両方対応した実装です。

で、今回はこのコードをベースに EventTrigger とか IPointerDownHandler、IPointerUpHandler、IPointerMoveHandlerとほぼ同じ動きになるように少し動きを調整したマネージャーを作成したいと思います。Unity でスマホの画面をタッチ(タップ)した時と PC で画面をクリックしたときに両方対応した実装です。

確認環境

確認環境は以下の通りです。

  • Unity 2021.3.16f1
  • Windows11
  • ★UniRx

外部ライブラリとして UniRx を使用します(イベント通知で使用しています)

実装例

// TouchManager.cs

#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WEBPLAYER
#undef IS_MOBILE
#else
#define IS_MOBILE // モバイル(=タッチ用の環境)の時だけ宣言する
#endif

using UnityEngine;

/// <summary>
/// モバイル・PC両対応の画面のタッチを検出・通知するクラス
/// </summary>
public class TouchManager : MonoBehaviour
{
    // InnerTypes

    public enum TouchState { None, Down/*タッチ開始*/, Move/*スワイプ中*/, Up/*タッチ終了*/ }

    // Inspector

    // Moveイベントを発生させるかどうかのフラグ
    // true: Moveイベントが発生する / false: 発生しない
    [SerializeField] bool _useMoveEvent = true;

    // Fields

    // 直前のタッチ位置
    Vector2 _previousPoint;
    Vector2 _startPos;

    // 無効な位置
    public static readonly Vector2 INVALID = new Vector2(float.NaN, float.NaN);

    // Props & Events

    // 凡そ、IPointerDownHandler, IPointerUpHandler, IPointerMoveHandler を実装したときと同じ動作

    /// <summary>
    /// 画面がタッチ or クリックされた時に発生します。
    /// </summary>
    public System.IObservable<TouchInfo> PointerDown => _pointerDown;
    private readonly UniRx.Subject<TouchInfo> _pointerDown = new();

    /// <summary>
    /// 画面が離された時に発生します。
    /// </summary>
    public System.IObservable<TouchInfo> PointerUp => _pointerUp;
    private readonly UniRx.Subject<TouchInfo> _pointerUp = new();

    /// <summary>
    /// 画面をドラッグ or スワイプ中に発生します。
    /// </summary>
    /// <remarks>
    /// 押されている最中のみ発生する。
    /// IPointerMoveHandler.OnPointerMove とは動きが違う。
    ///  → ポインターが画面上をうろうろしてもイベントは発生しない
    /// </remarks>
    public System.IObservable<TouchInfo> PointerMove => _pointerMove;
    private readonly UniRx.Subject<TouchInfo> _pointerMove = new();

    // Unity Impl

    private void Update()
    {
        bool isTouch = Input.GetMouseButtonDown(0);

        var state = GetPointerState();
        switch (state)
        {
            case TouchState.Down:
            {
                Vector2 curret = GetPosition();
                _startPos = curret;
                _previousPoint = curret;
                _pointerDown.OnNext(new TouchInfo(curret, Vector2.zero, curret));
                break;
            }
            case TouchState.Move:
            {
                if (!_useMoveEvent) return;

                Vector2 curret = GetPosition();
                if (curret == _previousPoint) return; // 動いた時だけ発生する
                Vector2 delta = curret - _previousPoint;
                _previousPoint = curret;
                _pointerMove.OnNext(new TouchInfo(curret, delta, _startPos));
                break;
            }
            case TouchState.Up:
            {
                Vector2 curret = GetPosition();
                Vector2 delta = curret - _previousPoint;
                _previousPoint = INVALID;
                _pointerUp.OnNext(new TouchInfo(curret, delta, _startPos));
                break;
            }
        }
    }

    private void OnDestroy()
    {
        using (_pointerDown) { }
        using (_pointerUp) { }
        using (_pointerMove) { }
    }

    // Methods

    /// <summary>
    /// 現在の操作状態を取得します。
    /// </summary>
    private TouchState GetPointerState()
    {
#if IS_MOBILE
            if (Input.touchCount == 0) return TouchState.None;
            return Input.GetTouch(0).phase switch
            {
                TouchPhase.Began => TouchState.Down,
                TouchPhase.Moved or TouchPhase.Stationary => TouchState.Move,
                TouchPhase.Canceled or TouchPhase.Ended => TouchState.Up,
                _ => TouchState.None,
            };
#else
        if (Input.GetMouseButtonDown(0)) return TouchState.Down;
        else if (Input.GetMouseButton(0)) return TouchState.Move;
        else if (Input.GetMouseButtonUp(0)) return TouchState.Up;
        return TouchState.None;
#endif
    }

    /// <summary>
    /// 現在の操作位置を取得します。
    /// </summary>
    private Vector2 GetPosition()
    {
#if IS_MOBILE
        return Input.GetTouch(0).position;
#else
        return GetPointerState() == TouchState.None ?
            Vector2.zero : (Vector2)Input.mousePosition;
#endif
    }
}

/// <summary>
/// タッチ情報
/// </summary>
public readonly struct TouchInfo
{
    /// <summary>
    /// タッチされたスクリーン座標を取得します。
    /// </summary>
    public readonly Vector2 Position;

    /// <summary>
    /// 前回のイベントからの移動量を取得します。
    /// </summary>
    public readonly Vector2 Delta;

    /// <summary>
    /// 開始位置のスクリーン座標を取得します。
    /// </summary>
    public readonly Vector2 PressPosition;

    public TouchInfo(Vector2 screenPoint, Vector2 delta, Vector2 pressPosition)
    {
        Position = screenPoint;
        Delta = delta;
        PressPosition = pressPosition;
    }
}

使用方法

イベント通知に UniRx を使用しているためイベントを登録するときは以下のように記述します。

private void Start()
{
    var mgr = FindObjectOfType<TouchManager>();
    mgr.PointerDown.Subscribe(p =>
    {
        Log.Trace($"Down={p.Position}, delta={p.Delta}", this);
    });
    mgr.PointerUp.Subscribe(p =>
    {
        Log.Trace($"Up={p.Position}, delta={p.Delta}", this);
    });
    mgr.PointerMove.Subscribe(p =>
    {
        Log.Trace($"Move={p.Position}, delta={p.Delta}", this);
    });
}

元のマネージャからの改変として、押しっぱなしにした時は移動したときだけ移動量と一緒にイベントを通知するようにています。スクリーン座標が返ってくるあたりは IPointerXXXHandler の PointerEventData と同じです。

最後に

よく考えたらシングルタッチしか対応しないならスマホもPCも Input.GetMouseButtonDown(0) でいいじゃんと思ったのは秘密です。 何でこんな実装をしたのかと言うと uGUI の EventTrigger や IPointerXXXHandler のメソッドでイベントを受け取ると後ろのコントロールが同時のイベントが発生しないので、画面のタッチ位置を uGUI 上に表示しつつボタンをクリックするという実装は IPointerXXXHandler とか EventTrigger だと難しいためこういったこの実装を使ってみました。