読者です 読者をやめる 読者になる 読者になる

みつろぐ

やれることはやってみる

【Unity】マウス/タッチ入力情報を管理しやすくしてみる

Unity

久々にUnityでスマートフォンゲームを作成する機会をもらったので、再勉強の意味も兼ねてタッチやマウスでの入力情報を管理する方法について検討してみました。


Unityではマウス入力やタッチ入力は比較的簡単に取れるのですが、それぞれが独立しているのため管理が面倒だなと感じたのが事の発端です。

どこまでを今回作成するものの管理対象にするかで非常に悩みましたが、実現すべき要件は次のような形で落ち着きました。

  • シングルタップのみサポートする
  • タッチ/クリックした画面上の座標が取得できること
  • サポートするアクションとしてはタッチ・ドラッグ・フリック
  • ロングタップなどのために経過時間を取得できるようにする

4.6以降の新UIなども考慮しようとも考えましたが既にEventSystemという自由度がまぁまぁ高いシステムが出来上がっているようですので、今回はゲームロジックに直結する部分のみをサポート範囲としました。tips.hecomi.com


実際に作成したものはこちらから参照できます。

作成するにあたって以下のサイトをめちゃくちゃ参考にさせていただきました。kikikiroku.session.jp

GestureInfo

まずは取得可能な情報のクラスについてです。
要件にあった情報に加えて、これは使用する場面があるのでは?という情報も追加しています。
タッチの場合、触れている指ごとにIDが指定されているのでそれも取得できるようにしました。タッチIDを取得できるようにすることでUIとの干渉が考えられる場面に対応できるようになります。

using UnityEngine;

public class GestureInfo 
{
	/// <summary>
	/// ジェスチャー座標
	/// </summary>
	public Vector3 ScreenPosition {
		get;
		set;
	}
	
	/// <summary>
	/// 前フレームからの移動距離
	/// </summary>
	public Vector3 DeltaPosition {
		get;
		set;
	}
	
	/// <summary>
	/// 前フレームからの経過時間
	/// </summary>
	public double DeltaTime {
		get;
		set;
	}
	
	/// <summary>
	/// ドラッグ距離
	/// </summary>
	/// <value> トレース有効時は特定期間中のドラッグ距離 </value>
	public Vector3 DragDistance {
		get;
		set;
	}
	
	/// <summary>
    /// 管理しているタッチ
    /// </summary>
    public int TouchId {
        get;
        set;
    }
	
	/// <summary>
	/// ジェスチャーステータス
	/// </summary>
	public bool IsDown {
		get;
		set;
	}
	public bool IsUp {
		get;
		set;
	}
	public bool IsDrag {
		get;
		set;
	}
}

InputGesture

次にタッチアクションが発生した際に呼ばれるイベント郡をまとめたインターフェイスです。
ゲームロジックを管理するクラスにこれを継承させて使用することを想定しています。実際にジェスチャーを有効にするには後述するシングルトンなマネージャークラスにクラス情報を登録する必要があります。

public interface InputGesture{
	/// <summary>
	/// ジェスチャーの処理優先度
	/// </summary>
	/// <value> 0が優先度高、数値が大きくなるにつれて優先度低 </value>
	int Order {
		get;
	}
	
	/// <summary>
	/// ジェスチャーを処理する必要があるのかを判定
	/// </summary>
	bool IsGestureProcess(GestureInfo info);
	
	/// <summary>
	/// ジェスチャー開始時に呼ばれる
	/// </summary>
	void OnGestureDown(GestureInfo info);
	
	/// <summary>
	/// ジェスチャー終了時に呼ばれる
	/// </summary>
	void OnGestureUp(GestureInfo info);
	
	/// <summary>
	/// ドラッグジェスチャー中に呼ばれる
	/// </summary>
	void OnGestureDrag(GestureInfo info);
	
	/// <summary>
	/// フリックジェスチャー時に呼ばれる
	/// </summary>
	void OnGestureFlick(GestureInfo info);
}

InputGestureManager

