Unity Editor Scripting Tutorial: Custom Editors, EditorGUILayout and Tools

Unity Editor scripting to automate your workflow

Most Unity tutorials teach you how to script gameplay. Far fewer teach you how to script the Editor itself โ€” but this is one of the highest-leverage skills you can build, especially as a solo developer. A well-placed Editor script turns a 20-minute repetitive task into a one-click button, and a good validation script catches the bug that would otherwise ship. This guide walks through every Editor scripting tool you’ll actually use, with complete, copy-paste-ready code that compiles in Unity 6 and 2022.3 LTS.

๐Ÿ›  Shipping soon?

Before you build, run through our platform-specific checklist of Player and Quality Settings worth double-checking.

Open the Project Settings Checklist →

Quick answer

An editor script is C# that runs only inside the Unity Editor: put it in a folder named Editor (or an Editor-only assembly definition) and add using UnityEditor;. For a custom Inspector, write [CustomEditor(typeof(EnemySpawner))] public class EnemySpawnerEditor : Editor and override OnInspectorGUI(), drawing fields with EditorGUILayout. For small editor-only bits inside a runtime script, wrap them in #if UNITY_EDITOR. Where editor code lives.

Why Editor Scripting Matters

Every Unity project accumulates repetitive manual tasks: renaming dozens of assets consistently, setting up the same component combinations on new GameObjects, validating that prefabs follow naming conventions, batch-converting import settings. Doing these by hand doesn’t just waste time โ€” it introduces human error. Editor scripts let you encode a process once and run it reliably forever after, and they’re ordinary C#: if you can write a MonoBehaviour, you can write an Editor tool.

Where Editor Code Lives (and Why It Matters)

Anything that uses the UnityEditor namespace is Editor-only. That namespace doesn’t exist in a player build, so Editor code has to be kept out of your game’s runtime assembly. You have three ways to do that:

  • An Editor folder. Any folder literally named Editor (you can have as many as you like, e.g. Assets/Editor/ or Assets/Enemies/Editor/) is compiled into the separate Assembly-CSharp-Editor assembly, which never ships.
  • An Editor-only assembly definition. If your project uses .asmdef files, the magic folder name stops working inside them: an Editor folder under a runtime asmdef gets compiled into that runtime assembly and breaks your build. Give the Editor folder its own asmdef (Create > Scripting > Assembly Definition), set Include Platforms to Editor only, and reference your runtime asmdef from it.
  • #if UNITY_EDITOR blocks for small bits of Editor code inside a runtime script:
using UnityEngine;

public class LevelMarker : MonoBehaviour
{
#if UNITY_EDITOR
    // Right-click the component header > "Ping Source Prefab"
    [ContextMenu("Ping Source Prefab")]
    void PingSourcePrefab()
    {
        var prefab = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
        if (prefab != null)
            UnityEditor.EditorGUIUtility.PingObject(prefab);
    }
#endif
}

Keep the #if tight around Editor-only members. Never wrap a whole MonoBehaviour class in #if UNITY_EDITOR: the component will exist in your scenes in the Editor and turn into a missing script in the build.

The [MenuItem] attribute turns any static method into a menu command. Here’s a small tool that logs the full hierarchy path of each selected object, with a keyboard shortcut and a validation function that greys it out when nothing is selected:

using UnityEditor;
using UnityEngine;

public static class QuickTools
{
    // %#l = Ctrl/Cmd + Shift + L. The third argument (100) is the sort priority.
    [MenuItem("Tools/DevForge/Log Selection Paths %#l", false, 100)]
    static void LogSelectionPaths()
    {
        foreach (GameObject go in Selection.gameObjects)
            Debug.Log(GetPath(go.transform), go); // clicking the log line pings the object
    }

    // Same path + "true" = validation function. Return false to grey the item out.
    [MenuItem("Tools/DevForge/Log Selection Paths %#l", true)]
    static bool LogSelectionPathsValidate() => Selection.gameObjects.Length > 0;

    static string GetPath(Transform t) =>
        t.parent == null ? t.name : GetPath(t.parent) + "/" + t.name;
}

Save it as Assets/Editor/QuickTools.cs and a new Tools > DevForge submenu appears once scripts recompile. The shortcut goes at the end of the path, after a space:

