いぬおさんのおもしろ数学実験室

おいしい紅茶でも飲みながら数学、物理、工学、プログラミング、そして読書を楽しみましょう

UnityでUIにピンチイン、ピンチアウトを実装する

 前回、UIをドラッグするコードを紹介しました。UIとしてはPanelを相手にしていますが、他でも同じだと思います。
www.omoshiro-suugaku.com
今回はそれに続けて、Panelにピンチイン、ピンチアウトを実装します。ピンチインとは2本の指で画面の一部をつまむような動作です。場面によりますが、表示の倍率を下げる(小さくする)ときの動作です。ピンチアウトは2本の指で画面に触れ、指を開く動作です。表示を拡大するときに使います。
 ネットには参考になるコードがいくつもあります。しかしなかなか思ったような動作のものが見つからず、自分で書いてみました。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    using UnityEngine.UI;

    float dist0 = 0f;
    float dist1 = 0f;
    float scale = 0f;
    float oldDist = 0f;//前回の2点間の距離
    float minRate = 0.7f;
    float maxRate = 3f;
    Vector3 v = Vector3.zero;
    //-----------------------------------
    void Start()
    {
        //使わない
    }
    //-----------------------------------
    void Update()
    {
        if (Input.touchCount >= 2)
        {
            Touch t1 = Input.GetTouch(0);
            Touch t2 = Input.GetTouch(1);
            if (t2.phase == TouchPhase.Began)
            {
                dist0 = Vector2.Distance(t1.position, t2.position);
                oldDist = dist0;
            }
            else if (t1.phase == TouchPhase.Moved && t2.phase == TouchPhase.Moved) {
                dist1 = Vector2.Distance(t1.position, t2.position);
                if (dist0 < 0.001f || dist1 < 0.001f) {
                    return;
                }
                else {
                    v = transform.localScale;
                    scale = v.x;
                    scale += (dist1 - oldDist) / 200f;
                    if (scale > maxRate) { scale = maxRate; }
                    if (scale < minRate) { scale = minRate; }
                    oldDist = dist1;
                }
                transform.localScale = new Vector3(scale, scale, scale);
            }
        }
    }

 何点か注意しておきましょう。

if (Input.touchCount >= 2)

で2本指でタッチされたことを検出しています。

if (t2.phase == TouchPhase.Began)

は、ピンチイン(アウト)しようとして2本指で画面に触れた瞬間を判定します。このときの2点の間の距離を保存しておきます。

else if (t1.phase == TouchPhase.Moved && t2.phase == TouchPhase.Moved)

で2本指が画面に触れたまま、触れている2点が移動していることを検出します。間隔が離れたり、近くなったりしていることを判定するのです。ピンチイン(アウト)の最中、ここが繰り返し実行されます。今回の実行時の距離を調べ、前回の距離と比較してどの程度拡大・縮小すればいいのか計算しています。

if (dist0 < 0.001f || dist1 < 0.001f)

は念のため書いておきました。もしも指の間の距離が非常に短い場合、倍率が極端に大きくなるなど、悪いことが起こりそうなので一応入れた部分です。

scale += (dist1 - oldDist) / 200f;

は実験で200という数値を決めました。小さくすると指を少し動かしただけで拡大率が大きく変化してしまいます。

transform.localScale = new Vector3(scale, scale, scale);

場合によってはUIのスケールがx方向、y方向で異なっている(あえて違えている)ことがあるかも知れません。ここでは共通だと仮定してコーディングしています。

注意:
 これでピンチイン、ピンチアウトは実現できるのですが、実は前回の記事で扱ったドラッグのコードと今回のコードは共存できません。実験すると分かります。ドラッグは
OnBeginDrag(PointerEventData eventData)と
OnDrag(PointerEventData eventData)を書いて実現したのですが、さらにピンチイン、ピンチアウトを実装しようとして上記のコードを追加すると動きがヘンになります。要するに2本指を触れたり動かしたりしたときにこの関数も実行されてしまうのです。だからドラッグ、ピンチイン、ピンチアウトの処理を入れたければ別の方法をとらなければなりません。例えば次のようにするとよいです。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    using UnityEngine.UI;

    public Vector2 startPos;
    public Vector2 mouseStartPos;
    float dist0 = 0f;
    float dist1 = 0f;
    float scale = 0f;
    float oldDist = 0f;//前回の2点間の距離
    float minRate = 0.7f;
    float maxRate = 3f;
    Vector3 v = Vector3.zero;
    //-----------------------------------
    void Start()
    {
        //使わない
    }
    //-----------------------------------
    void Update()
    {
        if (Input.touchCount == 1)
        {
            Touch t1 = Input.GetTouch(0);
            if (t1.phase == TouchPhase.Began)
            {
                //移動したいUIオブジェクトの最初の位置
                startPos = transform.position;
                //ドラッグを開始したときのマウスの位置
                mouseStartPos = t1.position;
            }
            else if (t1.phase == TouchPhase.Moved)
            {
                //ドラッグ中のUIオブジェクトの位置
                transform.position
                        = startPos + (t1.position - mouseStartPos);
            }
        }
        if (Input.touchCount >= 2)
        {
            Touch t1 = Input.GetTouch(0);
            Touch t2 = Input.GetTouch(1);
            if (t2.phase == TouchPhase.Began)
            {
                dist0 = Vector2.Distance(t1.position, t2.position);
                oldDist = dist0;
            }
            else if (t1.phase == TouchPhase.Moved && t2.phase == TouchPhase.Moved) {
                dist1 = Vector2.Distance(t1.position, t2.position);
                if (dist0 < 0.001f || dist1 < 0.001f) {
                    return;
                }
                else {
                    v = transform.localScale;
                    scale = v.x;
                    scale += (dist1 - oldDist) / 200f;
                    if (scale > maxRate) { scale = maxRate; }
                    if (scale < minRate) { scale = minRate; }
                    oldDist = dist1;
                }
                transform.localScale = new Vector3(scale, scale, scale);
            }
        }
    }

つまり、
OnBeginDrag(PointerEventData eventData)、
OnDrag(PointerEventData eventData)
は使わず、同じ働きをするコードを上記のように

if (Input.touchCount == 1)

のところに書くのです。

 ふー、これでようやく何とかなりました。しかしドラッグもピンチイン、アウトも頻繁に使われる操作なのだから、こういうコードを書かなくても実現できるようになっているとプログラマは助かると思うんですが……。