最後にタッチ情報を管理するシングルトンなマネージャークラスです。
タッチ/クリックの情報はこのクラスで処理をおこない、既に登録されているInputGestureを継承したクラスの対してイベントを発行していく形になっています。
ドラッグ距離・ドラッグ経過時間については、単純に開始地点と終了地点から計算する方法とトレース(フレーム毎のタッチ座標をキューに保存)した情報をもとに計算する方法の2つを用意しました。トレースを用いた場合、直近n秒のドラッグ情報が取得可能なためロジックによっては使用場面があるのではないかと思います。

using UnityEngine;
using System.Collections.Generic;

public class InputGestureManager : SingletonMonoBehaviour<InputGestureManager> 
{
    /// <summary>
    /// 登録済みのジェスチャー配列
    /// </summary>
    
    private List<InputGesture> gestures = new List<InputGesture>();

    /// <summary>
    /// ジェスチャー情報
    /// </summary>
    private GestureInfo gestureInfo = new GestureInfo ();

    /// <summary>
    /// ジェスチャー中のトレースを有効にするかどうか
    /// </summary>
    public bool IsTrace = true;

    /// <summary>
    /// トレース中の位置情報
    /// </summary>
    private Queue<Vector3> tracePositionQueue = new Queue<Vector3> ();

    /// <summary>
    /// トレース中の時間情報
    /// </summary>
    private Queue<float> traceTimeQueue = new Queue<float> ();

    /// <summary>
    /// トレースデータ保持数
    /// </summary>
    public int TraceQueryCount = 20;

    /// <summary>
    /// 登録済みのジェスチャー配列
    /// </summary>
    public List<InputGesture> Gestures {
        get { return gestures; }
        set { gestures = value; }
    }

    /// <summary>
    /// 現在処理中のジェスチャー情報
    /// </summary>
    private InputGesture processingGesture {
        get;
        set;
    }

    /// <summary>
    /// ジェスチャーの追加
    /// </summary>
    public void RegisterGesture( InputGesture gesture ) {
        var index = this.gestures.FindIndex (g => g.Order > gesture.Order);
        if (index < 0) {
            index = this.gestures.Count;
        }
        this.gestures.Insert (index, gesture);
    }

    /// <summary>
    /// ジェスチャーの解除を行います
    /// </summary>
    public void UnregisterGesture( InputGesture gesture ) {
        this.gestures.Remove (gesture);
    }
  
    void Start() {
        if (!IsTrace) {
            this.TraceQueryCount = 2;
        }
    }
  
    void Update () {
        this.gestureInfo.IsDown = false;
        this.gestureInfo.IsUp = false;
        this.gestureInfo.IsDrag = false;

        // 入力チェック
        var isInput = IsTouchPlatform() ? InputForTouch( ref this.gestureInfo ) : InputForMouse( ref this.gestureInfo );
        if (!isInput) {
            // 入力なし
            return;
        }

        // 入力処理
        if (this.gestureInfo.IsDown) {
            DoDown (this.gestureInfo);
        }
        if (this.gestureInfo.IsDrag) {
            DoDrag (this.gestureInfo);
        }
        if (this.gestureInfo.IsUp) {
            DoUp (this.gestureInfo);
        }
    }