Symbol Key
% Ctrl (Windows/Linux) or Cmd (macOS)
# Shift
& Alt / Option
_ No modifier, e.g. _g is plain G
F1โ€ฆF12, HOME, END, PGUP, PGDN, LEFT, RIGHT, UP, DOWN Special keys, combinable with the modifiers above

Shortcuts registered this way show up in Edit > Shortcuts (Unity > Shortcuts on macOS), where you or teammates can rebind them and spot conflicts.

Two menu roots are special. Items under Assets/ also appear when you right-click in the Project window, and items under GameObject/ also appear in the Hierarchy’s right-click create menu. The Hierarchy version receives the right-clicked object as MenuCommand.context, so new objects can be parented correctly:

[MenuItem("GameObject/DevForge/Spawn Point", false, 10)]
static void CreateSpawnPoint(MenuCommand command)
{
    var go = new GameObject("SpawnPoint");
    GameObjectUtility.SetParentAndAlign(go, command.context as GameObject); // parent under right-clicked object
    Undo.RegisterCreatedObjectUndo(go, "Create Spawn Point");                 // Ctrl+Z works
    Selection.activeObject = go;
}

Context Menus on Components and Fields

Sometimes the right place for a command is the component itself. There are three options:

  • [ContextMenu("Label")] on a method in a MonoBehaviour adds it to that component’s โ‹ฎ menu. It’s great for debugging in Play Mode (“Refill Health”, “Kill Player”).
  • [ContextMenuItem("Label", "MethodName")] on a serialized field adds a command when you right-click that field.
  • [MenuItem("CONTEXT/ComponentType/Label")] in an Editor script adds a command to any component type, including built-in ones you can’t edit:
// Adds "Snap Position To Whole Units" to every Transform's โ‹ฎ menu
[MenuItem("CONTEXT/Transform/Snap Position To Whole Units")]
static void SnapToWholeUnits(MenuCommand command)
{
    var t = (Transform)command.context;
    Undo.RecordObject(t, "Snap To Whole Units");
    Vector3 p = t.localPosition;
    t.localPosition = new Vector3(Mathf.Round(p.x), Mathf.Round(p.y), Mathf.Round(p.z));
}

Use the CONTEXT/ form for anything that changes saved data in Edit Mode, because it lets you register Undo explicitly, which is covered next.

Undo, Dirty Flags and Why Your Changes Vanish

The number one Editor scripting bug is a tool that “works” but whose changes can’t be undone, don’t mark the scene as modified, or quietly disappear after a restart. Unity only knows something changed if you tell it. These are the rules:

  • Before modifying an object: Undo.RecordObject(obj, "Action Name"). This also marks the scene dirty and records prefab-instance overrides.
  • After creating an object: Undo.RegisterCreatedObjectUndo(go, "Create X").
  • Deleting, adding components or reparenting: use Undo.DestroyObjectImmediate, Undo.AddComponent<T> and Undo.SetTransformParent instead of the plain versions.
  • Instantiating a prefab: use PrefabUtility.InstantiatePrefab. Plain Instantiate creates an unlinked copy with no blue prefab connection.
  • Changing an asset directly (a ScriptableObject, for example): call EditorUtility.SetDirty(asset), then AssetDatabase.SaveAssets() to write it to disk.

When a tool changes many objects, group everything into a single undo step so one Ctrl+Z reverts the whole operation:

[MenuItem("Tools/DevForge/Number Selected Objects")]
static void NumberSelected()
{
    GameObject[] selected = Selection.gameObjects;
    if (selected.Length == 0) return;

    Undo.SetCurrentGroupName("Number Selected Objects");
    int group = Undo.GetCurrentGroup();

    string baseName = selected[0].name;
    for (int i = 0; i < selected.Length; i++)
    {
        Undo.RecordObject(selected[i], "Rename");
        selected[i].name = $"{baseName}_{i + 1:00}";   // Crate_01, Crate_02, ...
    }

    Undo.CollapseUndoOperations(group);
}

Custom Inspectors the Right Way

A custom Inspector replaces or extends the default UI for one component type. Here’s the runtime component we’ll build tools for (this goes in a normal scripts folder, not an Editor folder):

