Roba Memo - 素人のUnity覚書と奮闘記

素人のUnity覚書と奮闘記

カメラを領域内でドラッグ移動させたい&カメラのズーム処理(2D)

やりたいこと

  • スプライトの領域内でカメラをドラッグで移動させたい。
  • ズーム機能をつけたい。

ちょっと見難いけど、こんな感じ。

ドラッグ処理

1:カメラにコライダーを追加する

Rayを飛ばして当たり判定をとりたいので、カメラにコライダーを追加する。
このコライダーがドラッグ領域になる。
今回は、カメラ領域からUI部分を除いた部分をドラッグ領域とした。
また、他のオブジェクトのコライダーを優先的に感知したいので、一番最下層にくるように配置。
f:id:nico-taniku:20181122104555p:plain:h200 f:id:nico-taniku:20181122104829p:plain:h200
f:id:nico-taniku:20181122105635p:plain:w300

カメラがX90度回転している理由

キャラクターをNavMeshを使って移動させているので、XZ方向に動くキャラクターを上から撮るためにカメラを回転させている。

2:スプライトを配置

ドラッグの領域となるスプライトを配置する。
これは、スクリプトからサイズを取得するだけなので、特にコライダーなどは必要なし。
座標も好きなところに配置でOK。
f:id:nico-taniku:20181122114629p:plain:h300

3:Rayを飛ばすクラスを作る

方法はいろいろあると思うが、今回は他でもRay飛ばして感知したいコライダーがあるので、使い回しできるようにRay用のクラスも作成して、ステージに配置。
f:id:nico-taniku:20181122115930p:plain:w350

using UnityEngine;

public class RayHitMouse : MonoBehaviour
{
    [HideInInspector] public GameObject hit;


    public GameObject Ray()
    {
        RaycastHit hit_info = new RaycastHit();

        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        float max_distance = 15f;
        var layer = LayerMask.NameToLayer("SpriteClick");
        var layer2 = LayerMask.NameToLayer("UI");
        int layer_mask = 1 << layer2 | 1 << layer;
        var is_hit = Physics.Raycast(ray, out hit_info, max_distance, layer_mask);
        // Debug.DrawRay(ray.origin, ray.direction * max_distance, Color.red, 5, false);

        if (is_hit)
        {
            hit = hit_info.transform.gameObject;
        }
        else
        {
            hit = null;
        }

        return hit;
    }

}

SpriteClickとUIレイヤーのみ感知するようにしてある。
レイヤーを作成し、カメラのレイヤーをSpriteClickに変更する。
f:id:nico-taniku:20181122120711p:plain:w350
※Rayを飛ばす距離を15fと短めにしてるので足りなかったら適宜変更すること。

4:ヒット判定とドラッグ判定

カメラをドラッグするクラスを作成し、カメラにアタッチしておく。
Input.GetMouseButtonDown(0)でヒット判定し、Input.GetMouseButton(0)でドラッグ判定をする。

using UnityEngine;

public class CameraDrag : MonoBehaviour
{

    RayHitMouse rayHitMouse;
    bool is_down;
    Vector3 mouseStartPos;

