📌

UnityのScene Viewからエリアを設定できるようにしたかった話

2023/03/13に公開

はじめに

ゲームでマップ上の特定の範囲に何かに使用したいことがよくあります。
例えばセーブができる範囲を指定してオーラを表示するなどです。

その範囲をあらかじめデータとして持っておく必要があり、座標で指定するのはとても大変そうだったので、UnityのScene Viewから位置をタップすることで指定できないかと思いました。
対応の途中で、うまくいかなかった部分があったので記事にしてみました。

最初に書いたコード

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public sealed class Test : MonoBehaviour
{
#if UNITY_EDITOR
    public List<Vector3> points = new();

    private void OnDrawGizmos()
    {
        // シーンのカメラを取得
        var sceneCamera = SceneView.currentDrawingSceneView.camera;

        // ウィンドウの左上には(0, 0)が返されます。右下は(Screen.width, Screen.height)
        Vector3 mousePosition = Event.current.mousePosition;

        // zを指定しないとrayが飛ばない
        mousePosition.z = 10;

        // スクリーンの点を通してカメラからカメラからレイを通します。
        // スクリーンスペースはピクセルで定義されます。画面の左下端は (0,0); 右上端は (pixelWidth -1,pixelHeight -1)。
        var ray = sceneCamera.ScreenPointToRay(mousePosition);
        if (Physics.Raycast(ray, out var hit))
        {
            var hitPoint = hit.point;
            if (!points.Contains(hitPoint)) points.Add(hitPoint);
        }

        // Listの地点にGizmoのSphereを表示
        Gizmos.color = Color.yellow;
        foreach (var point in points) Gizmos.DrawSphere(point, 1);

        // Listの地点同士を順番に線でつなぐ表示
        var pointsCount = points.Count;
        for (var i = 0; i < pointsCount; i++) {
            var start = points[i];
            var end = points[(i + 1) % pointsCount];
            Gizmos.DrawLine(start, end);
        }
    }
#endif
}

SceneのカメラからRayを飛ばして当たった箇所をpointsに覚えさせてGizmoで表示確認。
pointsの座標を出力させる予定でした。

位置がズレる画像
しかし予想と違い、Planeの位置をクリックしてもpointsには座標は入らず、画像の赤丸の位置らへんをクリックするとpointsに座標が入りました。

クリックした位置と座標の位置関係
動作を見る限り、Sceneの上下を反転させた箇所が反応しているようです。

なぜこうなるのかは、はっきりとは分からなかったのですが
Event.current.mousePositionが、左上が(0, 0)、右下が(Screen.width, Screen.height)
sceneCamera.ScreenPointToRayが、左下が(0, 0)、右上が(pixelWidth -1,pixelHeight -1)
と上下があべこべになっていることに起因してそうでした。

このままでは使えないので、Event.current.mousePositionの位置を上下反転させたりしようかと考えたのですが、HandleUtility.GUIPointToWorldRayを使うことでクリックした位置を取得できることがわかりました。

修正したコード

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public sealed class Test : MonoBehaviour
{
#if UNITY_EDITOR
    public List<Vector3> points = new();

    private void OnDrawGizmos()
    {
        // マウスのクリックがあったら処理
        // Sceneビューでクリックした地点からrayを飛ばして、TerrainColliderとぶつかったところの地点をListに覚えさせる
        if (Event.current != null && Event.current.type == EventType.MouseUp) {
            RaycastHit hit;
            var ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
            if (Physics.Raycast(ray, out hit))
            {
                var hitPoint = hit.point;
                if (!points.Contains(hitPoint)) points.Add(hitPoint);
            }
        }

        // Listの地点にGizmoのSphereを表示
        Gizmos.color = Color.yellow;
        foreach (var point in points) Gizmos.DrawSphere(point, 1);

        // Listの地点同士を順番に線でつなぐ表示
        var pointsCount = points.Count;
        for (var i = 0; i < pointsCount; i++) {
            var start = points[i];
            var end = points[(i + 1) % pointsCount];
            Gizmos.DrawLine(start, end);
        }
    }
#endif
}

さいごに

意外なところでつまづいて時間がかかりました・・・。
もし同様の現象で困っている方がいれば何かの参考になれば幸いです。

Happy Elements

Discussion