using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] GameObject enemyPrefab;
    [SerializeField, Min(1)] int spawnCount = 5;
    [SerializeField] float spawnRadius = 8f;

    public float SpawnRadius { get => spawnRadius; set => spawnRadius = value; }

    public void SpawnEnemy()
    {
        Vector2 offset = Random.insideUnitCircle * spawnRadius;
        Vector3 position = transform.position + new Vector3(offset.x, 0f, offset.y);
        Instantiate(enemyPrefab, position, Quaternion.identity);
    }
}

The quick-and-dirty approach is to cast target and set its fields directly. That works for a button, but for editing values you should go through SerializedObject. It handles Undo, prefab overrides (the bold blue override markers), multi-object editing and dirty flags for you, with no extra code:

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(EnemySpawner))]
[CanEditMultipleObjects]
public class EnemySpawnerEditor : Editor
{
    SerializedProperty enemyPrefab, spawnCount, spawnRadius;

    void OnEnable()
    {
        // Strings must match the serialized field names exactly
        enemyPrefab = serializedObject.FindProperty("enemyPrefab");
        spawnCount  = serializedObject.FindProperty("spawnCount");
        spawnRadius = serializedObject.FindProperty("spawnRadius");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.PropertyField(enemyPrefab);
        EditorGUILayout.IntSlider(spawnCount, 1, 50);
        EditorGUILayout.PropertyField(spawnRadius);

        if (enemyPrefab.objectReferenceValue == null)
            EditorGUILayout.HelpBox("Assign an enemy prefab.", MessageType.Warning);

        serializedObject.ApplyModifiedProperties();

        // Runtime Instantiate in Edit Mode would create unlinked, un-undoable copies,
        // so this button only runs in Play Mode.
        using (new EditorGUI.DisabledScope(!Application.isPlaying))
        {
            if (GUILayout.Button("Spawn Test Enemy"))
                foreach (Object t in targets)
                    ((EnemySpawner)t).SpawnEnemy();
        }
    }
}

The Update() and ApplyModifiedProperties() pair is not optional. Leave out the second call and every edit is thrown away on the next repaint.

UI Toolkit alternative. Since Unity 2022.2 the default Inspector is built with UI Toolkit, and you can write custom Inspectors the same way by overriding CreateInspectorGUI. Fields bind to their properties automatically:

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

[CustomEditor(typeof(EnemySpawner))]
public class EnemySpawnerEditor : Editor
{
    public override VisualElement CreateInspectorGUI()
    {
        var root = new VisualElement();
        InspectorElement.FillDefaultInspector(root, serializedObject, this); // all default fields

        root.Add(new Button(() =>
        {
            if (Application.isPlaying) ((EnemySpawner)target).SpawnEnemy();
            else Debug.LogWarning("Enter Play Mode to spawn test enemies.");
        }) { text = "Spawn Test Enemy" });

        return root;
    }
}

Pick one style per Inspector. If CreateInspectorGUI returns an element, Unity uses it and ignores OnInspectorGUI. For bigger layouts, UI Toolkit lets you design the Inspector visually in UI Builder (Window > UI Toolkit > UI Builder) and load the resulting UXML.

Illustration of gears and code representing Unity editor scripting automation

๐Ÿงฐ Don’t want to hand-write every custom inspector?

Odin Inspector adds 100+ attributes like [ShowIf] and [TableList], so you get rich inspectors straight from your data classes without writing a custom editor per component. It’s one of the Asset Store tools we think is worth paying for.

Read our take on Odin Inspector →

Property Drawers: Fix a Field Everywhere at Once

A custom Inspector changes one component. A property drawer changes how a type or attribute is drawn in every Inspector. The classic example is a [ReadOnly] attribute for values you want to see but not edit:

// ReadOnlyAttribute.cs (runtime folder, NOT Editor, so your components can use it)
using UnityEngine;
public class ReadOnlyAttribute : PropertyAttribute { }

// Editor/ReadOnlyDrawer.cs
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        using (new EditorGUI.DisabledScope(true))
            EditorGUI.PropertyField(position, property, label, true);
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) =>
        EditorGUI.GetPropertyHeight(property, label, true);
}

// Usage in any MonoBehaviour or ScriptableObject:
// [SerializeField, ReadOnly] int currentWave;

You can also target a type instead of an attribute, for example [CustomPropertyDrawer(typeof(DamageRange))], to draw a small serializable struct on a single line wherever it appears.

Automating Asset Imports

On demand: batch-edit a selection. Right-click textures or whole folders in the Project window and convert them all at once. SelectionMode.DeepAssets is what makes folders work: without it, selecting a folder gives you the folder object, not the textures inside it.

