[Unity] マウスとタッチを同じように扱いDown/Up/Drag/Flickに対応する
Androidでタップやドラッグを行う場合にはInput.touchesを使用しますが、開発中はPC+マウスの環境でも実行したいのが初心者の心情です。両方の環境でマウス/タッチを同じように扱いたい。
そんなわけでマルチタッチを必要とせず、タッチは1点で十分だよ!でもマウスにも対応したいよ!という自分が必要な機能を実装してみた結果をまとめたいと思います。
今回も実装方法が正しいかどうかわかりませんが、公式ドキュメントを見たり、Google先生に聞いてみたりなるべく「右へならえ」になるようにしたつもりです。
Contents
実装する機能
- Android(タッチパネル)とPC環境(マウス)で同じように動作すること
- Down/Up/Drag/Flickの4種類が判定できること
としました。
参考にしたサイトはこちら
Unity2D フリック操作
http://stickpan.hatenablog.com/entry/2014/01/23/193922
実装方法
いきなりですがクラス、インターフェースを定義します。
GestureInfoクラス
入力情報を扱います。
using UnityEngine; using System.Collections; /// <summary> /// ジェスチャーパラメータ /// </summary> public class GestureInfo { /// <summary> /// mouse/touch位置を取得します /// </summary> /// <value>The position.</value> public Vector3 ScreenPosition { get; set; } /// <summary> /// 前回からの移動量を取得します /// </summary> /// <value>The delta position.</value> public Vector3 DeltaPosition { get; set; } /// <summary> /// mouse/touch downステータスを取得します /// </summary> /// <value>押された時にtrueになります</value> public bool IsDown { get; set; } /// <summary> /// mouse/touch upステータスを取得します /// </summary> /// <value>離れた時にtrueになります</value> public bool IsUp { get; set; } /// <summary> /// mouse/touch dragステータスを取得します /// </summary> /// <value>ドラッグ移動した時にtrueになります</value> public bool IsDrag { get; set; } /// <summary> /// 経過時間を取得します /// </summary> /// <value></value> public double DeltaTime { get; set; } /// <summary> /// 経過時間で移動した距離を取得します /// </summary> /// <value></value> public Vector3 DragDistance { get; set; } }
InputGestureインターフェース
通知受け取り用のメソッドを定義しています。これを継承してごにょごにょします。
using UnityEngine; using System.Collections; /// <summary> /// Input gesture. /// </summary> public interface InputGesture { /// <summary> /// ジェスチャーの処理順番番号 /// </summary> /// <value>0が一番速い、数値が大きくなると判定順番が遅くなる</value> int Order { get; } /// <summary> /// 指定ジェスチャーを処理する必要があるかどうかを取得します /// </summary> /// <returns>処理する必要があるならtrueを返す</returns> /// <param name="info">Info.</param> bool IsGestureProcess( GestureInfo info ); /// <summary> /// Down時に呼び出されます /// </summary> /// <param name="info">Info.</param> void OnGestureDown( GestureInfo info ); /// <summary> /// Up時に呼び出されます /// </summary> /// <param name="info">Info.</param> void OnGestureUp( GestureInfo info ); /// <summary> /// Drag時に呼び出されます /// </summary> /// <param name="info">Info.</param> void OnGestureDrag( GestureInfo info ); /// <summary> /// Flick時に呼び出されます /// </summary> /// <param name="info">Info.</param> void OnGestureFlick( GestureInfo info ); }
タッチ入力かマウス入力かの判定
判定方法はOSで分岐させればよいのではないでしょうか。
/// <summary> /// タッチパネル向けのプラットフォームかどうか取得します /// </summary> /// <returns>Android/iOSの場合にtrueを返します</returns> bool IsTouchPlatform() { if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer) { return true; } return false; }
位置、押された、離れたなどの判定
タッチ用とマウス用の2つを用意して違いを吸収します。今回はマルチタッチ無しの1点のみなので簡単そうです。
まずはタッチ情報があるかどうか、かつ前回と同じ指かどうかを判断しタッチ情報を取り出します。マルチタッチして誤動作しないように注意しました。
/// <summary> /// タッチされたタッチ情報を取得します /// </summary> /// <returns>タッチ情報を返します。何もタッチされていなければnullを返します</returns> Touch? GetTouch() { // 前回と同じタッチを追跡します // 新しいタッチの場合は最初のタッチを使用します if (Input.touchCount <= 0) { return null; } for (int n=0; n<Input.touchCount; n++) { if( this.TouchId == Input.touches[n].fingerId ) { return Input.touches[n]; // 前回と同じタッチ } } // 新規タッチ(タッチ開始時のみ) if (Input.touches [0].phase == TouchPhase.Began) { this.TouchId = Input.touches [0].fingerId; return Input.touches [0]; } return null; }
タッチ用の処理
GestureInfoにデータを入れてタッチ、マウス両方で扱えるようにしています。
/// <summary> /// タッチ入力情報をGestureInfoへ変換します /// </summary> /// <returns>入力情報があればtrueを返します</returns> /// <param name="info"></param> bool InputForTouch( ref GestureInfo info ) { // 基本的にタッチは1点のみ検出するインターフェースとする Touch? touch = GetTouch (); if (!touch.HasValue) { return false; } // 2015.02.03 update begin //info.ScreenPosition = touch.GetValueOrDefault().position; //info.DeltaPosition = touch.GetValueOrDefault().deltaPosition; var saveOldPosition = info.ScreenPosition; info.ScreenPosition = touch.GetValueOrDefault().position; // どうもdeltaPositionの値が実機だと正しく取れない // 前回のScreenPositionからDeltaPositionを求めるように変更する // info.DeltaPosition = touch.GetValueOrDefault().deltaPosition; if (touch.GetValueOrDefault ().phase != TouchPhase.Began) { info.DeltaPosition = info.ScreenPosition - saveOldPosition; } else { info.DeltaPosition = new Vector3(); } // 2015.02.03 update end 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; this.TouchId = -1; // タッチ終了 break; } return true; }
2015.02.03 追記
手持ちのAndroid機(Memopad7 ME176)で試したところdeltaPositionの値がピクセル単位ではない模様。
ピクセル単位を想定していたので前回の位置と今回の位置の差をdeltaPositionとするように変更しました。
つい昨日か一昨日ぐらいに、Unity + Androidのスワイプの話つぶやいてた方が居たような気が… Touch.deltaPositionが端末によって違うとかいう話だったかな?
— moku@Unityでアプリ開発 (@mokusan) 2015, 1月 27
@narudesign_dev Android向けではdeltaPositionは使わない方が良いのかな…デバイス間で均一でないとの書き込みもある。 http://t.co/0kM4Ub39OL #unity3d
— narudesign (@narudesign_dev) 2015, 1月 24
マウス用の処理
左マウスボタンの処理しています。
/// <summary> /// マウス入力情報をGestureInfoへ変換します /// </summary> /// <returns>入力情報があればtrueを返します</returns> /// <param name="info"></param> 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; }
入力チェック処理
上の関数を使用すると
- Downした
- Dragした
- Upした
の3つの状態を検出できます。
まずはDown、Drag、Upへの処理を記述します。
/// <summary> /// /// </summary> void Update () { this._gesture_info.IsDown = false; this._gesture_info.IsUp = false; this._gesture_info.IsDrag = false; // 入力チェック var isInput = IsTouchPlatform() ? InputForTouch( ref this._gesture_info ) : InputForMouse( ref this._gesture_info ); if (!isInput) { return; // 入力無し } // 各種移動 if (this._gesture_info.IsDown) { DoDown (this._gesture_info); } if (this._gesture_info.IsDrag) { DoDrag (this._gesture_info); } if (this._gesture_info.IsUp) { DoUp (this._gesture_info); } }
ここまででDown、Drag、Upの3つの状態を検出し、処理できそうです。残りはFlick処理のみ。
Flickを発生させる
常に20フレーム分(20/60秒=333msと仮定しました)の位置を保存しておきます。(保持するフレーム数は要調整。)
- 長めの期間内のベクトル = フレーム[0(一番古い位置)]からフレーム[19(一番新しい位置)]へのベクトル
- 短めの期間内のベクトル = フレーム[15(少し前の位置)]からフレーム[19(一番新しい位置)]へのベクトル
の2つのベクトルを用意します。
2つのベクトルがほぼ同一方向を向いていれば「Flick発生」としました。これは素早く「上移動→下移動→離す」が行われた時の対処です。
(この処理はUp処理の中にあり、指やマウスが離されたときに一度だけ判定します)
// Flick判定 var v1 = GetTraceVector ( 0, 0 ); var v2 = GetTraceVector ( TRACE_QUE_COUNT - 5, 0 ); var dot = Vector3.Dot( v1.normalized, v2.normalized ); if (dot > 0.9) { // Flick発生 this.ProcessingGesture.OnGestureFlick (info); }
出来れば動いた距離も判定に加えた方がよさそうです。距離もピクセル単位ではなくcm/インチ単位で行うとどんな環境でもほぼ同じ条件でFlickを発生させることができるはず・・・。まだ試したことないのでわかりませんが、今後調べて実装すると思います。
カメラ移動サンプル
Flick操作でぬるっと動くカメラ移動サンプルを作成してみました。もちろんAndroid版でも同じように動いています。
使用Assetは「Animated Knight and Slime Monster」
マウスの左ボタンでドラッグorフリックすれば、スーっとカメラが移動します。
(※ボタンを押したときでもカメラが移動してしまいます。)
説明に使用したソース
作成した4つのソースは こちら になります。
まとめ
Flick発生条件のみが特殊で、残りは通常のマウスと同じ処理になりました。Flick/Swipeのジェスチャーを何気なく使っていましたが、いざ作ってみると難しいですね。Flick/Swipeの動作がうまくいかないと「このアプリなんか使いづらいなぁ」と悪い印象を持たれてしまうので今後も調整していきたいですね。
参考にしようとタッチ入力対応のAssetを探したが1つしか見つけられなかった。アプリ毎に入力処理が全く異なるから、毎回作るアプリに合わせて実装したほうが楽 or 低コストということなんだろうか。
どうしても、うまく動きません。
完成プロジェクトをUPしていただけないでしょうか。
よろしくお願いします。
この時のソースを見たのですが無料のAssetを使っているので全てUPしていいものか・・・
そのかわり補足というか使い方です。
0. Unityで新規プロジェクトを作成。
1. 4つのソースをAssetsフォルダにコピーし、Unity上でスクリプトとして認識させます。
2. Hierarchy上の0,0,0にCubeを作成(動作確認用)
3. Hierarchr上に空のGameObjectを作成
4. 作成したGameObjectにInputGestureManager、CameraMoveInputGestureの2つのスクリプトを追加
5. 実行
6. 画面をマウスで操作するとカメラが移動する!(Cubeが動きます)
どうでしょうか。うまく動きましたでしょうか。