Every Unity script you have written so far is runtime code: code that only does something while the game is actually playing. This chapter is about a different kind of code — editor scripting, C# that runs inside the Unity Editor itself, before the game ever plays, to help the people who are building the game.
Every section here follows the same shape: a small piece of C# code, what happens when you use it in the Unity Editor, then a plain explanation. Create the scripts as you read and click the buttons yourself — watching the Editor react is how this sticks. This is also a core skill for Tools and Technical Art roles at studios: these teams spend most of their day building exactly the kind of things in this chapter.
A studio the size of HoYoverse or Riot is not made only of programmers. It also has level designers who place enemies and set up encounters, artists who set up characters and effects, and a role that sits between the two called a Technical Artist (an artist who also codes, usually in the engine's editor). None of those people necessarily want to open a C# file and hand-edit numbers. They want to drag a slider, click a button, and see the result — right there in the Editor.
Custom editor tools are the UI you build, inside Unity, so a non-programmer can do their job safely and quickly, without an engineer doing it for them every time.
Everything in this chapter — Inspector attributes, menu commands, custom inspectors, editor windows, gizmos, and property drawers — is a different-sized piece of exactly that idea: turning a manual, error-prone task into a UI a teammate can use themselves.
Editor scripts use a special namespace, UnityEditor, which gives you access to the Editor's own classes: Editor, EditorWindow, EditorGUILayout, and more. Here is the rule that makes everything else in this chapter safe to use: the UnityEditor namespace only exists inside the Unity Editor. It is not part of the game you ship. A player's built game (the .exe, the mobile app, the console build) never contains it.
Unity enforces this with a folder convention. Any script inside a folder literally named Editor — anywhere under Assets, nested as deep as you like — is treated as editor-only: it compiles when you work in the Editor, and it is automatically left out of every build.
If you write a script that uses UnityEditor and forget to put it in an Editor folder, it still compiles fine while you are working in the Editor (the Editor always has UnityEditor loaded). The problem only shows up when you try to build the game, because the build process compiles your scripts without the Editor's assemblies:
Assets/Scripts/EnemySpawnerEditor.cs(1,7): error CS0246:
The type or namespace name 'UnityEditor' could not be found
(are you missing a using directive or an assembly reference?)
That is the single most common beginner mistake with editor scripting. The fix is always the same: move the file into a folder named Editor (or, if a file needs to mix a little editor code into an otherwise-runtime script, wrap just that part in #if UNITY_EDITOR / #endif — you will see that pattern in section 11).
Before improving anything, look at what Unity gives you for free. Here is an ordinary MonoBehaviour with no attributes at all — a spawner that will create waves of enemies:
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
public GameObject enemyPrefab;
public int enemyCount;
public float spawnRadius;
public string waveName;
}
Attach this to a GameObject and Unity's default Inspector draws one row per public field automatically, using the field's name and type to guess a label:
This already works — a designer can type numbers into those boxes. But it has real problems: nothing stops Enemy Count from being typed as -5, nothing explains what Spawn Radius is measured in, and if this script grows to twenty fields they will all sit in one undivided list. Sections 4 and 6 fix this without writing a single line of editor-only code.
An attribute is a piece of metadata you attach to a field or class with square brackets, like [Range(1, 10)]. Attributes do not run as code themselves — they are instructions that Unity's Inspector reads and reacts to. Four attributes solve most of what section 3 was missing:
[Header("text")] — draws a bold divider line with a label above the next field, to group related fields visually.[Tooltip("text")] — shows a hint bubble when the mouse hovers over the field's label, explaining what it does.[Range(min, max)] — turns a number field into a slider clamped between min and max, so it is physically impossible to type an out-of-range value.[SerializeField] — makes a private field show up and save in the Inspector, without making it public.using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[Header("Wave Settings")]
[Tooltip("Prefab to spawn for each enemy in the wave.")]
public GameObject enemyPrefab;
[Tooltip("How many enemies to spawn in one wave.")]
[Range(1, 50)]
public int enemyCount = 5;
[Tooltip("Radius around this object where enemies can appear.")]
public float spawnRadius = 3f;
[Header("Debug Info")]
[SerializeField] private int totalSpawnedSoFar;
}
The last field is the interesting one. totalSpawnedSoFar is private — no other script can read or write it directly, which is the encapsulation rule from the C# basics chapter. But Unity's serializer does not use normal C# access rules; it uses reflection (inspecting a type's members at runtime) and only checks for the [SerializeField] attribute. So the field shows up in the Inspector and gets saved with the scene, while your other C# code still cannot touch it. This is the standard way to expose a value for designers to see or tune without opening it up to every other script.
public field is also serialized and visible in the Inspector, with no attribute needed. Use public when other scripts should legitimately read or write the value too; use [SerializeField] private when only the Inspector should see it.The [MenuItem] attribute adds a new entry to Unity's menu bar (or a right-click menu) that runs a method when clicked. The method must be static (called on the class itself, not on an instance — the same static you learned in the C# basics chapter), and the class must live in an Editor folder, because MenuItem comes from UnityEditor.
using UnityEditor;
using UnityEngine;
public static class EnemySpawnerMenu
{
[MenuItem("Tools/Enemy Spawner/Log All Spawners In Scene")]
private static void LogAllSpawners()
{
EnemySpawner[] spawners = Object.FindObjectsOfType<EnemySpawner>();
Debug.Log("Found " + spawners.Length + " EnemySpawner(s) in the scene.");
}
}
The string "Tools/Enemy Spawner/Log All Spawners In Scene" is a path. Each / creates a nested submenu, so this appears as Tools -> Enemy Spawner -> Log All Spawners In Scene in Unity's top menu bar. Clicking it calls LogAllSpawners() immediately — no Play mode needed.
Console output (clicked once, with 3 EnemySpawner objects placed in the scene):
Found 3 EnemySpawner(s) in the scene.
You can also add a matching validate function: a second method with the same path string and a second attribute argument of true. It returns bool, and Unity calls it constantly to decide whether to grey the menu item out.
[MenuItem("Tools/Enemy Spawner/Log All Spawners In Scene", true)]
private static bool ValidateLogAllSpawners()
{
// return false to grey the item out, e.g. only allow it
// when the scene actually has a spawner in it.
return Object.FindObjectsOfType<EnemySpawner>().Length > 0;
}
Now the menu item is only clickable when there is at least one EnemySpawner to log — a small touch that stops a teammate from being confused by a command that silently does nothing.
A custom inspector replaces the automatic field-by-field Inspector for one specific component with a method you write yourself. This is where you can add buttons that actually run code at edit time. First, give EnemySpawner something worth a button — a method that spawns the wave right now:
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
// ... fields from section 4 stay the same ...
public void SpawnWave()
{
for (int i = 0; i < enemyCount; i++)
{
Vector3 offset = Random.insideUnitSphere * spawnRadius;
offset.y = 0f;
Instantiate(enemyPrefab, transform.position + offset, Quaternion.identity);
}
totalSpawnedSoFar += enemyCount;
}
}
Now the custom inspector, which must live in an Editor folder:
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(EnemySpawner))]
public class EnemySpawnerEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector(); // draw all the normal fields, as before
EnemySpawner spawner = (EnemySpawner)target;
GUILayout.Space(10);
if (GUILayout.Button("Spawn Wave Now"))
{
spawner.SpawnWave();
}
}
}
[CustomEditor(typeof(EnemySpawner))] tells Unity "use this class instead of the default Inspector, whenever an EnemySpawner is selected." Inside OnInspectorGUI, the base Editor class gives you target — a reference to the selected object, typed generically as Object. We cast it (convert it to a more specific type, as covered in the C# basics chapter) to EnemySpawner so we can call SpawnWave() on it.
Click Spawn Wave Now while the game is not playing, and five goblins appear around the spawner right there in the Scene view — instantly, with no Play button pressed. That is the real power of editor scripting: it runs the game's own methods at author time, so a designer can iterate on placement and numbers without ever entering Play mode.
Undo.RecordObject(spawner, "Spawn Wave") before making the change, and EditorUtility.SetDirty(spawner) afterward. The first makes the change undoable with Ctrl+Z like any other Editor action; the second tells Unity the scene has unsaved changes, so nothing gets silently lost. Small tools that skip this can quietly corrupt a teammate's work.target with a plain (EnemySpawner) cast works for a simple button like this one, but for fields that also need multi-object editing (selecting several spawners and editing them together) and automatic Undo support, real tools use serializedObject and SerializedProperty instead of touching target's fields directly. DrawDefaultInspector() already does this correctly for you — it is only your custom, hand-written GUI code that needs to be careful.A custom inspector only appears when its component is selected. Sometimes a tool is not "about" any single object — it is a workspace of its own, like a level batch-generator. For that, Unity gives you EditorWindow: a window you design yourself, opened from the menu, that can float or dock next to the Scene and Game tabs like any built-in Unity panel.
using UnityEditor;
using UnityEngine;
public class WaveGeneratorWindow : EditorWindow
{
private int waveCount = 3;
private float spacing = 2f;
[MenuItem("Tools/Wave Generator")]
private static void ShowWindow()
{
GetWindow<WaveGeneratorWindow>("Wave Generator");
}
private void OnGUI()
{
GUILayout.Label("Wave Generator Settings", EditorStyles.boldLabel);
waveCount = EditorGUILayout.IntField("Wave Count", waveCount);
spacing = EditorGUILayout.FloatField("Spacing", spacing);
if (GUILayout.Button("Generate Waves"))
{
for (int i = 0; i < waveCount; i++)
{
GameObject wave = new GameObject("Wave " + i);
wave.transform.position = new Vector3(i * spacing, 0f, 0f);
}
Debug.Log("Generated " + waveCount + " wave marker(s).");
}
}
}
ShowWindow() is called from the Tools/Wave Generator menu item, and GetWindow<WaveGeneratorWindow>("Wave Generator") creates (or focuses, if it is already open) the window with that title. OnGUI is the Unity message this window calls every time it needs to redraw itself — every frame it is visible, similar in spirit to how Update runs every frame in a normal MonoBehaviour, except this one draws editor UI instead of game logic.
Set Wave Count to 4 and click Generate Waves: four empty GameObjects named Wave 0 through Wave 3 appear in the Hierarchy, spaced 2 units apart along the X axis, and the Console prints:
Generated 4 wave marker(s).
The key difference from section 6: an EditorWindow is not tied to any selected object. It stays open across selection changes, can be dragged into a dock next to any other Unity panel, and is the right choice for batch operations, level-wide tools, or anything that needs its own persistent UI rather than living inside one component's Inspector.
A gizmo is a shape drawn directly in the Scene view to visualize something that has no visual representation on its own — like spawnRadius, which is just a number until you can actually see the circle it describes. Gizmos are drawn by two special Unity messages, and unlike everything else in this chapter, they live in a normal script, not an Editor folder:
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
// ... fields and SpawnWave() from earlier sections stay the same ...
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, spawnRadius);
}
}
OnDrawGizmosSelected only draws while this specific object (or its parent) is selected in the Hierarchy. Its sibling, OnDrawGizmos, draws all the time the Scene view is open, selected or not — useful for something you always want visible, like level boundaries, but noisier if you have hundreds of objects.
Here is the part that seems to contradict section 2, so it is worth saying clearly: OnDrawGizmos and OnDrawGizmosSelected can reference Gizmos and still sit in a plain runtime script, outside any Editor folder. Unity special-cases these two method names — it strips them out of the shipped build automatically, the same way it strips comments, without you needing UnityEditor or a special folder. The Editor-folder rule is about the UnityEditor namespace; Gizmos is a small, separate exception baked into the engine specifically so you can mix visualization into a normal gameplay script.
OnDrawGizmos, always). They cost nothing in a shipped build, so feel free to add generous ones while you are building a level.Sometimes the same small custom type appears as a field in many different scripts, and the default two-row-per-field layout wastes space every single time. A property drawer lets you customize the Inspector layout for a type, once, and it then applies automatically anywhere that type is used as a field. Start with a small serializable class, in a normal (non-Editor) script:
[System.Serializable]
public class MinMaxFloat
{
public float min;
public float max;
}
Used as a field — for example public MinMaxFloat spawnDelay; on EnemySpawner — Unity's default Inspector draws it as an indented sub-block with min and max stacked on separate lines. A property drawer can compress that into one tidy row:
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(MinMaxFloat))]
public class MinMaxFloatDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
SerializedProperty minProp = property.FindPropertyRelative("min");
SerializedProperty maxProp = property.FindPropertyRelative("max");
Rect labelRect = new Rect(position.x, position.y, 60, position.height);
Rect minRect = new Rect(position.x + 65, position.y, 60, position.height);
Rect maxRect = new Rect(position.x + 130, position.y, 60, position.height);
EditorGUI.LabelField(labelRect, label);
EditorGUI.PropertyField(minRect, minProp, GUIContent.none);
EditorGUI.PropertyField(maxRect, maxProp, GUIContent.none);
EditorGUI.EndProperty();
}
}
[CustomPropertyDrawer(typeof(MinMaxFloat))] tells Unity "whenever you are about to draw a field of type MinMaxFloat, call this instead." property.FindPropertyRelative("min") reaches inside the serialized object to grab its min field by name, and the Rect values slice the single row Unity gave us into three smaller boxes side by side. This file must also live in an Editor folder — it uses UnityEditor, same rule as every other tool in this chapter.
The payoff is that you write this drawer once, and every script in the entire project that has a MinMaxFloat field — today or added next year — gets the compact layout automatically, with zero extra work per script.
It helps to see the whole picture at once: editor scripting sits entirely on one side of a line that runtime code never crosses.
Notice where the split happens: everything above the "saved into files" line only exists to make authoring faster and safer. Everything below it is the actual game, and it has no idea any of those tools were ever used — it just reads the numbers and prefabs that got saved. That is also why the Editor-folder rule from section 2 is so strict: it physically enforces that split, so an editor tool can never accidentally become something the player depends on.
A short, practical recap before the exercises:
UnityEditor namespace must be inside a folder named Editor, anywhere under Assets. Otherwise your project fails to build with a "type or namespace name could not be found" error (section 2).OnDrawGizmos / OnDrawGizmosSelected are the one exception: they belong in normal runtime scripts, not an Editor folder, and Unity strips them from builds automatically (section 8).Undo.RecordObject(...) and mark it dirty with EditorUtility.SetDirty(...), so the change is undoable and actually gets saved.DrawDefaultInspector() and normal public/[SerializeField] fields already handle undo and multi-object editing correctly by themselves. Only your hand-written GUI code (buttons, custom layouts) needs to be careful about it.One more tool is worth knowing for the rare case where a single file genuinely needs both runtime code and a little editor-only code mixed together: the #if UNITY_EDITOR preprocessor directive. Unity defines UNITY_EDITOR automatically while you work in the Editor, and leaves it undefined in a player build, so code between #if UNITY_EDITOR and #endif is compiled only in the Editor and quietly removed from the build — no separate file or folder needed.
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class EnemySpawner : MonoBehaviour
{
public void SpawnWave()
{
// ... runtime logic, ships in every build ...
#if UNITY_EDITOR
// Editor-only convenience: also select the spawner after spawning,
// so the designer sees it highlighted. Compiled out of real builds.
Selection.activeGameObject = gameObject;
#endif
}
}
#if UNITY_EDITOR everywhere instead of an Editor folder. It works, but it clutters a runtime file with editor concerns and is easy to get wrong (forgetting the matching #endif, or referencing UnityEditor outside the guarded block). Prefer a whole separate file in an Editor folder whenever the editor-only part is more than a line or two — which is true for almost everything in sections 5 to 9.Editor, EditorWindow, EditorGUILayout, ...) only available inside the Editor, never in a build.Editor, anywhere under Assets; scripts inside it are compiled only for the Editor and excluded from every build.[Range(1,10)]) attached to a field or class that changes how Unity treats it.[MenuItem] method (path, true) that returns bool to enable or grey out a menu item.Editor subclass replace the default Inspector for a given component type.Editor overrides to draw its own Inspector UI.Editor base class's reference to the object currently being inspected.EditorWindow (or custom editor) calls every redraw to lay out its UI.[CustomPropertyDrawer] class that changes how one type is drawn in the Inspector, everywhere it appears.[Header("Player Settings")], each field has a [Tooltip] explaining what it does in one short sentence, and maxHealth uses [Range(50, 500)].
using UnityEngine;
public class PlayerStats : MonoBehaviour
{
public int maxHealth;
public float moveSpeed;
}
using UnityEngine;
public class PlayerStats : MonoBehaviour
{
[Header("Player Settings")]
[Tooltip("Maximum health points before the player dies.")]
[Range(50, 500)]
public int maxHealth = 100;
[Tooltip("Movement speed in meters per second.")]
public float moveSpeed = 5f;
}
[Header] only needs to appear once, above the first field of the group — it draws a divider and then every following field belongs to that group until the next [Header]. [Range(50, 500)] turns maxHealth into a slider that cannot go below 50 or above 500, even if someone types a number directly into the field. moveSpeed keeps its own tooltip but has no range, since a sensible minimum/maximum for speed is less obvious than for health.
[MenuItem] at "Tools/Player/Log Selected Count" that logs how many GameObjects are currently selected in the Hierarchy. (Hint: the editor-only class Selection, from UnityEditor, has a static property gameObjects that returns an array of the currently selected objects.)using UnityEditor;
using UnityEngine;
public static class SelectionLogger
{
[MenuItem("Tools/Player/Log Selected Count")]
private static void LogSelectedCount()
{
int count = Selection.gameObjects.Length;
Debug.Log("You have " + count + " GameObject(s) selected.");
}
}
This file must sit inside an Editor folder, because it uses both UnityEditor.MenuItem and UnityEditor.Selection. Selecting three objects in the Hierarchy and clicking Tools -> Player -> Log Selected Count prints:
You have 3 GameObject(s) selected.
Assets/Scripts/EnemySpawnerEditor.cs — note: Scripts, not Scripts/Editor.
using UnityEditor;
[CustomEditor(typeof(EnemySpawner))]
public class EnemySpawnerEditor : Editor
{
// ... OnInspectorGUI ...
}
It works fine while they play-test in the Editor. What happens the first time someone runs File -> Build Settings -> Build, and what are two different ways to fix it?The build fails with a compile error, because the build process compiles scripts without the Editor's assemblies, so UnityEditor cannot be found:
Assets/Scripts/EnemySpawnerEditor.cs(1,7): error CS0246:
The type or namespace name 'UnityEditor' could not be found
(are you missing a using directive or an assembly reference?)
Fix 1: move the file into any folder literally named Editor, for example Assets/Scripts/Editor/EnemySpawnerEditor.cs. This is the standard fix, and the right one here since the whole file is editor-only.
Fix 2: wrap the UnityEditor-using parts in #if UNITY_EDITOR / #endif instead of moving the file. This only makes sense when the same file also has runtime code that needs to stay; for a file that is entirely editor code, like this one, moving it into an Editor folder (Fix 1) is simpler and is what real projects do.
That covers the core toolkit: attributes that improve the default Inspector, [MenuItem] commands, custom inspectors with buttons, standalone EditorWindow tools, gizmos for Scene-view feedback, and property drawers that fix a type's layout everywhere at once. The one rule that ties all of it together is the Editor-folder boundary: editor code makes the people building the game faster, runtime code is the game itself, and Unity keeps the two from ever accidentally mixing into a shipped build.