プログラミングで世界を変える

ゲームプログラミングと技術のこと

UnityのuGUIでピンチ動作によってオブジェクトの拡大縮小をさせるスクリプト(モバイル対応)

こんな感じで動作するスクリプトを書きました。
使用用途としては、画像の拡大縮小を行うビューや、牧場系ゲームなどを想定しています。

f:id:splas_boomerang:20150709000851g:plain

iOS/Androidは実機で動作確認済みで、ピンチによって拡大縮小が可能です。
Editor上の場合は、上下キーの操作によって拡大縮小が可能です。

拡大縮小をしすぎた場合は適切な大きさに戻るようになっています。
もちろんスクロールによる移動も可能です。

こちらが今回のスクリプトを使ったサンプルです。 github.com

使い方

1. オブジェクトの配置

まずはヒエラルキービューにこんな感じでオブジェクトを配置しましょう。アタッチするコンポーネント等は下記に示します。
f:id:splas_boomerang:20150709001704p:plain

参照等は全てインスペクター上で設定するようにしているので、オブジェクト名はなんでもいいです。

ScrollView

f:id:splas_boomerang:20150709002253p:plain

RectTransformのWidth,Heightはビュー(見える範囲)の大きさを指定。サンプルではキャンバスサイズをiPhone 5 wide (1136*640)にしてあるので、一回り小さいサイズを指定しました。ScrollRectのContentにはヒエラルキービューよりContentをドラッグし、設定しておきましょう。
適当なImageでMaskをかけるのを忘れずに。

Content

f:id:splas_boomerang:20150709002256p:plain

ContentSizeFitterはどちらもPreferredSizeを指定。GridLayoutGroupのCellSizeはスクロールするオブジェクトの大きさを指定しましょう(サンプルでは前面のUnityちゃんの画像サイズを指定)。

paddingに関してはこんな感じで、端までスクロールした時の余白を表しています。
f:id:splas_boomerang:20150709003538p:plain

Wrapper

f:id:splas_boomerang:20150709002301p:plain

LayoutElementをアタッチするだけで大丈夫です。Anchorsの値は一度Playすると勝手に変わってしまうみたいです。

Wrapper内のオブジェクト

拡大縮小を適用したいオブジェクトを配意しましょう。この時、オブジェクトのAnchorsが常にこの状態になるようにしてください。
f:id:splas_boomerang:20150709002307p:plain

Anchorsを自由に設定したい場合、Wrapperの下に同じ大きさのWrapperを作ることでそれ以下の階層では自由に配置可能です。

PinchScalingManager

f:id:splas_boomerang:20150709002310p:plain

スクリプト自体は適当なオブジェクトにアタッチすれば大丈夫です。ContentとWrapperは対応する名前のオブジェクトを付加しておいてください。

2. パラメータの設定

上記、PinchScalingManagerのインスペクター上でパラメータを調整できます。

Scaleは拡大率を示し、例えば2を設定しておくと2倍された状態で開始されます。

RangeScaleは拡大率を変更可能な範囲を示し、RangeLimitedScaleは収束する拡大率の範囲を示します。分かりづらいのですが、収束させる拡大率をRangeLimitedScaleで設定し、ピンチ中のみ変更出来る範囲(遊びの拡大率)をRangeScaleで設定するといったイメージです。RangeScaleは必ずRangeLimitedScaleを含む範囲になるように設定してください。

イメージとしてはこんな関係になるように。
f:id:splas_boomerang:20150709010537p:plain

TweenSecondは、収束するまでにかかる秒数を示しています。

3. Playで動作確認

サンプルを参考に色々試してみてください。
実機の場合、Editor上と大きく操作感が異なるので、可能であれば確認してみると良いと思います。

実装

ソースコード

制御の流れ

基本的な制御としては以下の流れとなります。
1. Update()で入力開始を感知し、ピンチ処理の初期化をする。
2. 入力に合わせて拡大率を変更し、状態を更新する。
3. 入力終了を感知し、拡大率が収束範囲外であれば収束させる。

ピンチ入力の取得

//初期化時
float distance = Vector2.Distance (Input.touches [0].position, Input.touches [1].position);
max_distance = distance / scale;

//更新時
float distance = Vector2.Distance (Input.touches [0].position, Input.touches [1].position);
SetNewScale (distance / max_distance);

タッチ入力は、Input.touches[0]が1番目に認識された指、Input.touches[1]が2番目に認識された指といった形で取得出来ます。

ピンチ入力の取得は、ピンチ開始時の2入力間の距離を基準値とし、例えば指の距離が2倍になれば、開始時の拡大率が相対的に2倍になるといった実装にしました。体感として、iOS標準の拡大縮小のような自然な操作感を実現できました。

拡大率を収束させる処理

 /// <summary>
/// 拡大率を設定された値に収束させる
/// </summary>
IEnumerator TweenLimitedScale(float limited_scale){

        if (scale == limited_scale)
                yield break;

        float timer = 0;
        float def_scale = scale - limited_scale;

        //scaleをTweenSecond秒以内にlimited_rateにする
        while(timer < TweenSecond){
                timer += Time.deltaTime;
                scale -= def_scale * Time.deltaTime * (1f / TweenSecond);
                UpdateScaling ();
                yield return 0;
        }

}

ピンチ入力の終了時には、収束すべき拡大率を計算し、一定時間で収束させるコルーチンを呼び出します。実装は「現在の拡大率」と「収束すべき拡大率」との差を求め、TweenSecond秒以内に「現在の拡大率」に差を減算するという処理を行っています。

拡大縮小時によって変化するScrollViewの視点

//ピンチ開始時
center = contentRect.localPosition / scale;

//更新時
contentRect.localPosition = center * scale;

ScrollViewの視点はContentの座標とのずれで表現されるますが、このずれは絶対的な値で指定されるため、そのまま拡大縮小を行うと、コンテンツの座標とずれてしまいます。そのため、拡大縮小時の開始時にContentの座標とのずれをビューの中心座標として保存し、拡大率に応じてずれを更新する必要があります。サンプルでは、ずれが発生しないことを確認するため、ScrollViewに固定されたスコープを用意してあります。

課題

収束範囲外の時の挙動について、一定の割合で収束する実装が少し体感とずれている印象を受けました。

ピンチの挙動についても、ピンチ中でも移動は出来た方がいいのか、拡大縮小時の中心座標は指の間にすべきなのか、など気になっています。

もし実装や仕様について何かあれば教えて下さい。