【Unity】【C#】ピンチ操作を取得してコールバックする 
2018/01/09 Tue [edit]
既にスワイプ、ロングタップ(クリック)は掲載しているが、スマートフォン等はピンチという操作もよく使われるので、Androidのプラグインに同梱する形で配布することにした。プラグイン自体は Android 専用のものだが、今回のピンチや既存のスワイプ、ロングタップは Unity 上でのスクリプトなので、iOS などでも使えると思う。
ついでにこのピンチを使って、オブジェクトを拡大・縮小、またはカメラを近づけたり、遠ざけたりする方法(スクリプト)も書いておこう。これらはプラグインのパッケージにデモとしてセットアップされているので、自分で作るのが面倒な人はプラグインをダウンロードして、コピペで利用しても良いと思う。

プラグインデモをダウンロード


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。
(※) Unity 5.6.3p1 / Windows10(x64) / Galaxy S7 Edge (Android 7.0) で確認
●ピンチを取得してコールバックする
using System;
using UnityEngine;
using UnityEngine.Events;
namespace FantomLib
{
/// <summary>
/// ピンチ操作を取得してコールバックする
/// 2018/01/09 Fantom (Unity 5.6.3p1)
/// http://fantom1x.blog130.fc2.com/blog-entry-288.html
///(使い方)
///・適当な GameObject にアタッチして、インスペクタから OnPinchStart, OnPinch にコールバックする関数を登録すれば使用可。
///・またはプロパティ IsPinching, Width, Delta, Ratio をフレーム毎監視しても良い(こちらの場合は使用してない状態(IsPinching=false, Width=0, Delta=0, Ratio=1)も含まれる)。
///(仕様説明)
///・内部的には画面でタッチされた2本の指の間隔をピクセル単位で取得する。ただし戻り値は画面幅で割った正規化された値とピクセルそのもので返すかを選べる(isNormalized)。
///・ピンチの操作は1本→2本となったときのみ認識する。3本以上→2本になったときは無効。
///・タッチデバイスを UNITY_ANDROID, UNITY_IOS としているので、他のデバイスも加えたい場合は #if の条件文にデバイスを追加する(Input.touchCount が取得できるもののみ)。
/// </summary>
public class PinchInput : MonoBehaviour
{
public bool isNormalized = true; //画面幅(or 高さ)で正規化した値でコールバックする(false=ピクセル単位で返す)
public bool widthReference = true; //isNormalized=true のとき、画面幅(Screen.width)を基準にする(false=高さ(Screen.height)を基準)[単位が px/Screen.width のようになる]
//認識する画面上の領域(0.0~1.0)[(0,0):画面左下, (1,1):画面右上]
public Rect validArea = new Rect(0, 0, 1, 1);
//ピンチ検出プロパティ(フレーム毎取得用)
public bool IsPinching {
get; private set;
}
//ピンチ幅(距離) プロパティ(フレーム毎取得用)
public float Width {
get; private set;
}
//ピンチ幅(距離)の直前との差分 プロパティ(フレーム毎取得用)
public float Delta {
get; private set;
}
//ピンチ幅(距離)の変化比 プロパティ(フレーム毎取得用)
public float Ratio {
get; private set;
}
//ピンチ開始コールバック
[Serializable]
public class PinchStartHandler : UnityEvent<float, Vector2> { } //Width, center(2指間の中心座標)が返る
public PinchStartHandler OnPinchStart;
//ピンチ中コールバック(伸縮率とその差分)
[Serializable]
public class PinchHandler : UnityEvent<float, float, float> { } //Width, Delta, Ratio が返る
public PinchHandler OnPinch;
//Local Values
float startDistance; //ピンチ開始時の指の距離(px)
float oldDistance; //直前の伸縮距離(px)
//アクティブになったら、初期化する(アプリの中断などしたときはリセットする)
void OnEnable()
{
IsPinching = false;
}
// Update is called once per frame
void Update()
{
//プロパティはフレーム毎にリセット
Width = 0; Delta = 0; Ratio = 1;
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS) //タッチで取得したいプラットフォームのみ
if (Input.touchCount == 2) //ピンチでの操作(2本指のみ)
{
//※fingerId と touches[] のインデクスは必ずしも一致しないらしいので fingerId=1 となっている方を取得(指1本→2本になったとき可能とするため)
Touch touch = (Input.touches[1].fingerId == 1) ? Input.touches[1] : Input.touches[0];
if (!IsPinching && touch.phase == TouchPhase.Began) //新しく認識したときのみ
{
//認識する画面上の領域内か?(2本の指の中心の座標を基準にする)
Vector2 center = (Input.touches[0].position + Input.touches[1].position) / 2;
if (validArea.xMin * Screen.width <= center.x && center.x <= validArea.xMax * Screen.width &&
validArea.yMin * Screen.height <= center.y && center.y <= validArea.yMax * Screen.height)
{
IsPinching = true; //ピンチ開始
//fingerId=0~1 のみ(必ず最初と2本目の指)。指3本→2本(0-2 など)は不可とする。
Width = startDistance = oldDistance = Vector2.Distance(Input.touches[0].position, Input.touches[1].position);
if (isNormalized)
{
float unit = widthReference ? Screen.width : Screen.height;
Width /= unit; //画面幅で正規化すれば、解像度に依存しなくなる
center /= unit;
}
if (OnPinchStart != null)
OnPinchStart.Invoke(Width, center); //開始時は必ず Delta=0, Ratio=1 となる
}
}
else if (IsPinching) //既に認識されているときのみ:3本→2本になったときは無効になる
{
float endDistance = Vector2.Distance(Input.touches[0].position, Input.touches[1].position);
Width = endDistance;
Delta = endDistance - oldDistance; //直前との差分
Ratio = endDistance / startDistance; //開始時のピンチ幅(px距離)を基準にした倍率になる
oldDistance = endDistance;
if (isNormalized)
{
float unit = widthReference ? Screen.width : Screen.height;
Width /= unit; //画面幅で正規化すれば、解像度に依存しなくなる
Delta /= unit;
}
if (OnPinch != null)
OnPinch.Invoke(Width, Delta, Ratio);
}
}
else //タッチが2つでないときは全て無効にする
#endif
{
IsPinching = false;
}
}
}
}
ピンチ検出の簡単な解説をしておくと、スマホなどのタッチデバイスで指を2本置いたとき、それぞれの座標からそれらの距離を取得し、指を動かすことによってその座標間の距離の差分でピンチと認識するというものである。
ただこのスクリプトではもう少し細かく検出されていて、例えば指が1本→2本となったときはピンチと認識するが、指が3本以上→2本となったときは認識しない(1本、3本以上のときは常に無視する)。
またコールバックの引数で返ってくる値は「現在のピンチの幅(距離)」「直前のピンチ幅との差分」「ピンチ開始したときの比率」と3つの値を返す。そしてそれらは実機により画面解像度が違うため、正規化オブション(isNormalized = true)により、端末の解像度で割った値を返すようにしてある(isNormalized = false にするとピクセル単位で返ってくる)。
他にもフレーム毎で取得したいときのプロパティ(IsPinching, Width, Delta, Ratio)や認識できる画面領域(validArea)などは以前に書いたスワイプ、ロングタップと同じ仕様だ。実際にこれらスクリプトを同時に使用しても問題ないように作ってある。
プラットフォームに関しては、タッチデバイスとして Android と iOS しか考えてないので、必要あればプリプロセッサ(#if 文)に条件を追加して欲しい。これらは Input.touchCount が取得できるものに限られる。しかしこのスクリプトの場合、Update() での検出部分は Input.touchCount で囲まれているので、スマホで使うこと前提なら、プリプロセッサ(#if 文)をコメントアウトしても良いだろう(Input.touchCount は PCなどでは無効になるため)。
あと、コールバックイベントはインスペクタで設定(UnityEvent)するようにしてあるが、コードだけにしたいなら、Action を使ったコールバックや、直接 delegate でコールバックするように書き換えても良いだろう。もちろんそのままで UnityEvent.AddListener を使ってコードから追加する方法もある。自由に改造して使って欲しい(※Unity でのコールバック実装は以下を参照)。
・【Unity】【C#】UnityEvent, Action, delegate, interface でのコールバック実装方法とインスペクタでの登録
■ピンチでオブジェクトを拡大・縮小する
ここからは上記の「PinchInput」を使って、実際にオブジェクトの拡大・縮小の操作をやってみよう。このスクリプトは使い回しできるのでライブラリとして追加もしてある。考え方を理解すれば他のものに利用することも可能だろう。
|
using UnityEngine;
namespace FantomLib
{
/// <summary>
/// ピンチでスケールを変化させる(ローカルスケール)
/// 2018/01/09 Fantom (Unity 5.6.3p1)
/// http://fantom1x.blog130.fc2.com/blog-entry-288.html
///(使い方)
///・伸縮したい GameObject にアタッチして、インスペクタから PinchInput のコールバックを登録すれば使用可。
/// </summary>
public class PinchToScale : MonoBehaviour
{
public Transform target; //スケール変化させるオブジェクト
//Local Values
Vector3 startScale; //ピンチ開始時スケール
Vector3 initScale; //起動初期スケール(リセット用)
// Use this for initialization
private void Start()
{
if (target == null)
target = gameObject.transform; //指定がないときは自身を対象とする
initScale = target.localScale;
}
//width: ピンチ幅, center: ピンチの2本指の中心の座標
public void OnPinchStart(float width, Vector2 center)
{
if (target != null)
startScale = target.localScale;
}
//width: ピンチ幅, delta: 直前のピンチ幅の差, ratio: ピンチ幅の開始時からの伸縮比(1:ピンチ開始時, 1以上拡大, 1より下(1/2,1/3,...)縮小)
public void OnPinch(float width, float delta, float ratio)
{
if (target != null)
target.localScale = startScale * ratio;
}
//スケールを元に戻す
public void ResetScale()
{
if (target != null)
target.localScale = initScale;
}
}
}

仮に拡大・縮小するオブジェクトを「Cube」としたとき、インスペクタでの設定は上のようにしよう。スクリプト自体は他の GameObject にアタッチしても構わないが、「PinchToScale」の「Target」には対象となるオブジェクトを指定し、「PinchInput」のピンチ開始時のコールバック「OnPinchStart」に「PinchToScale.OnPinchStart」を登録(Dynamicの方)、ピンチ中のコールバック「OnPinch」には「PinchToScale.OnPinch」を登録(Dynamicの方)する。あとは「PinchInput」の「isNormalize」がオンになっているのを確認すればOKだ(デフォルト=オン)。
スクリプトの簡単な解説をしておくと、ピンチ開始時のコールバック(OnPinchStart)のときのピンチ幅(指と指の距離)を記録し、ピンチ中は指の開閉による距離の比率(指を開くと 2, 3, 4...倍[小数も含む]となり、指を閉じると 1/2, 1/3, 1/4,..倍[実際には小数=0.5, 0.3, 0.25,...など])で拡大・縮小を行っている。この操作には主にコールバック引数の「ratio」(比率)を使っている。また、スケール操作はローカルスケール(transform.localScale)になる。ただ1つだけ注意して欲しいのは、物理的に指を3倍、4倍と開くのはキツイけど、指を1/3, 1/4 倍と閉じるのは簡単だということだ。指の間隔を比率ではなく、線形的な差分にしたいなら、コールバック引数の「delta」(距離差分)を使っても良いが、実際に試してみると ratio(比率)の方が人間の感覚・見た目には合ってるようだ。線形でやりたい場合はカーブを間に挟んだ方が良い感じになるかも知れない。その辺りは自分で工夫してみて欲しい。
■ピンチでカメラの遠近を操作する
次に上記の「PinchInput」を使って、視点となるオブジェクトの遠近操作をやってみよう。このスクリプトは使い回しできるのでライブラリとして追加もしてある。考え方を理解すれば他のものに利用することも可能だろう。
|
using System;
using UnityEngine;
namespace FantomLib
{
/// <summary>
/// ピンチで距離を操作する
/// 2018/01/09 Fantom (Unity 5.6.3p1)
/// http://fantom1x.blog130.fc2.com/blog-entry-288.html
///(使い方)
///・カメラなどの GameObject にアタッチして、インスペクタから PinchInput のコールバックを登録すれば使用可。
///・距離は target からの直線距離となる。
/// </summary>
public class PinchToDistance : MonoBehaviour
{
public Transform target; //視点となるオブジェクト
public float speed = 2f; //変化速度
public float minDistance = 1.0f; //近づける最小距離
public bool lookAt = true; //オブジェクトの方を向く
//LocalValues
float initDistance; //起動初期距離(リセット用)
// Use this for initialization
private void Start()
{
if (target != null)
{
Vector3 dir = target.position - transform.position;
initDistance = dir.magnitude;
if (lookAt)
transform.LookAt(target.position);
}
}
//width: ピンチ幅, delta: 直前のピンチ幅の差, ratio: ピンチ幅の開始時からの伸縮比(1:ピンチ開始時, 1以上拡大, 1より下(1/2,1/3,...)縮小)
public void OnPinch(float width, float delta, float ratio)
{
if (target == null)
return;
Vector3 dir = target.position - transform.position;
float distance = Math.Max(minDistance, dir.magnitude - delta * speed);
Vector3 pos = target.position - dir.normalized * distance;
transform.position = pos;
if (lookAt)
transform.LookAt(target.position);
}
//初期の距離に戻す
public void ResetDistance()
{
if (target == null)
return;
Vector3 dir = target.position - transform.position;
Vector3 pos = target.position - dir.normalized * initDistance;
transform.position = pos;
if (lookAt)
transform.LookAt(target.position);
}
}
}

仮に視点先となるオブジェクトを「Cube」としたとき、インスペクタでの設定は上のようにしよう。スクリプト自体は他の GameObject にアタッチしても構わないが、「PinchToDistance」の「Target」には対象となるオブジェクトを指定し、「PinchInput」のピンチ開始時のコールバック「OnPinchStart」に「PinchToDistance.OnPinchStart」を登録(Dynamicの方)、ピンチ中のコールバック「OnPinch」には「PinchToDistance.OnPinch」を登録(Dynamicの方)する。あとは「PinchInput」の「isNormalize」がオンになっているのを確認すればOKだ(デフォルト=オン)。
スクリプトの簡単な解説をしておくと、ピンチ開始時のコールバック(OnPinchStart)は使ってないのだが、ピンチ中は指の開閉による直線の距離の差分(指を開くと正の値が、指を閉じると負の値が返る)で遠近操作を行っている。この操作には主にコールバック引数の「delta」(距離差分)を使っていて、直前の指と指の距離との差を利用し、オブジェクトとの距離を加減算することによって、カメラを移動する感じだ(ただし指を開いたときに近づくには距離をマイナス、指を閉じたときは遠くにするには距離をプラスなので、符号は逆)。これは「ratio」(比率)とは違って線形な値になるので、指を開くときも閉じるときも同じような相対量になる(「ratio」(比率)の解説は PinchToScale を参照)。
具体的な値については「isNormalize」がオンになっているときは画面幅で正規化(widthReference=falseのときは画面の高さで正規化)された値が返り、オフになっているときはピクセル単位で返ってくる。それは例えば、画面解像度の幅が 800px の機種と 960px の機種があったとき、ピクセル単位だと画面の半分を移動したときそれぞれ、400px と 480px と異なった値になってしまうからだ。なので、端末の画面幅で割っておけば(=正規化する)どちらも 0.5 (= 400/800, 480/960)となり、画面解像度に依存しなくなる。しかし実際のピクセル座標で欲しいときは isNormalize=false にするか、正規化された値×解像度でもピクセル単位に変換できるので、好きな方を使うと良いだろう。

プラグインデモをダウンロード


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。
(関連記事)
【Unity】【C#】スワイプ(フリック)を判定、方向を取得してコールバックする
【Unity】【C#】長押し(ロングタップ)を取得してコールバックする
【Unity】【C#】UnityEvent, Action, delegate, interface でのコールバック実装方法とインスペクタでの登録
【Unity】【C#】インスペクタでの UnityEvent のコールバック登録の有無を調べる
【Unity】Androidのトーストやダイアログ、通知、音声認識、ハード音量操作など基本的な機能を使えるプラグインを作ってみた
- 関連記事
トラックバック
トラックバックURL
→http://fantom1x.blog130.fc2.com/tb.php/288-daec1dc8
この記事にトラックバックする(FC2ブログユーザー)
| h o m e |