概要
動作確認環境: Unity 2022.3 LTS / Unity 6
「繰り返し作業が多くて時間がかかる…」「チーム内で作業を標準化したい…」「デザイナーやプランナーが使いやすいツールを作りたい…」
Unity開発では、このような課題に直面することがあります。エディタ拡張 は、Unity Editorの機能を拡張し、開発効率を向上させる技術です。カスタムウィンドウの作成、インスペクターのカスタマイズ、メニュー項目の追加など、プロジェクト固有のワークフローに対応したツールを作成できます。
Editorフォルダの配置
エディタ拡張のスクリプトは、必ず Editorフォルダ 内に配置する必要があります。
なぜEditorフォルダが必要なのか
- Unity Editorでのみ使用するコードを分離
- ビルド時に自動的に除外される
- エディタ専用のAPIを使用できる
- コンパイル順序の制御
配置場所
- プロジェクト内のどこにでも配置可能
- 一般的には
Assets/Editorに配置 - 複数のEditorフォルダを持つことも可能
- エディタ拡張で使用するリソース(画像、フォントなど)は
Editor Default Resourcesフォルダに配置
MenuItem:メニュー項目の追加
MenuItemは、Unity Editorのメニューバーに項目を追加する最もシンプルなエディタ拡張です。
基本的な使い方
using UnityEngine;
using UnityEditor;
public class MenuItemExample
{
[MenuItem("Tools/Do Something")]
static void DoSomething()
{
Debug.Log("Something!");
}
}
この例では、Tools > Do Somethingというメニュー項目が追加されます。
ショートカットキーの設定
[MenuItem("Tools/Do Something %g")] // Ctrl+G (Windows) / Cmd+G (Mac)
static void DoSomething()
{
Debug.Log("Something!");
}
ショートカットキーの記号:
%:Ctrl (Windows) / Cmd (Mac)#:Shift&:Alt_:キー(例:_gはG)
メニュー項目の有効/無効
[MenuItem("Tools/Do Something")]
static void DoSomething()
{
Debug.Log("Something!");
}
[MenuItem("Tools/Do Something", true)]
static bool ValidateDoSomething()
{
// 何かが選択されている場合のみ有効
return Selection.activeGameObject != null;
}
コンテキストメニューの追加
[MenuItem("CONTEXT/Transform/Reset Position")]
static void ResetPosition(MenuCommand command)
{
Transform transform = (Transform)command.context;
Undo.RecordObject(transform, "Reset Position");
transform.position = Vector3.zero;
}
重要: オブジェクトを変更する際は
Undo.RecordObject()でUndo操作に対応してください。Undo.RecordObject()を使用する場合、EditorUtility.SetDirty()は 不要 です(Undo自体が変更を追跡するため)。
EditorWindow:カスタムウィンドウの作成
EditorWindowは、独自のウィンドウを作成できる強力な機能です。
基本的な使い方
using UnityEngine;
using UnityEditor;
public class MyEditorWindow : EditorWindow
{
[MenuItem("Window/My Editor Window")]
static void Open()
{
GetWindow<MyEditorWindow>("My Editor Window");
}
void OnGUI()
{
GUILayout.Label("Hello, Editor Window!");
}
}
UI Toolkitを使用する場合(Unity 2021以降)
Unity 2022以降では、UI Toolkit がエディタ拡張の推奨UIシステムになっています。
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
public class MyEditorWindow : EditorWindow
{
[MenuItem("Window/My Editor Window")]
static void Open()
{
GetWindow<MyEditorWindow>("My Editor Window");
}
void CreateGUI()
{
// ラベル
var label = new Label("Hello, UI Toolkit!");
rootVisualElement.Add(label);
// テキストフィールド
var textField = new TextField("Name");
textField.RegisterValueChangedCallback(evt => Debug.Log(evt.newValue));
rootVisualElement.Add(textField);
// ボタン
var button = new Button(() => Debug.Log("Clicked!")) { text = "Click Me" };
rootVisualElement.Add(button);
}
}
UI Toolkitの利点: スタイルシート(USS)でデザインを分離でき、データバインディングにも対応しています。新規エディタ拡張ではUI Toolkitの使用を検討してください。
データの保存
public class MyEditorWindow : EditorWindow
{
string text = "";
void OnGUI()
{
text = EditorGUILayout.TextField("Text", text);
}
void OnEnable()
{
text = EditorPrefs.GetString("MyEditorWindow_Text", "");
}
void OnDisable()
{
EditorPrefs.SetString("MyEditorWindow_Text", text);
}
}
CustomEditor:インスペクターのカスタマイズ
CustomEditorは、特定のコンポーネントのインスペクター表示をカスタマイズできます。
基本的な使い方(推奨:SerializedObject)
// コンポーネント
using UnityEngine;
public class MyComponent : MonoBehaviour
{
public int value;
public string text;
}
// カスタムエディタ(推奨パターン)
using UnityEditor;
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
SerializedProperty valueProp;
SerializedProperty textProp;
void OnEnable()
{
valueProp = serializedObject.FindProperty("value");
textProp = serializedObject.FindProperty("text");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(valueProp);
EditorGUILayout.PropertyField(textProp);
if (GUILayout.Button("Reset"))
{
valueProp.intValue = 0;
textProp.stringValue = "";
}
serializedObject.ApplyModifiedProperties();
}
}
重要:
SerializedObjectを使用することで、Undo/Redo対応、複数オブジェクト同時編集、Prefabオーバーライド検出 が自動的に有効になります。EditorUtility.SetDirty()を呼ぶ必要はありません。
⚠️ targetを直接操作する方法(非推奨)
警告: この方法はUndo/Redo非対応・マルチオブジェクト編集非対応のため、上記のSerializedObject版を推奨します。既存コードの理解のためにのみ記載します。
// 非推奨パターン - Undo/Redo非対応
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
public override void OnInspectorGUI()
{
MyComponent myComponent = (MyComponent)target;
// 直接編集はUndo非対応、マルチ編集非対応
myComponent.value = EditorGUILayout.IntField("Value", myComponent.value);
myComponent.text = EditorGUILayout.TextField("Text", myComponent.text);
if (GUI.changed)
{
EditorUtility.SetDirty(target); // 変更をUnityに通知(非推奨パターン)
}
}
}
SetDirtyについて:
SerializedObject.ApplyModifiedProperties()を使用する場合、EditorUtility.SetDirty()は 不要 です。SetDirty()はtargetを直接操作する場合のみ必要ですが、そのパターン自体が非推奨です。
SerializedObjectのメリット
- Undo/Redo: 自動的に対応
- マルチ編集: 複数オブジェクト同時選択時も動作
- Prefab: オーバーライドの太字表示が自動
- SetDirty不要:
ApplyModifiedProperties()が自動的に変更を通知
EditorGUILayoutリファレンス
よく使うEditorGUILayoutのフィールドをまとめた参照表です。
| メソッド | 用途 | 戻り値 |
|---|---|---|
IntField | 整数入力 | int |
FloatField | 小数入力 | float |
TextField | 文字列入力 | string |
Toggle | チェックボックス | bool |
Popup | ドロップダウン選択 | int(インデックス) |
EnumPopup | Enum選択 | Enum |
ObjectField | オブジェクト参照 | Object |
Vector3Field | Vector3入力 | Vector3 |
ColorField | 色選択 | Color |
Slider | スライダー | float |
IntSlider | 整数スライダー | int |
Foldout | 折りたたみ | bool |
BeginHorizontal/EndHorizontal | 横並びレイアウト | - |
BeginVertical/EndVertical | 縦並びレイアウト | - |
Space | スペース追加 | - |
OnSceneGUI:シーンビューへの描画
CustomEditorでOnSceneGUIを実装すると、シーンビューにハンドルやガイドを描画できます。
// ウェイポイント管理コンポーネント
using UnityEngine;
public class WaypointManager : MonoBehaviour
{
public Transform[] waypoints;
}
// カスタムエディタ(OnSceneGUI実装)
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(WaypointManager))]
public class WaypointManagerEditor : Editor
{
void OnSceneGUI()
{
WaypointManager manager = (WaypointManager)target;
// ウェイポイント間に線を描画
Handles.color = Color.yellow;
for (int i = 0; i < manager.waypoints.Length - 1; i++)
{
Handles.DrawLine(
manager.waypoints[i].position,
manager.waypoints[i + 1].position
);
}
// 移動可能なハンドルを表示
for (int i = 0; i < manager.waypoints.Length; i++)
{
EditorGUI.BeginChangeCheck();
Vector3 newPos = Handles.PositionHandle(
manager.waypoints[i].position,
Quaternion.identity
);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(manager.waypoints[i], "Move Waypoint");
manager.waypoints[i].position = newPos;
}
}
}
}
活用例: ウェイポイントの配置、攻撃範囲の調整、スポーン位置の設定など、シーンビュー上で直感的に編集したい場合に便利です。
PropertyDrawer:プロパティの表示カスタマイズ
PropertyDrawerは、特定のプロパティの表示をカスタマイズできます。
基本的な使い方
// カスタムクラス
using UnityEngine;
[System.Serializable]
public class Range
{
public float min;
public float max;
}
// PropertyDrawer
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(Range))]
public class RangeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
var minRect = new Rect(position.x, position.y, position.width / 2 - 5, position.height);
var maxRect = new Rect(position.x + position.width / 2 + 5, position.y, position.width / 2 - 5, position.height);
EditorGUI.PropertyField(minRect, property.FindPropertyRelative("min"), GUIContent.none);
EditorGUI.PropertyField(maxRect, property.FindPropertyRelative("max"), GUIContent.none);
EditorGUI.EndProperty();
}
}
PropertyAttributeの使用(ReadOnly属性の例)
// 属性の定義
using UnityEngine;
public class ReadOnlyAttribute : PropertyAttribute
{
}
// PropertyDrawer
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
}
使用例:
public class MyComponent : MonoBehaviour
{
[ReadOnly]
public int readOnlyValue;
}
AssetDatabase:アセットの操作
AssetDatabaseは、プロジェクト内のアセットを操作するためのAPIです。
// すべてのPrefabを検索
string[] guids = AssetDatabase.FindAssets("t:Prefab");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
Debug.Log(prefab.name);
}
// ScriptableObjectの作成
MyScriptableObject asset = ScriptableObject.CreateInstance<MyScriptableObject>();
AssetDatabase.CreateAsset(asset, "Assets/MyAsset.asset");
AssetDatabase.SaveAssets();
// アセットの削除・リネーム・移動
AssetDatabase.DeleteAsset("Assets/MyAsset.asset");
AssetDatabase.RenameAsset("Assets/OldName.asset", "NewName");
AssetDatabase.MoveAsset("Assets/OldPath/MyAsset.asset", "Assets/NewPath/MyAsset.asset");
Selection:選択中のオブジェクト
Selectionクラスは、Unity Editorで選択中のオブジェクトを取得・設定できます。
// 選択中のオブジェクトを取得
GameObject selectedObject = Selection.activeGameObject;
// 複数選択の取得
GameObject[] selectedObjects = Selection.gameObjects;
foreach (GameObject obj in selectedObjects)
{
Debug.Log(obj.name);
}
// オブジェクトを選択
GameObject obj = GameObject.Find("MyObject");
Selection.activeGameObject = obj;
Undo:Undo/Redoの実装
Undoクラスは、エディタ拡張でUndo/Redo機能を実装できます。
// オブジェクトの変更を記録
Undo.RecordObject(myObject, "Change Value");
myObject.value = 10;
// オブジェクトの作成を記録
GameObject obj = new GameObject("New Object");
Undo.RegisterCreatedObjectUndo(obj, "Create Object");
// オブジェクトの削除を記録
Undo.DestroyObjectImmediate(obj);
実践的な使用例
例1:選択中のオブジェクトをすべて削除
[MenuItem("Tools/Delete Selected Objects")]
static void DeleteSelectedObjects()
{
if (Selection.gameObjects.Length == 0)
{
Debug.LogWarning("No objects selected.");
return;
}
foreach (GameObject obj in Selection.gameObjects)
{
Undo.DestroyObjectImmediate(obj);
}
}
[MenuItem("Tools/Delete Selected Objects", true)]
static bool ValidateDeleteSelectedObjects()
{
return Selection.gameObjects.Length > 0;
}
例2:カスタムウィンドウでアセットを一覧表示
public class AssetBrowserWindow : EditorWindow
{
List<GameObject> prefabs = new List<GameObject>();
Vector2 scrollPosition;
[MenuItem("Window/Asset Browser")]
static void Open()
{
GetWindow<AssetBrowserWindow>("Asset Browser");
}
void OnEnable()
{
LoadPrefabs();
}
void LoadPrefabs()
{
prefabs.Clear();
string[] guids = AssetDatabase.FindAssets("t:Prefab");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
prefabs.Add(prefab);
}
}
void OnGUI()
{
if (GUILayout.Button("Reload"))
{
LoadPrefabs();
}
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
foreach (GameObject prefab in prefabs)
{
EditorGUILayout.ObjectField(prefab, typeof(GameObject), false);
}
EditorGUILayout.EndScrollView();
}
}
ベストプラクティス
- Editorフォルダに配置 - エディタ拡張のスクリプトは必ずEditorフォルダ内に
- Undo/Redoの実装 - オブジェクトを変更する場合は必ず
Undo.RecordObjectを使用 - SerializedObjectの使用 - CustomEditorではSerializedObjectを使用して複数オブジェクト編集に対応
- エラーハンドリング - try-catchでエラーを適切に処理し、エディタの安定性を保つ
- パフォーマンスの考慮 - OnGUIは毎フレーム呼ばれるため、重い処理はキャッシュを活用
まとめ
Unity エディタ拡張は、開発効率を大幅に向上させる強力な技術です。
- MenuItem - メニューバーに項目を追加、ショートカットキーも設定可能
- EditorWindow - カスタムウィンドウを作成、複雑なツールを構築
- CustomEditor - インスペクターをカスタマイズ、使いやすいUIを提供
- PropertyDrawer - 特定のプロパティの表示をカスタマイズ
繰り返し作業を自動化し、チーム内での作業を標準化し、プロジェクト固有のワークフローに対応したツールを作成しましょう。