    /// <summary>
    /// タッチパネル向けのプラットフォームかどうか取得します
    /// </summary>
    /// <returns>スマートフォン/タブレットの場合にtrueを返します</returns>
    bool IsTouchPlatform() {
        if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer) {
          return true;
        }
        return false;
    }

    /// <summary>
    /// タッチされたタッチ情報を取得します
    /// </summary>
    /// <returns>タッチ情報を返します。何もタッチされていなければnullを返します</returns>
    Touch? GetTouch() {
        if (Input.touchCount <= 0) {
            return null;
        }
        // 前回と同じタッチを追跡します
        for (int n=0; n<Input.touchCount; n++) {
            if( gestureInfo.TouchId == Input.touches[n].fingerId ) {
                return Input.touches[n];
            }
        }
        // 新規タッチ(タッチ開始時のみ)
        if (Input.touches[0].phase == TouchPhase.Began) {
            gestureInfo.TouchId = Input.touches[0].fingerId;
            return Input.touches[0];
        }
    
        return null;
    }

    /// <summary>
    /// タッチ入力情報をGestureInfoへ変換します
    /// </summary>
    /// <returns>入力情報があればtrueを返します</returns>
    bool InputForTouch( ref GestureInfo info ) {
        Touch? touch = GetTouch ();
        if (!touch.HasValue) {
            return false;
        }
    
        info.ScreenPosition = touch.GetValueOrDefault().position;
        info.DeltaPosition = touch.GetValueOrDefault().deltaPosition;
        switch (touch.GetValueOrDefault().phase) {
            case TouchPhase.Began:
                info.IsDown = true;
                break;
            case TouchPhase.Moved:
            case TouchPhase.Stationary:
                info.IsDrag = true;
                break;
            case TouchPhase.Ended:
            case TouchPhase.Canceled:
                info.IsUp = true;
                gestureInfo.TouchId = -1;
                break;
        }
        return true;
    }

    /// <summary>
    /// マウス入力情報をGestureInfoへ変換します
    /// </summary>
    /// <returns>入力情報があればtrueを返します</returns>
    bool InputForMouse( ref GestureInfo info ) {
        if (Input.GetMouseButtonDown(0)) {
            info.IsDown = true;
            info.DeltaPosition = new Vector3();
            info.ScreenPosition = Input.mousePosition;
        }
        if (Input.GetMouseButtonUp(0)) {
            info.IsUp = true;
            info.DeltaPosition = Input.mousePosition - info.ScreenPosition;
            info.ScreenPosition = Input.mousePosition;
        }
        if( Input.GetMouseButton(0)) {
            info.IsDrag = true;
            info.DeltaPosition = Input.mousePosition - info.ScreenPosition;
            info.ScreenPosition = Input.mousePosition;
        }
        return true;
    }

    /// <summary>
    /// ジェスチャー開始処理を行います
    /// </summary>
    void DoDown (GestureInfo info) {
        this.processingGesture = gestures.Find (ges => ges.IsGestureProcess (info));
        if (this.processingGesture == null) {
            return;
        }

        ClearTracePosition ();

        info.DeltaTime = 0;
        info.DragDistance = new Vector3 ();
        this.processingGesture.OnGestureDown (info);
    }

    /// <summary>
    /// ドラッグジェスチャー処理を行います
    /// </summary>
    void DoDrag (GestureInfo info) {
        if (this.processingGesture == null) {
            return;
        }

        AddTracePosition (info.ScreenPosition);
        info.DeltaTime = IsTrace ? GetTraceDeltaTime() : info.DeltaTime + Time.deltaTime;
        info.DragDistance = IsTrace ? GetTraceVector ( 0, 0 ) : info.DragDistance + GetTraceVector( 0, 0 );
    
        this.processingGesture.OnGestureDrag (info);
    }

    /// <summary>
    /// ジェスチャー終了処理を行います
    /// </summary>
    void DoUp (GestureInfo info) {
        if (this.processingGesture == null) {
            return;
        }
    
        info.DeltaTime = IsTrace ? GetTraceDeltaTime() : info.DeltaTime + Time.deltaTime;
        info.DragDistance = IsTrace ? GetTraceVector ( 0, 0 ) : info.DragDistance + GetTraceVector( 0, 0 );
        this.processingGesture.OnGestureUp (info);

        // フリックジェスチャー判定
        var v1 = GetTraceVector ( 0, 0 );
        var v2 = GetTraceVector ( this.tracePositionQueue.Count - 5, 0 );
        var dot = Vector3.Dot( v1.normalized, v2.normalized );
        if (dot > 0.9) {
            this.processingGesture.OnGestureFlick (info);
        }

        this.processingGesture = null;
    }

    /// <summary>
    /// トレース情報をクリアします
    /// </summary>
    void ClearTracePosition() {
        this.tracePositionQueue.Clear ();
        this.traceTimeQueue.Clear ();
    }

    /// <summary>
    /// ドラッグジェスチャー中の入力位置を追加します
    /// </summary>
    void AddTracePosition (Vector3 trace_position) {
        this.tracePositionQueue.Enqueue (trace_position);
        this.traceTimeQueue.Enqueue (Time.deltaTime);
    
        if (this.tracePositionQueue.Count > TraceQueryCount) {
            this.tracePositionQueue.Dequeue();
            this.traceTimeQueue.Dequeue();
        }
    }

    /// <summary>
    /// トレース経過時間を取得します
    /// </summary>
    float GetTraceDeltaTime() {
        float delta = 0;
        var times = this.traceTimeQueue.ToArray ();
        foreach (var t in times) {
            delta += t;
        }
  
        return delta;
    }

    /// <summary>
    /// トレースデータからベクトルを取得します
    /// </summary>
    Vector3 GetTraceVector ( int start_index_ofs, int end_index_ofs ) {
        var positions = this.tracePositionQueue.ToArray ();
        var sindex = start_index_ofs;
        var eindex = positions.Length - 1 - end_index_ofs;
        if (sindex < 0) {
            sindex = 0;
        }
        if (eindex < 0) {
            eindex = positions.Length - 1;
        }
        if (sindex >= positions.Length) {
            sindex = positions.Length - 1;
        }
        if (eindex >= positions.Length) {
            eindex = positions.Length - 1;
        }
        if (sindex > eindex) {
            var temp = sindex;
            sindex = eindex;
            eindex = temp;
        }
        return positions[eindex] - positions[sindex];
    }
  
    /// <summary>
    /// デバッグ表示のOn/Off
    /// </summary>
    public bool showDebug = false;
  
    /// <summary>
    /// デバッグ表示
    /// </summary>
    void OnGUI() {
        if (showDebug) {
            var info = this.gestureInfo;
            int x = 0;
            int y = 0;
            GUI.Label( new Rect(x,y,300,20), "ScreenPosition = " + info.ScreenPosition.ToString() );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "DeltaPosition = " + info.DeltaPosition.ToString() );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "IsDown = " + info.IsDown.ToString() );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "IsDrag = " + info.IsDrag.ToString() );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "IsUp = " + info.IsUp.ToString() );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "DeltaTime = " + info.DeltaTime.ToString() );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "DragDistance = " + info.DragDistance.ToString()  );
            y += 20;
            GUI.Label( new Rect(x,y,300,20), "processingGesture = " + (this.processingGesture == null ? "null" : "live")  );      
        }
    }
}

