Editor Scripting — Extending the Unity Editor
Custom Inspectors, EditorWindow, MenuItem, Gizmos, ExecuteAlways — extending the Unity Editor.
The Unity Editor can be extended with C# code just like runtime logic. This lets you:
- Build custom inspectors for a non-standard field-editing UI.
- Create editor windows — standalone tool panels inside the editor.
- Add menu items, gizmos, scene view handles.
- Run your code in the editor (not only in Play Mode).
Editor scripts — the main rule
Editor scripts live in an Editor/ folder (any nested one), and are compiled into a separate
assembly that is not included in the game build.
Assets/
└── Editor/
└── EnemyEditor.cs ← Editor script
└── Scripts/
└── Enemy.cs ← Runtime script
Inside Editor scripts you can use using UnityEditor; (this namespace is not available at
runtime).
DevTools extensions for Chrome: you write JS that adds a panel to DevTools for debugging your React applications. Only the Unity equivalent goes much deeper — it can change the editor itself.
An Editor script is a separate assembly that does not exist at runtime. The editor UI is drawn via IMGUI or UI Toolkit. Full access to Selection, AssetDatabase, EditorPrefs.
Custom Inspector — override the UI
The standard Inspector draws fields by default rules. Want to see a “Heal” button or a colored slider instead of a plain float field? You make a CustomEditor:
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Enemy))]
public class EnemyEditor : Editor
{
public override void OnInspectorGUI() {
// First — the standard inspector (you can remove it if you want full control)
DrawDefaultInspector();
var enemy = (Enemy)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel);
if (GUILayout.Button("Reset Health")) {
Undo.RecordObject(enemy, "Reset Health");
enemy.CurrentHp = enemy.MaxHp;
EditorUtility.SetDirty(enemy);
}
if (GUILayout.Button("Take 10 Damage")) {
Undo.RecordObject(enemy, "Take Damage");
enemy.TakeDamage(10);
EditorUtility.SetDirty(enemy);
}
EditorGUILayout.HelpBox(
$"HP: {enemy.CurrentHp} / {enemy.MaxHp}",
MessageType.Info);
}
}
Now the Enemy component will have two buttons + a colored hint in the Inspector. Especially useful for testing right during Play Mode.
Without Undo.RecordObject, your edits won’t be captured by Ctrl+Z. That’s bad UX — the user
clicked “Reset Health”, then changed their mind, and Ctrl+Z does nothing. Recording Undo is a
standard of good practice.
SerializedProperty — the right way
For serialized fields, use SerializedObject / SerializedProperty — this is cleaner than reading
target directly:
[CustomEditor(typeof(Enemy))]
public class EnemyEditor : Editor
{
SerializedProperty maxHpProp;
SerializedProperty currentHpProp;
void OnEnable() {
maxHpProp = serializedObject.FindProperty("maxHp");
currentHpProp = serializedObject.FindProperty("currentHp");
}
public override void OnInspectorGUI() {
serializedObject.Update();
EditorGUILayout.PropertyField(maxHpProp);
EditorGUILayout.Slider(currentHpProp, 0, maxHpProp.intValue, "Current HP");
serializedObject.ApplyModifiedProperties();
}
}
PropertyField handles multi-object editing by itself (select 5 enemies → the change applies to
all of them) and Undo.
EditorWindow — your own panel
If the functionality is not about a specific component but about a “tool” (a level generator, an asset inspector):
using UnityEditor;
using UnityEngine;
public class LevelGeneratorWindow : EditorWindow
{
private int width = 10;
private int height = 10;
private GameObject tilePrefab;
[MenuItem("Tools/Level Generator")]
public static void ShowWindow() {
GetWindow<LevelGeneratorWindow>("Level Generator");
}
private void OnGUI() {
GUILayout.Label("Level Settings", EditorStyles.boldLabel);
width = EditorGUILayout.IntField("Width", width);
height = EditorGUILayout.IntField("Height", height);
tilePrefab = (GameObject)EditorGUILayout.ObjectField(
"Tile Prefab", tilePrefab, typeof(GameObject), false);
if (GUILayout.Button("Generate")) {
Generate();
}
}
private void Generate() {
if (tilePrefab == null) return;
var parent = new GameObject("GeneratedLevel").transform;
for (int x = 0; x < width; x++) {
for (int z = 0; z < height; z++) {
var tile = PrefabUtility.InstantiatePrefab(tilePrefab) as GameObject;
tile.transform.position = new Vector3(x, 0, z);
tile.transform.SetParent(parent);
}
}
Undo.RegisterCreatedObjectUndo(parent.gameObject, "Generate Level");
}
}
After saving, “Level Generator” will appear in the Tools menu. It opens as a separate window with configurable fields.
MenuItem — menu items
public class CustomMenu
{
[MenuItem("Tools/Cleanup/Remove All Empty GameObjects")]
static void RemoveEmpty() {
int removed = 0;
foreach (var go in Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None)) {
if (go.transform.childCount == 0 &&
go.GetComponents<Component>().Length == 1) { // only Transform
Object.DestroyImmediate(go);
removed++;
}
}
Debug.Log($"Removed {removed} empty GameObjects");
}
[MenuItem("CONTEXT/Light/Reset Intensity")]
static void ResetLightIntensity(MenuCommand command) {
var light = (Light)command.context;
Undo.RecordObject(light, "Reset Intensity");
light.intensity = 1f;
}
}
The first is an item in the main menu. The second is an item in the context menu of the Light component (right-click on Light in the Inspector).
OnDrawGizmos — visualization in the Scene View
The most common Editor scripting use case is drawing a debug shape right in the 3D viewport:
public class Patrol : MonoBehaviour
{
public Vector3[] points;
private void OnDrawGizmos() {
if (points == null || points.Length == 0) return;
Gizmos.color = Color.green;
for (int i = 0; i < points.Length; i++) {
Vector3 p = transform.TransformPoint(points[i]);
Gizmos.DrawWireSphere(p, 0.3f);
if (i > 0) {
Vector3 prev = transform.TransformPoint(points[i - 1]);
Gizmos.DrawLine(prev, p);
}
}
}
private void OnDrawGizmosSelected() {
// Drawn only when the object is selected
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, 2f); // visibility range
}
}
OnDrawGizmos is a standard Unity function and does not require an Editor folder. It works
right in a runtime script.
ExecuteAlways — runtime logic in the editor
Want your script to update in Edit Mode (without Play)? The [ExecuteAlways] attribute:
[ExecuteAlways]
public class Compass : MonoBehaviour
{
public Transform target;
private void Update() {
if (target == null) return;
Vector3 dir = (target.position - transform.position).normalized;
transform.rotation = Quaternion.LookRotation(dir);
}
}
Now the “compass needle” turns toward the target right in the Scene View, without Play Mode. The
analog of Godot’s @tool.
A script in the editor calls Update every frame. Heavy code slows down the editor. An infinite
loop will freeze Unity. Before using ExecuteAlways, weigh whether you really need it.
Asset Postprocessor
Want to automatically set the Compression Mode for all imported PNGs? AssetPostprocessor:
public class TexturePostprocessor : AssetPostprocessor
{
void OnPreprocessTexture() {
var importer = (TextureImporter)assetImporter;
if (assetPath.Contains("/UI/")) {
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.spritePixelsPerUnit = 100;
}
}
}
This code fires on the first import of each PNG. Very handy for projects with rules like “UI textures must be uncompressed, otherwise they get blurry”.
What to read next
- UI Toolkit for Editor (UIElements) — the modern way to build complex editor UI, replacing IMGUI. Supports UXML+USS just like runtime UI Toolkit.
- CustomPropertyDrawer — for a single property type, so that Unity draws it the same way in all inspectors.
- EditorPrefs — storing per-machine settings for your tool.
Comparison with Godot
The concepts map 1:1:
[CustomEditor]↔ GodotEditorInspectorPlugin._parse_propertyEditorWindow↔ GodotEditorPlugin.add_control_to_dock[MenuItem]↔ GodotEditorPlugin.add_tool_menu_itemOnDrawGizmos↔ GodotEditorNode3DGizmoPlugin._redraw[ExecuteAlways]↔ Godot@tool
In Godot, all of this is written in GDScript. In Unity — in C# in a separate assembly.