LoginSignup
11
7

More than 1 year has passed since last update.

[Unity]よくあるスマホFPSゲームの移動とカメラのマルチタッチ操作

Last updated at Posted at 2021-05-31

こういうの

  • 青のポインタが左手の指で、移動操作
  • 黄緑のポインタが右手の指で、カメラ操作(エイム)

マルチタッチの管理って今まであんまりちゃんと向き合ってこなかったので、どう実装するかやってみた。
そんなに珍しいものでもないと思うけど、いざやってみると「この実装でいいんだっけ・・・」ってなったのでツッコミ待ち的な目的でちょっとまとめてみます。
プルリク的なノリで見てもらえると嬉しいです。

本記事のUnityプロジェクトはGithubで公開しています。

スワイプ操作を管理するDragHandlerコンポーネントを作成

DragHandler.cs
using System;
using UnityEngine;
using UnityEngine.EventSystems;

public class DragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public Action<PointerEventData> OnBeginDragEvent;

    public Action<PointerEventData> OnDragEvent;

    public Action<PointerEventData> OnEndDragEvent;

    public void OnBeginDrag(PointerEventData eventData)
    {
        OnBeginDragEvent?.Invoke(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        OnDragEvent?.Invoke(eventData);
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        OnEndDragEvent?.Invoke(eventData);
    }
}

ドラッグ操作のコールバックを取得する各種インターフェースを継承した、DragHandler.csを作りました。

  • IBeginDragHandler
  • IDragHandler
  • IEndDragHandler

外部から各種Actionメンバにドラッグ開始、ドラッグ中、ドラッグ終了のコールバックで呼び出す処理を渡せるようにして、汎用的に使えるようにしています。

タッチ判定を設置

image.png

画面に対して↑のように、画面半分ずつ覆うImageオブジェクトを配置します。
※わかりやすいよう可視化してますが、実際にはαを0にして透明にしています

それぞれに先ほどのDragHandler.csコンポーネントを追加しておきます。

プレイヤーの移動をDragHandler.csを使って実装

Player.cs
using UnityEngine;
using UnityEngine.EventSystems;

public class Player : MonoBehaviour
{
    /// <summary> 移動操作を受け付けるタッチエリア </summary>
    [SerializeField]
    private DragHandler _moveController;

    /// <summary> 移動速度(m/秒) </summary>
    [SerializeField]
    private float _movePerSecond = 7f;

    /// <summary> 移動操作としてタッチ開始したスクリーン座標 </summary>
    private Vector2 _movePointerPosBegin;

    private Vector3 _moveVector;

    /// <summary> 起動時 </summary>
    private void Awake()
    {
        _moveController.OnBeginDragEvent += OnBeginDragMove;
        _moveController.OnDragEvent += OnDragMove;
        _moveController.OnEndDragEvent += OnEndDragMove;
    }

    /// <summary> 更新処理 </summary>
    private void Update()
    {
        UpdateMove(_moveVector);
    }

    ////////////////////////////////////////////////////////////
    /// 移動操作
    ////////////////////////////////////////////////////////////
    #region Move

    /// <summary> ドラッグ操作開始(移動用) </summary>
    private void OnBeginDragMove(PointerEventData eventData)
    {
        // タッチ開始位置を保持
        _movePointerPosBegin = eventData.position;
    }

    /// <summary> ドラッグ操作中(移動用) </summary>
    private void OnDragMove(PointerEventData eventData)
    {
        // タッチ開始位置からのスワイプ量を移動ベクトルにする
        var vector = eventData.position - _movePointerPosBegin;
        _moveVector = new Vector3(vector.x, 0f, vector.y);
    }

    private void UpdateMove(Vector3 vector)
    {
        // 現在向きを基準に、入力されたベクトルに向かって移動
        transform.position += transform.rotation * vector.normalized * _movePerSecond * Time.deltaTime;
    }

    /// <summary> ドラッグ操作終了(移動用) </summary>
    private void OnEndDragMove(PointerEventData eventData)
    {
        // 移動ベクトルを解消
        _moveVector = Vector3.zero;
    }
    #endregion
}

プレイヤーとして操作するオブジェクトに付与するコンポーネントとして、Player.csを作ります。
メンバ_moveControllerに、先ほどDragHandler.csを付与したImageオブジェクトのうち、左の方をInspectorで渡しておきます。

これで画面左半分をタッチ起点としたスワイプが移動操作として反応するようになりました。

カメラの回転をDragHandler.csを使って実装

DragHandler.csにキャンバス上の座標取得する処理を追加

カメラ回転はスワイプ量によって回転角度が変わる処理を扱います。
dpiによって操作感が変わることを避けるため、タッチしたスクリーン座標をキャンバス上でのタッチ座標(ひいては操作量)に変換する処理を追加します。

メンバ追加

DragHandler.cs
/// <summary> 自身が所属してるキャンバス </summary>
private Canvas _belongedCanvas;