    void Awake()
    {
        rayHitMouse = GameObject.Find("RayHitMouse").GetComponent<RayHitMouse>();
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            OnTouchDown();

        }
        if (Input.GetMouseButton(0))
        {
            if (is_down && mouseStartPos != Input.mousePosition)
            {
                OnTouchMove();
            }
        }
        if (Input.GetMouseButtonUp(0))
        {
            is_down = false;
        }

    }

    public void OnTouchDown()
    {
        var hit = rayHitMouse.Ray();
        if (hit == gameObject)
        {
            is_down = true;
            mouseStartPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        }
    }

    public void OnTouchMove()
    {

    {
}
ドラッグ判定が2重チェックな理由

if (is_down && mouseStartPos != Input.mousePosition) この部分についての説明。
マウスの移動があったかどうかは、Down時のマウスの位置と現在のマウスの位置の差で判定できる。
mouseStartPos != Input.mousePosition ←この部分。
is_downの役割は、カメラにヒットしているかどうかを判定するためで、これをつけておかないと、マウスをドラッグするたびに感知してしまうことになる。

5:ドラッグ処理を追加する

OnTuchMoveメソッドにドラッグ処理を追加していく。
処理内容はシンプルで、マウスの移動量をカメラ座標に追加していくだけ。

    public void OnTouchMove()
    {
        //ドラッグ処理
        var mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        var moving_distance = mouseStartPos - mousePos;
        transform.position += moving_distance;
    }

ここまでで、ひとまずカメラをドラッグすることができる。

ドラッグ領域の設定

1:限界点を求める

カメラを端に寄せた時の座標を求める方法を考える。
スプライトとカメラの最初の座標が(0.0)のとき、(スプライトの横幅 - コライダーの横幅)/2となる。
スプライトが(0,0)以外の場合、スプライトの座標分をオフセットする。
f:id:nico-taniku:20181122185215p:plain:w600
このままでは、カメラの中心点とコライダーの中心点が異なる場合、ずれてしまう。
なので、コライダーのCenter座標もオフセットする。
f:id:nico-taniku:20181122185316p:plain:w300

    Vector2 contentSize;
    Vector2 contentOffset;
    BoxCollider cameraCollider;

    void Awake()
    {
        //つづけて追加
        var content = GameObject.Find("MapSprite").gameObject;
        var sr = content.GetComponent<SpriteRenderer>();
        contentSize = new Vector2(sr.bounds.size.x, sr.bounds.size.z);
        contentOffset = new Vector2(content.transform.position.x, content.transform.position.z);
        cameraCollider = GetComponent<BoxCollider>();
    }

    public void OnTouchMove()
    {
        //つづけて追加
        //コライダーオフセット
        var colliderOffset = new Vector2(cameraCollider.center.x, cameraCollider.center.y);

        //限界値の算出
        var limitR = (contentSize.x - cameraCollider.size.x) / 2 - colliderOffset.x + contentOffset.x;
        var limitL = (contentSize.x - cameraCollider.size.x) / -2 - colliderOffset.x + contentOffset.x;
        var limitT = (contentSize.y - cameraCollider.size.y) / 2 - colliderOffset.y + contentOffset.y;
        var limitB = (contentSize.y - cameraCollider.size.y) / -2 - colliderOffset.y + contentOffset.y;
    }

2:カメラ座標に設定する

スプライトのサイズがコライダーのサイズよりも小さい場合はドラッグする必要がないので、最初の座標で固定する。
限界点を超えたら、限界点で固定。

    Vector3 defCameraPosition;

    void Awake()
    {
        //つづけて追加
        defCameraPosition = Camera.main.transform.position;
    }

    public void OnTouchMove()
    {
        //つづけて追加
        //限界まで移動した場合の処理
        var posi = transform.position;

        if (contentSize.x > cameraCollider.size.x)
        {
            if (transform.position.x > limitR)
            {
                posi.x = limitR;
            }
            else if (transform.position.x < limitL)
            {
                posi.x = limitL;
            }
        }
        else
        {
            posi.x = defCameraPosition.x;
        }

        if (contentSize.y > cameraCollider.size.y)
        {
            if (transform.position.z > limitT)
            {
                posi.z = limitT;
            }
            else if (transform.position.z < limitB)
            {
                posi.z = limitB;
            }
        }
        else
        {
            posi.z = defCameraPosition.z;
        }
        transform.position = posi;
    }

ズームに対応させる

1:ズームボタンを作る

ズームの仕組みは単純で、カメラのorthographicSizeを変更するだけ。

using UnityEngine;

public class StageZoom : MonoBehaviour
{
    float def;
    float[] zoom = { 1.0f, 0.75f, 0.5f };
    int counter;

    void Awake()
    {
        def = Camera.main.orthographicSize;
    }

    public void OnZoom()
    {
        counter++;
        if (counter == zoom.Length)
        {
            counter = 0;
        }
        Camera.main.orthographicSize = def * zoom[counter];

        //位置調整
        Camera.main.GetComponent<CameraDrag>().OnTouchMove();

    }

}

この最後の位置調整で、先に作成したドラッグのメソッドを呼び出してある。 このボタンをCanvasに作成した場合UIレイヤーになるので、Rayで感知した場合はコライダーをつけておく。
そうするとボタンを押してる間はドラッグされないようになる。

2:倍率に合わせてコライダーもリサイズする

OnTouchMoveメソッドが呼び出されるので、そこに追加していく。 カメラをズームすると、カメラの領域が狭くなるのに対し、コライダーサイズはそのままなので、最初のコライダーサイズに倍率を掛けて調整する。
また、コライダーの位置もカメラの領域に合わせて調整する必要がある。ただし、Y軸(高さ)はそのままにしておきたいのでY軸だけは初期値に固定する。
ここで注意が必要なのは、コライダーのcenterはカメラに対してのローカル座標になるので、固定するのはz座標になる。
これをコライダーのオフセットの記述より前に追加する。

    Vector2 defColliderSize;
    Vector3 defColliderCenter;
    float defCameraOrthographicSize;

    void Awake()
    {
        defCameraOrthographicSize = Camera.main.orthographicSize;
        defColliderSize = cameraCollider.size;
        defColliderCenter = cameraCollider.center;
    }

    public void OnTouchMove()
    {
        //ズーム率
        var zoom = Camera.main.orthographicSize / defCameraOrthographicSize;
        //コライダーをズームに合わせる
        cameraCollider.size = defColliderSize * zoom;
        //z軸は固定(コライダーはカメラに対してのローカル座標)
        cameraCollider.center = new Vector3(defColliderCenter.x * zoom, defColliderCenter.y * zoom, defColliderCenter.z);
    }

3:ズームボタンから呼び出した時はドラッグしないようにする

ドラッグ処理のときにDown時にmouseStartPosにマウス座標を代入してあるが、ズームボタンから直接呼び出す場合は、移動する必要がないのでis_down=trueのときしかドラッグ処理をしないようにしておく。

    public void OnTouchMove()
    {

        if (is_down)
        {
            //ドラッグ処理
            var mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            var moving_distance = mouseStartPos - mousePos;
            transform.position += moving_distance;
        }

    }

CameraDrag の完成スクリプト

using UnityEngine;

public class CameraDrag : MonoBehaviour
{

    RayHitMouse rayHitMouse;
    bool is_down;
    Vector3 mouseStartPos;
    Vector2 contentSize;
    Vector2 contentOffset;
    BoxCollider cameraCollider;
    Vector3 defCameraPosition;
    Vector2 defColliderSize;
    Vector3 defColliderCenter;
    float defCameraOrthographicSize;

    void Awake()
    {
        rayHitMouse = GameObject.Find("RayHitMouse").GetComponent<RayHitMouse>();
        var content = GameObject.Find("MapSprite").gameObject;
        var sr = content.GetComponent<SpriteRenderer>();
        contentSize = new Vector2(sr.bounds.size.x, sr.bounds.size.z);
        contentOffset = new Vector2(content.transform.position.x, content.transform.position.z);
        cameraCollider = GetComponent<BoxCollider>();
        defCameraPosition = Camera.main.transform.position;
        defColliderSize = cameraCollider.size;
        defColliderCenter = cameraCollider.center;
        defCameraOrthographicSize = Camera.main.orthographicSize;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            OnTouchDown();

        }
        if (Input.GetMouseButton(0))
        {
            if (is_down && mouseStartPos != Input.mousePosition)
            {
                OnTouchMove();
            }
        }
        if (Input.GetMouseButtonUp(0))
        {
            is_down = false;
        }

    }

    public void OnTouchDown()
    {
        var hit = rayHitMouse.Ray();
        if (hit == gameObject)
        {
            is_down = true;
            mouseStartPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        }
    }

    public void OnTouchMove()
    {
        //ドラッグ処理
        if (is_down)
        {
            //マウスの移動量を加算
            var mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            var moving_distance = mouseStartPos - mousePos;
            transform.position += moving_distance;
        }

        //ズーム対応
        var zoom = Camera.main.orthographicSize / defCameraOrthographicSize;
        cameraCollider.size = defColliderSize * zoom;
        cameraCollider.center = new Vector3(defColliderCenter.x * zoom, defColliderCenter.y * zoom, defColliderCenter.z);

        //コライダーのオフセット
        var colliderOffset = new Vector2(cameraCollider.center.x, cameraCollider.center.y);

        //限界値の算出
        var limitR = (contentSize.x - cameraCollider.size.x) / 2 - colliderOffset.x + contentOffset.x;
        var limitL = (contentSize.x - cameraCollider.size.x) / -2 - colliderOffset.x + contentOffset.x;
        var limitT = (contentSize.y - cameraCollider.size.y) / 2 - colliderOffset.y + contentOffset.y;
        var limitB = (contentSize.y - cameraCollider.size.y) / -2 - colliderOffset.y + contentOffset.y;

        //限界まで移動した場合の処理
        var posi = transform.position;

        if (contentSize.x > cameraCollider.size.x)
        {
            if (transform.position.x > limitR)
            {
                posi.x = limitR;
            }
            else if (transform.position.x < limitL)
            {
                posi.x = limitL;
            }
        }
        else
        {
            posi.x = defCameraPosition.x;
        }

        if (contentSize.y > cameraCollider.size.y)
        {
            if (transform.position.z > limitT)
            {
                posi.z = limitT;
            }
            else if (transform.position.z < limitB)
            {
                posi.z = limitB;
            }
        }
        else
        {
            posi.z = defCameraPosition.z;
        }
        transform.position = posi;
    }

}

以上。
合ってるかわかんないけど、すごく長くなっちゃった。