Unityエディタ拡張でグリッドスナップ時に配置する機能を実装した件

自己紹介


始めまして。ゲーム開発系の専門学校に通う3年生のながせまるです。
初めての起稿となるため、拙い点があるかとおもいますが、何卒宜しくお願い致します。

本題

Unityの機能の一つであるエディタ拡張を使ってグリッドスナップ時にオブジェクトを配置する機能を作ったよ! ということで、ウキウキで記事を書いております。一部制限はありますが…

経緯

とあるコンテストに応募するゲーム作品を作るべく、プログラマー3人とデザイナー4人が立ち上がりました。そう、プランナー不在のチーム開発です。この際一番の障壁となるのが、ステージ制作です。
今までの開発ではプランナーが居たのでそう深刻に考えてはいなかったのですが、いざいないとなると本当、まあ、かなりつらい。
みんなコーディングしたいもんね。というわけでステージ制作をめっちゃ楽に、めっちゃ楽しくする機能を作ろうと思い奮起したよ。という話です。
3D空間で2Dのタイルマップのようなことをするのが最終目標。

制作物


グリッドスナップと配置時設置

一応Unityにもグリッドスナップ自体はあるんですよね。
作ってから気付きました。なんてこったい。Unityすごい。
ですが、移動時に設置してくれるものはなかったため、差別化出来ているでしょう。ということでここは一旦。詳しくはリファレンスの方をご覧ください。

回転なりメッシュ結合なり、現行の開発においておいて便利な機能を多数盛り込んであります。ですが今回はグリッドスナップ&配置に焦点を置いて解説していきます。その他便利機能については後々解説するかもしれないし、しないかもしれない。

やったこと

何はともあれコード全文です。
SceneビューにGUIを出して、ON/OFFの操作をさせるようにしています
何も処理をせず生成してしまうとヒエラルキー上が混雑してしまうことに加え、編集もしにくいため、親オブジェクトを生成し、その子にすることにより解決しています。
かなり無駄が多いと思いますが目を瞑っていただければ幸いです。

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

[CustomEditor(typeof(BoxCollider))]
public class GridSnapObject : Editor
{
    // グリッドスナップを行う
    bool _gridFlag           = false;
    // 移動時設置を行う
    bool _putFlag            = false;

    Vector3 _gridSnapVal = Vector3.zero;

    Vector3 _saveObjectPos = Vector3.zero;

