【Unity】エディタ拡張でプレハブを配置するツールを作る

投稿者: | 2024-04-25

エディタ拡張でプレハブを簡単に配置するツールを作ってみました。

エディタウィンドウのアイコンでプレハブを選択できます。左クリックした位置にプレハブをインスタンス化します。

配置ツール

プレハブと設定値が一つのデータにまとめられています。EditorWindowには、現在選択中のプレハブの設定が表示されます。

設定を変更することで、プレハブの配置位置や回転角度、スケールをカスタマイズできます。

高さ調節
ランダム回転
固定回転
ランダム拡大縮小
固定拡大縮小

プレハブは面の垂直方向に自動的に整列します。

ScriptableObject

配置するプレハブのデータはScriptableObjectに保存します。

using UnityEngine;

[CreateAssetMenu(fileName = "PrefabPlacerData", menuName = "ScriptableObjects/PrefabPlacerData", order = 1)]
public class PrefabPlacerData : ScriptableObject
{
    [SerializeField] GameObject prefab;
    [SerializeField] float offset;
    [SerializeField] bool randomRotation;
    [SerializeField] float fixedRotationAngle;
    [SerializeField] bool applyRandomScale;
    [SerializeField] Vector2 randomScaleRange = new Vector2(1f, 2f);
    [SerializeField] float fixedScale = 1f;

    public GameObject Prefab => prefab;
    public float Offset { get => offset; set => offset = value; }
    public bool RandomRotation { get => randomRotation; set => randomRotation = value; }
    public float FixedRotationAngle { get => fixedRotationAngle; set => fixedRotationAngle = value; }
    public bool ApplyRandomScale { get => applyRandomScale; set => applyRandomScale = value; }
    public Vector2 RandomScaleRange { get => randomScaleRange; set => randomScaleRange = value; }
    public float FixedScale { get => fixedScale; set => fixedScale = value; }
}

データのリストを持つScriptableObjectも作ります。エディタウィンドウにアタッチして使います。

using UnityEngine;

[CreateAssetMenu(fileName = "PrefabPlacerDataCollection", menuName = "ScriptableObjects/PrefabPlacerDataCollection", order = 1)]
public class PrefabPlacerDataCollection : ScriptableObject
{
    [SerializeField] PrefabPlacerData[] list;

    public PrefabPlacerData[] List => list;
}

エディタウィンドウの状態を保存

エディタウィンドウの状態を保存できるようにします。専用のScriptableSingletonの派生クラスで行います。

using UnityEditor;
using UnityEngine;

[FilePath("SomeSubFolder/PrefabPlacerSettings.foo", FilePathAttribute.Location.ProjectFolder)]
public class PrefabPlacerSettings : ScriptableSingleton<PrefabPlacerSettings>
{
    [SerializeField]
    PrefabPlacerDataCollection collection;
    public PrefabPlacerDataCollection Collection => collection;
    public void SavePrefabCollection(PrefabPlacerDataCollection collection)
    {
        if (this.collection == collection) return;

        this.collection = collection;

        Save(true);
    }
}

エディタウィンドウを作成

EditorフォルダにC#スクリプトを作り、EditorWindowクラスの派生クラスを作成します。プレハブデータのリストや選択中のデータのインデックスが定義されています。

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

public class PrefabPlacer : EditorWindow
{

    PrefabPlacerDataCollection dataCollection;
    private int selectedIndex = 0;
    public bool PlacingPrefab { get; private set; } = false;
    string ButtonText => PlacingPrefab ? "Stop Placing" : "Place Prefab";

    [MenuItem("Window/Prefab Placer")]
    public static void ShowWindow()
    {
        GetWindow<PrefabPlacer>("Prefab Placer");
    }

OnEnableメソッドとOnDisableメソッドで、SceneView.duringSceneGuiイベントを登録/解除して、シーンビューでの操作を追加できるようにします。作成したScriptableSingletonのインスタンスを使って、データの取得や保存をしています。

    private void OnEnable()
    {
        dataCollection = PrefabPlacerSettings.instance.Collection;
        SceneView.duringSceneGui += OnSceneGUI;
   
    }
    void OnDisable()
    {
        // スクリプタブルシングルトンにデータを保存
        PrefabPlacerSettings.instance.SavePrefabCollection(dataCollection);
        
        SceneView.duringSceneGui -= OnSceneGUI;

        PlacingPrefab = false;
    }

EditorWindowの内容をOnGuiメソッドに実装します。データリストのScriptableObjectをアタッチするためのオブジェクトフィールドを表示しています。アタッチされていなければ以降は何も表示しません。

    void OnGUI()
    {
        // シーンビューが非表示のとき
        if (SceneView.lastActiveSceneView == null)
        {
            PlacingPrefab = false;
        }

        EditorGUILayout.BeginVertical();

        dataCollection = (PrefabPlacerDataCollection)EditorGUILayout.ObjectField("Collection ", dataCollection, typeof(PrefabPlacerDataCollection), false);

        // アタッチされていないとき
        if(dataCollection == null)
        {
            PlacingPrefab = false;
            EditorGUILayout.EndVertical();
            return;
        }

        if(selectedIndex > dataCollection.List.Length - 1)
        {
            selectedIndex = dataCollection.List.Length - 1;
        }

データを一つずつ見ていき、AssetPreview.GetAssetPreviewメソッドでプレハブのプレビュー画像(Texture2D)を取得します。ボタンを表示した後、同じ場所にプレビューを表示します。テクスチャ画像の表示には、GUI.DrawTextureWithTexCoordsメソッドを使います。

        using (var horizontalScope = new EditorGUILayout.HorizontalScope("box"))
        {
            for (int i = 0; i < dataCollection.List.Length; i++)
            {
                var data = dataCollection.List[i];

                // プレハブのプレビューを取得
                var previewImage = AssetPreview.GetAssetPreview(data.Prefab);

                if (GUILayout.Button("", GUILayout.Width(60), GUILayout.Height(60)))
                {
                    // ボタンがクリックされたらインデックスを更新
                    selectedIndex = i;
                }

                // ボタンのRectを取得
                var rect = GUILayoutUtility.GetLastRect();

                // ボタン上にプレビューを表示
                if (previewImage != null)
                    GUI.DrawTextureWithTexCoords(rect, previewImage, new Rect(0f, 0f, 1f, 1f));

そのプレハブが選択されている場合は、さらにボタン上に薄くハイライトを表示しています。

                // 選択されている場合ハイライト
                if (i == selectedIndex)
                {
                    var color = Color.white;
                    color.a = 0.2f;

                    EditorGUI.DrawRect(rect, color);
                }
            }
        }

アイコンが並ぶ下に、選択中のデータの値を表示しています。フィールドで値を変更できます。

        // 選択中のプレハブデータ
        var selectedData = dataCollection.List[selectedIndex];

        if (selectedData != null)
        {
            EditorGUI.BeginChangeCheck();

            // プレハブのデータを表示
            EditorGUILayout.ObjectField("Prefab ", selectedData.Prefab, typeof(GameObject), false);
            var offset = EditorGUILayout.FloatField("Offset", selectedData.Offset);

            var randomRotation = EditorGUILayout.Toggle("Random Rotation", selectedData.RandomRotation);
            var fixedRotationAngle = EditorGUILayout.FloatField("Fixed Rotation Angle", selectedData.FixedRotationAngle);
            var applyRandomScale = EditorGUILayout.Toggle("Apply Random Scale", selectedData.ApplyRandomScale);
            var randomScaleRange = EditorGUILayout.Vector2Field("Random Scale Range", selectedData.RandomScaleRange);

            var fixedScale = EditorGUILayout.FloatField("Fixed Scale", selectedData.FixedScale);

フィールドが変更されたら、元に戻せるようにしてからデータの値を変更して保存します。

            if (EditorGUI.EndChangeCheck())
            {
                // 変更点を記録
                Undo.RecordObject(selectedData, "Change " + selectedData.name);

                // データの値を変更
                selectedData.Offset = offset;
                selectedData.RandomRotation = randomRotation;
                selectedData.FixedRotationAngle = fixedRotationAngle;
                selectedData.ApplyRandomScale = applyRandomScale;
                selectedData.RandomScaleRange = randomScaleRange;
                selectedData.FixedScale = fixedScale;

                // 保存
                EditorUtility.SetDirty(selectedData);
                AssetDatabase.SaveAssets();
            }

EditorWindowの最後にモードを変更するボタンを表示します。プレハブを配置できるかどうかを切り替えられます。

            // ボタンを押すとモードを変更
            if (GUILayout.Button(ButtonText))
            {
                TogglePlacingMode();
            }

        }

        EditorGUILayout.EndVertical();
    }

    public void TogglePlacingMode()
    {
        PlacingPrefab = !PlacingPrefab;
    }

プレハブを配置

プレハブを配置するときは、他のゲームオブジェクトの操作を無効にします。左クリックでレイを投げ、ヒットした地点にプレハブをインスタンス化します。その後、設定に基づいて回転やスケール、位置を設定しています。

    void OnSceneGUI(SceneView sceneView)
    {
        if (dataCollection == null) return;

        if (PlacingPrefab && dataCollection.List.Length > 0 && selectedIndex < dataCollection.List.Length)
        {
            // 他のゲームオブジェクトを操作できないようにする
            HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));


            // 左クリックしたとき
            if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
            {

                // レイを投げる
                Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit))
                {
                    // 選択中のデータ
                    var data = dataCollection.List[selectedIndex];

                    // プレハブをインスタンス化
                    GameObject prefabInstance = PrefabUtility.InstantiatePrefab(data.Prefab) as GameObject;

                    // 元に戻せるようにする
                    Undo.RegisterCreatedObjectUndo(prefabInstance, "create " + prefabInstance.name);

                    if (prefabInstance != null)
                    {

                        // 回転を設定
                        var rot = prefabInstance.transform.rotation;
                        rot = Quaternion.FromToRotation(Vector3.up, hit.normal) * rot;

                        rot = Quaternion.AngleAxis(data.RandomRotation ? Random.Range(0, 360f) : data.FixedRotationAngle, hit.normal) * rot;

                        prefabInstance.transform.rotation = rot;

                        // スケールを設定
                        var scale = data.ApplyRandomScale ? Random.Range(data.RandomScaleRange.x, data.RandomScaleRange.y) : data.FixedScale;

                        prefabInstance.transform.localScale *= scale;

                        // スケールに基づいてオフセットを調整
                        float adjustedOffset = data.Offset * scale;

                        // 位置を設定
                        prefabInstance.transform.position = hit.point + hit.normal * adjustedOffset;

                    }
                }


                Event.current.Use();
            }
        }
    }

これでプレハブを簡単に配置できるようになりました。

スクリプト

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

public class PrefabPlacer : EditorWindow
{

    PrefabPlacerDataCollection dataCollection;
    private int selectedIndex = 0;
    public bool PlacingPrefab { get; private set; } = false;
    string ButtonText => PlacingPrefab ? "Stop Placing" : "Place Prefab";

    [MenuItem("Window/Prefab Placer")]
    public static void ShowWindow()
    {
        GetWindow<PrefabPlacer>("Prefab Placer");
    }

    private void OnEnable()
    {
        dataCollection = PrefabPlacerSettings.instance.Collection;
        SceneView.duringSceneGui += OnSceneGUI;
   
    }
    void OnDisable()
    {
        // スクリプタブルシングルトンにデータを保存
        PrefabPlacerSettings.instance.SavePrefabCollection(dataCollection);
        
        SceneView.duringSceneGui -= OnSceneGUI;

        PlacingPrefab = false;
    }

    void OnGUI()
    {
        // シーンビューが非表示のとき
        if (SceneView.lastActiveSceneView == null)
        {
            PlacingPrefab = false;
        }

        EditorGUILayout.BeginVertical();

        dataCollection = (PrefabPlacerDataCollection)EditorGUILayout.ObjectField("Collection ", dataCollection, typeof(PrefabPlacerDataCollection), false);

        // アタッチされていないとき
        if(dataCollection == null)
        {
            PlacingPrefab = false;
            EditorGUILayout.EndVertical();
            return;
        }

        if(selectedIndex > dataCollection.List.Length - 1)
        {
            selectedIndex = dataCollection.List.Length - 1;
        }
        
        using (var horizontalScope = new EditorGUILayout.HorizontalScope("box"))
        {
            for (int i = 0; i < dataCollection.List.Length; i++)
            {
                var data = dataCollection.List[i];

                // プレハブのプレビューを取得
                var previewImage = AssetPreview.GetAssetPreview(data.Prefab);

                if (GUILayout.Button("", GUILayout.Width(60), GUILayout.Height(60)))
                {
                    // ボタンがクリックされたらインデックスを更新
                    selectedIndex = i;
                }

                // ボタンのRectを取得
                var rect = GUILayoutUtility.GetLastRect();

                // ボタン上にプレビューを表示
                if (previewImage != null)
                    GUI.DrawTextureWithTexCoords(rect, previewImage, new Rect(0f, 0f, 1f, 1f));

                // 選択されている場合ハイライト
                if (i == selectedIndex)
                {
                    var color = Color.white;
                    color.a = 0.2f;

                    EditorGUI.DrawRect(rect, color);
                }
            }
        }


        // 選択中のプレハブデータ
        var selectedData = dataCollection.List[selectedIndex];

        if (selectedData != null)
        {
            EditorGUI.BeginChangeCheck();

            // プレハブのデータを表示
            EditorGUILayout.ObjectField("Prefab ", selectedData.Prefab, typeof(GameObject), false);
            var offset = EditorGUILayout.FloatField("Offset", selectedData.Offset);

            var randomRotation = EditorGUILayout.Toggle("Random Rotation", selectedData.RandomRotation);
            var fixedRotationAngle = EditorGUILayout.FloatField("Fixed Rotation Angle", selectedData.FixedRotationAngle);
            var applyRandomScale = EditorGUILayout.Toggle("Apply Random Scale", selectedData.ApplyRandomScale);
            var randomScaleRange = EditorGUILayout.Vector2Field("Random Scale Range", selectedData.RandomScaleRange);

            var fixedScale = EditorGUILayout.FloatField("Fixed Scale", selectedData.FixedScale);


            if (EditorGUI.EndChangeCheck())
            {
                // 変更点を記録
                Undo.RecordObject(selectedData, "Change " + selectedData.name);

                // データの値を変更
                selectedData.Offset = offset;
                selectedData.RandomRotation = randomRotation;
                selectedData.FixedRotationAngle = fixedRotationAngle;
                selectedData.ApplyRandomScale = applyRandomScale;
                selectedData.RandomScaleRange = randomScaleRange;
                selectedData.FixedScale = fixedScale;

                // 保存
                EditorUtility.SetDirty(selectedData);
                AssetDatabase.SaveAssets();
            }

            // ボタンを押すとモードを変更
            if (GUILayout.Button(ButtonText))
            {
                TogglePlacingMode();
            }

        }

        EditorGUILayout.EndVertical();
    }

    public void TogglePlacingMode()
    {
        PlacingPrefab = !PlacingPrefab;
    }
    
    void OnInspectorUpdate()
    {
        Repaint();
    }

    void OnSceneGUI(SceneView sceneView)
    {
        if (dataCollection == null) return;

        if (PlacingPrefab && dataCollection.List.Length > 0 && selectedIndex < dataCollection.List.Length)
        {
            // 他のゲームオブジェクトを操作できないようにする
            HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));


            // 左クリックしたとき
            if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
            {

                // レイを投げる
                Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit))
                {
                    // 選択中のデータ
                    var data = dataCollection.List[selectedIndex];

                    // プレハブをインスタンス化
                    GameObject prefabInstance = PrefabUtility.InstantiatePrefab(data.Prefab) as GameObject;

                    // 元に戻せるようにする
                    Undo.RegisterCreatedObjectUndo(prefabInstance, "create " + prefabInstance.name);

                    if (prefabInstance != null)
                    {

                        // 回転を設定
                        var rot = prefabInstance.transform.rotation;
                        rot = Quaternion.FromToRotation(Vector3.up, hit.normal) * rot;

                        rot = Quaternion.AngleAxis(data.RandomRotation ? Random.Range(0, 360f) : data.FixedRotationAngle, hit.normal) * rot;

                        prefabInstance.transform.rotation = rot;

                        // スケールを設定
                        var scale = data.ApplyRandomScale ? Random.Range(data.RandomScaleRange.x, data.RandomScaleRange.y) : data.FixedScale;

                        prefabInstance.transform.localScale *= scale;

                        // スケールに基づいてオフセットを調整
                        float adjustedOffset = data.Offset * scale;

                        // 位置を設定
                        prefabInstance.transform.position = hit.point + hit.normal * adjustedOffset;

                    }
                }


                Event.current.Use();
            }
        }
    }
}

コメントを残す

メールアドレスが公開されることはありません。