using UnityEditor;
using UnityEngine;

public static class TextureBatchTool
{
    [MenuItem("Assets/DevForge/Convert Textures To Sprites")]
    static void ConvertToSprites()
    {
        Texture2D[] textures = Selection.GetFiltered<Texture2D>(SelectionMode.DeepAssets);
        int changed = 0;

        AssetDatabase.StartAssetEditing();   // batch the reimports instead of one at a time
        try
        {
            foreach (Texture2D tex in textures)
            {
                string path = AssetDatabase.GetAssetPath(tex);
                if (AssetImporter.GetAtPath(path) is TextureImporter importer &&
                    importer.textureType != TextureImporterType.Sprite)
                {
                    importer.textureType = TextureImporterType.Sprite;
                    importer.SaveAndReimport();
                    changed++;
                }
            }
        }
        finally
        {
            AssetDatabase.StopAssetEditing(); // always runs, even if something throws
        }
        Debug.Log($"Converted {changed} of {textures.Length} texture(s) to Sprite.");
    }
}

Automatically: an AssetPostprocessor. Better still, never set import settings by hand again. An AssetPostprocessor runs every time an asset is imported. This one gives every new texture dropped into Assets/Art/Sprites/ pixel-art settings:

using UnityEditor;
using UnityEngine;

public class PixelArtImportRules : AssetPostprocessor
{
    // Bump this number to make Unity reimport affected assets with the new rules
    public override uint GetVersion() => 1;

    void OnPreprocessTexture()
    {
        if (!assetPath.StartsWith("Assets/Art/Sprites/")) return;

        // Only on first import, so manual tweaks made later in the Inspector are respected
        if (!assetImporter.importSettingsMissing) return;

        var importer = (TextureImporter)assetImporter;
        importer.textureType = TextureImporterType.Sprite;
        importer.spritePixelsPerUnit = 16;
        importer.filterMode = FilterMode.Point;
        importer.textureCompression = TextureImporterCompression.Uncompressed;
        importer.mipmapEnabled = false;
    }
}

The importSettingsMissing guard matters. Without it, the postprocessor overwrites your settings on every reimport, and the Inspector seems to “ignore” your changes. The same pattern works for models (OnPreprocessModel) and audio (OnPreprocessAudio).

Editor Windows: Build Your Own Panels

An EditorWindow is a dockable panel like the Inspector or Console. This one shows live scene statistics, including a missing-script count. It recounts only when the hierarchy changes, not on every repaint:

using UnityEditor;
using UnityEngine;

public class LevelStatsWindow : EditorWindow
{
    int objectCount, rendererCount, lightCount, missingScripts;

    [MenuItem("Tools/DevForge/Level Stats")]
    static void ShowWindow() => GetWindow<LevelStatsWindow>("Level Stats");

    void OnEnable() => Recount();

    void OnHierarchyChange()  // called by Unity when anything in the open scenes changes
    {
        Recount();
        Repaint();
    }

    void Recount()
    {
        GameObject[] all = FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
        objectCount   = all.Length;
        rendererCount = FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length;
        lightCount    = FindObjectsByType<Light>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length;

        missingScripts = 0;
        foreach (GameObject go in all)
            missingScripts += GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(go);
    }

    void OnGUI()
    {
        EditorGUILayout.LabelField("GameObjects", objectCount.ToString());
        EditorGUILayout.LabelField("Renderers", rendererCount.ToString());
        EditorGUILayout.LabelField("Lights", lightCount.ToString());
        EditorGUILayout.LabelField("Missing scripts", missingScripts.ToString());

        if (GUILayout.Button("Recount")) Recount();
    }
}

Two notes. FindObjectsOfType is obsolete as of Unity 2023.1/Unity 6, and FindObjectsByType with FindObjectsSortMode.None is the faster replacement (available since 2021.3.18 and 2022.2). And OnGUI can run many times per second, so never do expensive searches inside it. To build the same window with UI Toolkit, put your UI in void CreateGUI() and add elements to rootVisualElement.

Scene View Tools with Handles and Gizmos

Draggable handles in the Scene view let designers tune values visually. Add OnSceneGUI to the EnemySpawnerEditor from earlier (the IMGUI version) to get a radius you can drag:

void OnSceneGUI()
{
    var spawner = (EnemySpawner)target;

    EditorGUI.BeginChangeCheck();
    float radius = Handles.RadiusHandle(Quaternion.identity, spawner.transform.position, spawner.SpawnRadius);
    if (EditorGUI.EndChangeCheck())
    {
        Undo.RecordObject(spawner, "Change Spawn Radius");
        spawner.SpawnRadius = radius;
    }
}

For display-only visuals you don’t need an Editor script at all. OnDrawGizmos and OnDrawGizmosSelected in the runtime component (Gizmos.DrawWireSphere, Gizmos.DrawLine) are stripped from builds automatically.

Validation: Catch Problems Before They Ship

Editor scripting is the best place to enforce rules instead of just documenting them. A common mistake is looping over FindObjectsOfType<MonoBehaviour>() and checking for null to find missing scripts. That never works, because a missing script isn’t a loadable MonoBehaviour, so it’s never returned. Use GameObjectUtility.GetMonoBehavioursWithMissingScriptCount instead, and scan every open scene, including inactive objects:

using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

public static class SceneValidator
{
    [MenuItem("Tools/DevForge/Validate Open Scenes")]
    static void ValidateOpenScenes()
    {
        int problems = 0;
        for (int i = 0; i < SceneManager.sceneCount; i++)
        {
            Scene scene = SceneManager.GetSceneAt(i);
            if (!scene.isLoaded) continue;

            foreach (GameObject root in scene.GetRootGameObjects())
            foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
            {
                int missing = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(t.gameObject);
                if (missing > 0)
                {
                    problems++;
                    Debug.LogError($"{missing} missing script(s) on '{t.name}' in {scene.name}", t.gameObject);
                }
            }
        }
        Debug.Log(problems == 0 ? "Validation passed." : $"Validation found {problems} problem object(s).");
    }
}

To make the check impossible to forget, run it during every build. IProcessSceneWithReport is called once per scene as it’s built, and throwing a BuildFailedException stops the build with your message:

using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MissingScriptBuildCheck : IProcessSceneWithReport
{
    public int callbackOrder => 0;

    public void OnProcessScene(Scene scene, BuildReport report)
    {
        if (report == null) return; // also called when entering Play Mode; only enforce in real builds

        foreach (GameObject root in scene.GetRootGameObjects())
        foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
            if (GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(t.gameObject) > 0)
                throw new BuildFailedException($"Missing script on '{t.name}' in {scene.path}");
    }
}

The same approach works for naming rules, like flagging misnamed assets or scripts outside the agreed folder structure, so drift gets caught before review does. If your project doesn’t have a written standard yet, start with our Naming Convention Reference. For a full walkthrough of finding and removing missing scripts in scenes and prefabs, see How to Fix Missing Script References in Unity.

Running Code Automatically: InitializeOnLoad

A class marked [InitializeOnLoad] has its static constructor run whenever the Editor starts or scripts recompile. That’s where you subscribe to Editor events. This one saves your open scenes every time you press Play, so a crash in Play Mode can’t cost you unsaved work:

using UnityEditor;
using UnityEditor.SceneManagement;

[InitializeOnLoad]
static class SaveOnPlay
{
    static SaveOnPlay()
    {
        EditorApplication.playModeStateChanged += state =>
        {
            if (state == PlayModeStateChange.ExitingEditMode)
            {
                EditorSceneManager.SaveOpenScenes();
                AssetDatabase.SaveAssets();
            }
        };
    }
}

Other useful hooks: EditorApplication.hierarchyChanged, EditorApplication.update (runs every Editor tick, so keep it light), Selection.selectionChanged, and [InitializeOnLoadMethod] on a static method if you don’t need a whole class.

Which API for Which Job

You want toโ€ฆ Use Where it lives
Run a command from a menu or shortcut [MenuItem] Editor folder
Add a command to a component’s โ‹ฎ menu [MenuItem("CONTEXT/Type/โ€ฆ")] or [ContextMenu] Editor folder / the component
Change how one component looks [CustomEditor] + CreateInspectorGUI or OnInspectorGUI Editor folder
Change how a field type or attribute looks everywhere [CustomPropertyDrawer] + PropertyAttribute Drawer in Editor, attribute in runtime
Build a dockable tool panel EditorWindow Editor folder
Drag values in the Scene view OnSceneGUI + Handles Editor folder
Draw debug shapes in the Scene view OnDrawGizmos + Gizmos The component
Enforce import settings AssetPostprocessor Editor folder
Run code on Editor load or recompile [InitializeOnLoad] + EditorApplication events Editor folder
Block a bad build IProcessSceneWithReport, IPreprocessBuildWithReport Editor folder
Create data assets from the Create menu [CreateAssetMenu] on a ScriptableObject Runtime