    void OnSceneGUI()
    {
       // 選択対象を取得する
       BoxCollider box = target as BoxCollider;

       // BeginEndで挟むとScene上にGUILayoutでGUIを出せるようになる
       Handles.BeginGUI();

       _gridFlag           = GUILayout.Toggle(_gridFlag, "グリッドスナップ(G)", GUILayout.MaxWidth(170));

        // グリッドスナップ時以外表示する必要がない為
        if (_gridFlag)
        {
            _putFlag = GUILayout.Toggle(_putFlag, "移動時設置(I)", GUILayout.MaxWidth(170));
        }
 
     // キーボードを押した瞬間のみ切り替える
        Event ev = Event.current;
        if (ev.type == EventType.KeyDown)
        {
            if (ev.keyCode == KeyCode.G)
            {
                _gridFlag = !_gridFlag;
            }
            if (ev.keyCode == KeyCode.I)
            {
                _putFlag = !_putFlag;
            }
        }

         // どれぐらい移動させるかの指標として使う
         Matrix4x4 matrix = 
              Matrix4x4.identity
              * Matrix4x4.Scale(box.size)
              * Matrix4x4.Rotate(box.transform.rotation);

         // グリッドスナップを回転に対応させる
         _gridSnapVal = matrix.lossyScale;

       Handles.EndGUI();

        if (!_gridFlag) { return; }

        // 親,子オブジェクトの取得
        GameObject parent = null;
        // 子オブジェクト達を入れる配列の初期化
        Transform[] children = new Transform[box.transform.parent ? box.transform.parent.childCount : 0];

        if (box.transform.parent)
        {
            parent = box.transform.parent.gameObject;

            for (int i = 0; i < parent.transform.childCount; i++)
            {
                children[i] = parent.transform.GetChild(i);
            }
        }

        //グリッドの色
        Color color = Color.cyan * 0.7f;

        //グリッドの中心座標を覚えておく
        Vector3 orig = _saveObjectPos;

        const int num = 2;

        //グリッド描画
        for (int x = -num; x <= num; x++)
        {
            Vector3 pos = orig + Vector3.right * x * _gridSnapVal.x;
            Debug.DrawLine(pos + Vector3.up * (_gridSnapVal.x / 2), pos - Vector3.up * (_gridSnapVal.x / 2), color);
        }
        for (int y = -num; y <= num; y++)
        {
            Vector3 pos = orig + Vector3.up * y * _gridSnapVal.y;
            Debug.DrawLine(pos + Vector3.right * (_gridSnapVal.y / 2), pos - Vector3.right * (_gridSnapVal.y / 2), color);
        }
        for (int z = -num; z <= num; z++)
        {
            Vector3 pos = orig + Vector3.forward * z * _gridSnapVal.z;
            Debug.DrawLine(pos + Vector3.up * (_gridSnapVal.z / 2), pos - Vector3.up * (_gridSnapVal.z / 2), color);
        }

        
        Vector3 position = box.transform.position;
        
        //グリッドの位置にそろえる
        position.x = Mathf.Floor(position.x / _gridSnapVal.x) * _gridSnapVal.x;
        position.y = Mathf.Floor(position.y / _gridSnapVal.y) * _gridSnapVal.y;
        position.z = Mathf.Floor(position.z / _gridSnapVal.z) * _gridSnapVal.z;

        box.transform.position = position;

        if (_putFlag)
        {
            // 元の場所にオブジェクトを設置する
            if (!CheckObjectPosition(children, box.gameObject, parent, _saveObjectPos))
            {
                Undo.RecordObject(box.gameObject, "Snap");
                SpawnObject(box.gameObject, parent, _saveObjectPos);
            }
        }

        _saveObjectPos = box.gameObject.transform.position;

        //Sceneビュー更新
        EditorUtility.SetDirty(box);

    }


    // オブジェクトの重なりを検知する
    public static bool CheckObjectPosition(Transform[] children,GameObject selectObject, GameObject parent,Vector3 putPosition)
    {
        bool flag = false;

        // 既にその位置に子オブジェクトがあれば処理を行わない
        if (selectObject.transform.parent)
        {
            for (int k = 0; k < parent.transform.childCount; k++)
            {
                if ((putPosition - children[k].position).sqrMagnitude <= 0.0001f) { flag = true; }
            }
        }

        return flag;
    }

    // オブジェクトを配置する
    public static GameObject SpawnObject(GameObject selectObject,GameObject parent,Vector3 position)
    {
        if (parent)
        {
            Undo.RecordObject(parent, "SpawnObject");
        }

        // 無い場合は親を生成する
        if (!selectObject.transform.parent)
        {
            parent = new GameObject(selectObject.name + "'s");

            parent.transform.position = Vector3.zero;

            selectObject.name = parent.transform.childCount.ToString();

            selectObject.transform.SetParent(parent.transform, true);
        }

        // 新しく生成したオブジェクトを親の子にする
        GameObject child = Instantiate(selectObject, position, selectObject.transform.rotation);

        // 名前の変更
        child.name = parent.transform.childCount.ToString();

        Undo.SetTransformParent(child.transform, parent.transform,"SnapParent");
        Undo.RegisterCreatedObjectUndo(child,"Instantiate_SnapChild");

        return child;
    }


}

以上になります。
グリッドのマス目の大きさを取得したかったため、ローカルサイズでのコライダーの大きさを返すBoxCollider.sizeからマス目を設定しています。現段階の開発ではその他コライダーの需要が無かったため、このような形を取っています。
もちろん多分に改良点はありますし、Undoの挙動も少し怪しいところがあります。

以上です。
また何かあれば執筆してみたいと思います。
駄文失礼しました。


この記事が気に入ったらサポートをしてみませんか?