サンプル

実際にゲームロジックを作る際のサンプルを作成しました。画面をタップ/クリック時にcolliderコンポーネントが付与されたオブジェクトがあればそのオブジェクトの名前をログに出力してくれます。
使い方としては、インターフェイスInputGestureを継承してジェスチャーを有効にすれば使えるので非常に簡単だと思います。
サンプルコード中にも書いてありますが、UIと重なった時の処理はEventSystem.current.IsPointOverGameObject()を使って条件分岐してやればいいです。
ちなみに引数なしはマウス用、引数ありはタッチ用です。

using UnityEngine;
using UnityEngine.EventSystems;

public class ExampleGesture : MonoBehaviour, InputGesture {
	private Camera mainCamera;

	void Awake () {
		EnableGesture();
		mainCamera = Camera.main;
	}
	void OnDestroy () {
		DisableGesture();
	}
	
	public void EnableGesture () {
		InputGestureManager.Instance.RegisterGesture(this);			
	}
	public void DisableGesture () {
		InputGestureManager.Instance.UnregisterGesture(this);		
	}
	
	public int Order {
		get { return 0; }
	}
	
	public 	bool IsGestureProcess(GestureInfo info) {
		return true;
	}
	
	public void OnGestureDown(GestureInfo info) {
	    /// for 3D ///
	    RaycastHit hit3D;
	    Ray ray = mainCamera.ScreenPointToRay(info.ScreenPosition);
    	    if (Physics.Raycast(ray,out hit3D)) {
     	        Debug.Log(hit3D.collider.transform.name);
    	    }	

	    /// for 2D ///
	    Vector3 pos = Camera.main.ScreenToWorldPoint(info.ScreenPosition);
	    Collider2D hit2D = Physics2D.OverlapPoint(pos);
	    if (hit2D) {
		Debug.Log(hit2D.transform.name);
	    }
		
	    // オブジェクトの前のUIを考慮する場合は以下のような条件分岐を使用 ///
	    if (EventSystem.current.IsPointerOverGameObject() || EventSystem.current.IsPointerOverGameObject(info.TouchId)) {
				
	    }
	}
	
	public void OnGestureUp(GestureInfo info) {

	}
	
	public void OnGestureDrag(GestureInfo info) {

	}
	
	public void OnGestureFlick(GestureInfo info) {
	
	}
}