メソッド追加

DragHandler.cs
/// <summary>
/// スクリーン座標を自身が所属してるキャンバス上の座標に変換
/// </summary>
/// <param name="pointerPos">クリーン座標</param>
/// <returns>自身が所属してるキャンバス上の座標</returns>
public Vector2 GetPositionOnCanvas(Vector2 pointerPos)
{
    if(_belongedCanvas == null)
    {
        _belongedCanvas = GetBelongedCanvas(transform);
    }
    RectTransformUtility.ScreenPointToLocalPointInRectangle(_belongedCanvas.transform as RectTransform, pointerPos, _belongedCanvas.worldCamera, out Vector2 localPointerPos);
    return localPointerPos;
}

/// <summary> 所属するCanvasを取得 </summary>
private Canvas GetBelongedCanvas(Transform t)
{
    if (t == null)
    {
        return null;
    }

    var canvas = t.GetComponent<Canvas>();
    if (canvas != null)
    {
        return canvas;
    }

    return GetBelongedCanvas(t.parent);
}

Player.csにカメラ操作に関する処理を追加

メンバ追加

Player.cs
[SerializeField]
private Camera _camera;

/// <summary> カメラ操作を受け付けるタッチエリア </summary>
[SerializeField]
private DragHandler _lookController;

/// <summary> カメラ速度(°/px) </summary>
[SerializeField]
private float _angularPerPixel = 1f;

/// <summary> カメラ操作として前フレームにタッチしたキャンバス上の座標 </summary>
private Vector2 _lookPointerPosPre;

Awake処理追加

Player.cs
private void Awake()
{
    _moveController.OnBeginDragEvent += OnBeginDragMove;
    _moveController.OnDragEvent += OnDragMove;
    _moveController.OnEndDragEvent += OnEndDragMove;

    // ここからが追加分
    _lookController.OnBeginDragEvent += OnBeginDragLook;
    _lookController.OnDragEvent += OnDragLook;
}

メソッド追加

Player.cs
////////////////////////////////////////////////////////////
/// カメラ操作
////////////////////////////////////////////////////////////
#region Look
/// <summary> ドラッグ操作開始(カメラ用) </summary>
private void OnBeginDragLook(PointerEventData eventData)
{
    _lookPointerPosPre = _lookController.GetPositionOnCanvas(eventData.position);
}

/// <summary> ドラッグ操作中(カメラ用) </summary>
private void OnDragLook(PointerEventData eventData)
{
    var pointerPosOnCanvas = _lookController.GetPositionOnCanvas(eventData.position);
    // キャンバス上で前フレームから何px操作したかを計算
    var vector = pointerPosOnCanvas - _lookPointerPosPre;
    // 操作量に応じてカメラを回転
    LookRotate(new Vector2(-vector.y, vector.x));
    _lookPointerPosPre = pointerPosOnCanvas;
}

private void LookRotate(Vector2 angles)
{
    Vector2 deltaAngles = angles * _angularPerPixel;
    transform.eulerAngles += new Vector3(0f, deltaAngles.y);
    _camera.transform.localEulerAngles += new Vector3(deltaAngles.x, 0f);
}
#endregion

メンバ_lookControllerに、先ほどDragHandler.csを付与したImageオブジェクトのうち、右の方をInspectorで渡しておきます。
また、シーン中のMain Cameraをプレイヤーオブジェクトの視点の位置にアタッチして、Player.csのメンバ_cameraにInspectorで渡しておきます。

これで画面右半分をタッチ起点としたスワイプがカメラ操作として反応するようになりました。

これで完成です。

所感

IDragHandlerでタッチ管理すると、タッチ開始位置さえタッチ判定のRectTransform内であれば、以降は判定外までスワイプしても同じコンポーネント内で操作を管理してくれるから楽ですね。
これ系のインターフェースはタッチ位置が対象オブジェクトの矩形外になったら何も反応しなくなると思ってたので意外でした。

それを知るまではInput.GetTouchでタッチ情報を管理して、マルチタッチ中にどちらかの指が離されたら賢くインデックス値を切り替えて・・・みたいなことをしないといけないと思い込んでました。(マジで今更すぎる・・・)

ただ、いつだったか、「画面全体を覆うようなRaycast Targetオブジェクトは負荷が高くて非推奨」みたいなことを聞いたことがある気がするので、今回の実装はめちゃくちゃ楽だけど実はNGパターンだったりするんでしょうか・・・?知ってる人いたら教えてほしいです。

あと、これも今回知ったことですが、OnBeginDragが呼び出されるのは画面タッチした瞬間ではなく、タッチした指(あるいはカーソル)が一定距離動くまで呼び出されないみたいですね。
この記事では実装してないですが、これのおかげでピンチイン/アウト操作との共存もやりやすかったりしました。

こういう「よくある仕様」を復習がてら実装すると発見があるのでまた何か思いついたらやってみたいです。
今回はここまで。

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7