Troubleshooting

“error CS0246: The type or namespace name ‘UnityEditor’ could not be found” when building

The Editor works fine, but the build fails because some script using UnityEditor is being compiled into the player. Find the file named in the error and either move it into an Editor folder, give its folder an Editor-only asmdef (remember that Editor folders inside a runtime asmdef don’t count), or wrap the Editor-only lines in #if UNITY_EDITOR.

My menu item doesn’t show up

Check the Console for compile errors first, because nothing new appears until every script compiles. The method must be static. The path needs at least one submenu: "MyCommand" is rejected, while "Tools/MyCommand" works. If the item is there but greyed out, its validation function is returning false.

My Inspector changes don’t save, or revert after reopening the scene

You’re probably writing to target directly. Switch to serializedObject + ApplyModifiedProperties(), or call Undo.RecordObject before the change. For ScriptableObject assets changed from code, add EditorUtility.SetDirty(asset) and AssetDatabase.SaveAssets().

NullReferenceException on a SerializedProperty

FindProperty returned null. Either the string doesn’t exactly match the field name, or the field isn’t serialized. Properties, static fields, [NonSerialized] fields and private fields without [SerializeField] are all invisible to it. For an auto-property marked [field: SerializeField], the serialized name is <Name>k__BackingField.

Spawned prefabs aren’t blue in the Hierarchy

You used Instantiate, which makes a plain copy. In Editor tools, use PrefabUtility.InstantiatePrefab(prefab) followed by Undo.RegisterCreatedObjectUndo.

The Editor got sluggish after adding my tool

Something expensive is running in OnGUI, OnSceneGUI or EditorApplication.update, which can run many times per second. Cache results and recompute on events like OnHierarchyChange or a button click. The Profiler in Edit Mode (Profiler window > target dropdown > Edit Mode) will point straight at it.

FAQ

Do Editor scripts end up in my game build?

No, as long as they’re in an Editor folder, in an Editor-only assembly definition, or inside #if UNITY_EDITOR. They add nothing to build size or runtime cost.

Should I learn IMGUI or UI Toolkit for Editor tools?

Learn enough IMGUI (OnInspectorGUI, EditorGUILayout) to read existing tools and Asset Store code, since most of it still uses IMGUI. Build new, larger tools with UI Toolkit: it’s what Unity’s own Editor now uses, it supports UI Builder and styling, and it only redraws when something changes. For small one-off tools, either is fine.

Do Editor tools work in Play Mode?

Yes. Inspectors and Editor windows keep running. Remember that changes to scene objects made during Play Mode are reverted when you exit, but changes to assets on disk (ScriptableObjects, prefab assets) persist.

How do I reuse my tools across projects?

Move them into a local package: a folder with a package.json and an Editor-only asmdef. Add it via Package Manager > + > Install package from disk (or from a Git URL). Every project then gets updates from one source.

Why does my Editor tool break after a Unity upgrade?

Editor APIs change more often than runtime APIs. Read the upgrade guide for your version, fix obsolete-API warnings as they appear rather than letting them pile up, and keep your tools in their own asmdef so compile errors are isolated from gameplay code.

What is the difference between EditorGUI and EditorGUILayout?

EditorGUILayout positions controls automatically one under another, which is what you want in OnInspectorGUI and editor windows. EditorGUI needs an explicit Rect for every control, which is required inside property drawers’ OnGUI(Rect, ...).

When does OnValidate run?

In the Editor only: when the script loads and whenever a value changes in the Inspector. Use it to clamp or validate fields, not to create objects or change other assets, because it can run at times you don’t control.

Where to Go Next

Start small. The next time you catch yourself doing the same three clicks in the Inspector for the fifth time in a session, that’s your signal: write a ten-line MenuItem script instead. Once that feels natural, add an AssetPostprocessor for your import settings and a validation check for your build, which are the two tools that pay for themselves fastest. The return on even basic automation is immediate and compounds over the life of a project.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *