エディタ拡張でプレハブを簡単に配置するツールを作ってみました。
エディタウィンドウのアイコンでプレハブを選択できます。左クリックした位置にプレハブをインスタンス化します。
配置ツール
プレハブと設定値が一つのデータにまとめられています。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();
}
}
}
}