Overview
Tested with: Unity 2022.3 LTS / Unity 6
"Repetitive tasks are taking too much time..." "I want to standardize workflows across the team..." "I want to build user-friendly tools for designers and planners..."
These are common challenges in Unity development. Editor extensions are techniques for extending Unity Editor functionality to improve development efficiency. You can create custom windows, customize the Inspector, add menu items, and build tools tailored to your project's specific workflows.
- Editor Folder Placement
- MenuItem: Adding Menu Items
- EditorWindow: Creating Custom Windows
- CustomEditor: Customizing the Inspector
- PropertyDrawer: Customizing Property Display
- EditorGUILayout Reference
- OnSceneGUI: Drawing in the Scene View
- AssetDatabase: Asset Operations
- Selection: Selected Objects
- Undo: Implementing Undo/Redo
- Practical Examples
- Best Practices
- Summary
Editor Folder Placement
Editor extension scripts must always be placed inside an Editor folder.
Why is the Editor Folder Required?
- Separates code used only in Unity Editor
- Automatically excluded from builds
- Enables use of editor-only APIs
- Controls compilation order
Where to Place It
- Can be placed anywhere in the project
- Commonly placed at
Assets/Editor - Multiple Editor folders are allowed
- Resources used by editor extensions (images, fonts, etc.) go in the
Editor Default Resourcesfolder
MenuItem: Adding Menu Items
MenuItem is the simplest form of editor extension, adding items to the Unity Editor menu bar.
Basic Usage
using UnityEngine;
using UnityEditor;
public class MenuItemExample
{
[MenuItem("Tools/Do Something")]
static void DoSomething()
{
Debug.Log("Something!");
}
}
This adds a Tools > Do Something menu item.
Setting Shortcut Keys
[MenuItem("Tools/Do Something %g")] // Ctrl+G (Windows) / Cmd+G (Mac)
static void DoSomething()
{
Debug.Log("Something!");
}
Shortcut key symbols:
%: Ctrl (Windows) / Cmd (Mac)#: Shift&: Alt_: Key only (e.g.,_gfor G)
Enabling/Disabling Menu Items
[MenuItem("Tools/Do Something")]
static void DoSomething()
{
Debug.Log("Something!");
}
[MenuItem("Tools/Do Something", true)]
static bool ValidateDoSomething()
{
// Only enabled when something is selected
return Selection.activeGameObject != null;
}
Adding Context Menu Items
[MenuItem("CONTEXT/Transform/Reset Position")]
static void ResetPosition(MenuCommand command)
{
Transform transform = (Transform)command.context;
Undo.RecordObject(transform, "Reset Position");
transform.position = Vector3.zero;
}
Important: Always use
Undo.RecordObject()to support undo operations when modifying objects. When usingUndo.RecordObject(),EditorUtility.SetDirty()is not needed (Undo itself tracks the changes).
EditorWindow: Creating Custom Windows
EditorWindow is a powerful feature that lets you create custom windows.
Basic Usage
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!");
}
}
Using UI Toolkit (Unity 2021+)
Starting with Unity 2022, UI Toolkit is the recommended UI system for editor extensions.
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()
{
// Label
var label = new Label("Hello, UI Toolkit!");
rootVisualElement.Add(label);
// Text field
var textField = new TextField("Name");
textField.RegisterValueChangedCallback(evt => Debug.Log(evt.newValue));
rootVisualElement.Add(textField);
// Button
var button = new Button(() => Debug.Log("Clicked!")) { text = "Click Me" };
rootVisualElement.Add(button);
}
}
Benefits of UI Toolkit: Supports style separation with stylesheets (USS) and data binding. Consider using UI Toolkit for new editor extensions.
Saving Data
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: Customizing the Inspector
CustomEditor lets you customize the Inspector display for specific components.
Basic Usage (Recommended: SerializedObject)
// Component
using UnityEngine;
public class MyComponent : MonoBehaviour
{
public int value;
public string text;
}
// Custom Editor (recommended pattern)
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();
}
}
Important: Using
SerializedObjectautomatically enables Undo/Redo support, multi-object editing, and Prefab override detection. There is no need to callEditorUtility.SetDirty().
Warning: Directly Accessing target (Not Recommended)
Warning: This approach does not support Undo/Redo or multi-object editing. The SerializedObject approach above is recommended. This is documented only for understanding legacy code.
// Not recommended - no Undo/Redo support
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
public override void OnInspectorGUI()
{
MyComponent myComponent = (MyComponent)target;
// Direct editing does not support Undo or multi-editing
myComponent.value = EditorGUILayout.IntField("Value", myComponent.value);
myComponent.text = EditorGUILayout.TextField("Text", myComponent.text);
if (GUI.changed)
{
EditorUtility.SetDirty(target); // Notify Unity of changes (not recommended pattern)
}
}
}
About SetDirty: When using
SerializedObject.ApplyModifiedProperties(),EditorUtility.SetDirty()is not needed.SetDirty()is only necessary when directly modifyingtarget, but that pattern itself is not recommended.
Benefits of SerializedObject
- Undo/Redo: Automatic support
- Multi-editing: Works when multiple objects are selected simultaneously
- Prefab: Bold override display is automatic
- No SetDirty needed:
ApplyModifiedProperties()automatically notifies of changes
EditorGUILayout Reference
A reference table of commonly used EditorGUILayout fields.
| Method | Purpose | Return Type |
|---|---|---|
IntField | Integer input | int |
FloatField | Float input | float |
TextField | String input | string |
Toggle | Checkbox | bool |
Popup | Dropdown selection | int (index) |
EnumPopup | Enum selection | Enum |
ObjectField | Object reference | Object |
Vector3Field | Vector3 input | Vector3 |
ColorField | Color picker | Color |
Slider | Slider | float |
IntSlider | Integer slider | int |
Foldout | Foldout section | bool |
BeginHorizontal/EndHorizontal | Horizontal layout | - |
BeginVertical/EndVertical | Vertical layout | - |
Space | Add spacing | - |
OnSceneGUI: Drawing in the Scene View
Implementing OnSceneGUI in a CustomEditor lets you draw handles and guides in the Scene view.
// Waypoint manager component
using UnityEngine;
public class WaypointManager : MonoBehaviour
{
public Transform[] waypoints;
}
// Custom Editor (OnSceneGUI implementation)
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(WaypointManager))]
public class WaypointManagerEditor : Editor
{
void OnSceneGUI()
{
WaypointManager manager = (WaypointManager)target;
// Draw lines between waypoints
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
);
}
// Display movable handles
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;
}
}
}
}
Use cases: This is useful for intuitively editing waypoint placement, attack range adjustments, spawn position configuration, and more directly in the Scene view.
PropertyDrawer: Customizing Property Display
PropertyDrawer lets you customize how specific properties are displayed.
Basic Usage
// Custom class
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();
}
}
Using PropertyAttribute (ReadOnly Attribute Example)
// Attribute definition
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;
}
}
Usage:
public class MyComponent : MonoBehaviour
{
[ReadOnly]
public int readOnlyValue;
}
AssetDatabase: Asset Operations
AssetDatabase is the API for manipulating assets within the project.
// Find all Prefabs
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);
}
// Create a ScriptableObject
MyScriptableObject asset = ScriptableObject.CreateInstance<MyScriptableObject>();
AssetDatabase.CreateAsset(asset, "Assets/MyAsset.asset");
AssetDatabase.SaveAssets();
// Delete, rename, and move assets
AssetDatabase.DeleteAsset("Assets/MyAsset.asset");
AssetDatabase.RenameAsset("Assets/OldName.asset", "NewName");
AssetDatabase.MoveAsset("Assets/OldPath/MyAsset.asset", "Assets/NewPath/MyAsset.asset");
Selection: Selected Objects
The Selection class lets you get and set the currently selected objects in the Unity Editor.
// Get the selected object
GameObject selectedObject = Selection.activeGameObject;
// Get multiple selections
GameObject[] selectedObjects = Selection.gameObjects;
foreach (GameObject obj in selectedObjects)
{
Debug.Log(obj.name);
}
// Select an object
GameObject obj = GameObject.Find("MyObject");
Selection.activeGameObject = obj;
Undo: Implementing Undo/Redo
The Undo class enables Undo/Redo functionality in editor extensions.
// Record object changes
Undo.RecordObject(myObject, "Change Value");
myObject.value = 10;
// Record object creation
GameObject obj = new GameObject("New Object");
Undo.RegisterCreatedObjectUndo(obj, "Create Object");
// Record object deletion
Undo.DestroyObjectImmediate(obj);
Practical Examples
Example 1: Delete All Selected Objects
[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;
}
Example 2: Custom Window for Browsing Assets
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();
}
}
Best Practices
- Place scripts in the Editor folder - Editor extension scripts must always be in an Editor folder
- Implement Undo/Redo - Always use
Undo.RecordObjectwhen modifying objects - Use SerializedObject - Use SerializedObject in CustomEditors for multi-object editing support
- Handle errors - Use try-catch to handle errors gracefully and maintain editor stability
- Consider performance - OnGUI is called every frame, so use caching for expensive operations
Summary
Unity editor extensions are a powerful technique for dramatically improving development efficiency.
- MenuItem - Add items to the menu bar with optional shortcut keys
- EditorWindow - Create custom windows for complex tools
- CustomEditor - Customize the Inspector for user-friendly UIs
- PropertyDrawer - Customize how specific properties are displayed
Automate repetitive tasks, standardize team workflows, and build tools tailored to your project's unique needs.