~2 мин чтения

Editor Scripting — расширение Unity Editor

Custom Inspectors, EditorWindow, MenuItem, Gizmos, ExecuteAlways — расширение редактора Unity.

Unity Editor можно расширять C#-кодом так же, как и runtime-логику. Это позволяет:

  • Делать custom inspectors для нестандартного UI редактирования полей.
  • Создавать editor windows — отдельные панели-инструменты внутри редактора.
  • Добавлять пункты меню, gizmos, scene view handles.
  • Запускать ваш код в редакторе (не только в Play Mode).

Editor scripts — главное правило

Скрипты редактора живут в папке Editor/ (любой вложенной), и собираются в отдельный assembly, который не попадает в билд игры.

Assets/
└── Editor/
    └── EnemyEditor.cs           ← Editor-скрипт
└── Scripts/
    └── Enemy.cs                  ← Runtime-скрипт

Внутри Editor-скриптов вы можете использовать using UnityEditor; (этот namespace недоступен в runtime).

Веб

DevTools extensions для Chrome: вы пишете JS, который добавляет панель в DevTools для отладки своих React-приложений. Только Unity-эквивалент гораздо более глубокий — может менять сам редактор.

Unity

Editor-скрипт — отдельная сборка, которая в runtime не существует. UI редактора рисуется через IMGUI или UI Toolkit. Полный доступ к Selection, AssetDatabase, EditorPrefs.

Custom Inspector — переопределить UI

Стандартный Inspector рисует поля по дефолтным правилам. Хотите вместо обычного float-поля видеть кнопку “Heal” или цветной слайдер? Делаете CustomEditor:

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Enemy))]
public class EnemyEditor : Editor
{
    public override void OnInspectorGUI() {
        // Сначала — стандартный inspector (можно убрать, если хочешь полный контроль)
        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);
    }
}

Теперь в Inspector у компонента Enemy будут две кнопки + цветная подсказка. Особенно полезно для тестирования с прямо во время Play Mode.

Undo.RecordObject — обязательно перед изменением

Без Undo.RecordObject ваши edits не попадут в Ctrl+Z. Это плохо UX — пользователь нажал “Reset Health”, потом передумал, Ctrl+Z ничего не делает. Запись Undo — стандарт хорошего тона.

SerializedProperty — правильный путь

Для сериализованных полей используйте SerializedObject / SerializedProperty — это аккуратнее, чем напрямую читать target:

[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 сам обработает multi-object editing (выделили 5 врагов → правка применилась ко всем) и Undo.

EditorWindow — собственная панель

Если функционал не про конкретный компонент, а про “tool” (генератор уровней, ассет-инспектор):

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");
    }
}

После сохранения в меню Tools появится “Level Generator”. Открывается отдельным окном с configurable полями.

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) { // только 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;
    }
}

Первый — пункт в главном меню. Второй — пункт в контекстном меню компонента Light (правый клик на Light в Inspector).

OnDrawGizmos — визуализация в Scene View

Самый частый случай Editor scripting — рисовать debug-форму прямо в 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() {
        // Рисуется только когда выбран объект
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, 2f); // зона видимости
    }
}

OnDrawGizmos — стандартная Unity-функция, не требует Editor-папки. Работает прямо в runtime скрипте.

ExecuteAlways — runtime-логика в редакторе

Хотите, чтобы ваш скрипт обновлялся в Edit Mode (без Play)? Атрибут [ExecuteAlways]:

[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);
    }
}

Теперь “стрелка компаса” поворачивается к target прямо в Scene View, без Play Mode. Аналог Godot’s @tool.

ExecuteAlways — осторожно

Скрипт в редакторе вызывает Update каждый кадр. Тяжёлый код тормозит редактор. Бесконечный цикл повесит Unity. Перед ExecuteAlways — взвесьте, действительно ли нужно.

Asset Postprocessor

Хотите автоматически выставлять Compression Mode для всех импортированных PNG? AssetPostprocessor:

public class TexturePostprocessor : AssetPostprocessor
{
    void OnPreprocessTexture() {
        var importer = (TextureImporter)assetImporter;
        if (assetPath.Contains("/UI/")) {
            importer.textureCompression = TextureImporterCompression.Uncompressed;
            importer.spritePixelsPerUnit = 100;
        }
    }
}

Этот код срабатывает при первом импорте каждого PNG. Очень удобно для проектов с правилами “UI-текстуры должны быть несжатыми, иначе размытие”.

Что почитать дальше

  • UI Toolkit for Editor (UIElements) — современный путь делать сложный editor UI, заменяет IMGUI. Поддерживается UXML+USS как в runtime UI Toolkit.
  • CustomPropertyDrawer — для одного типа свойства, чтобы Unity рисовал его одинаково во всех inspector’ах.
  • EditorPrefs — хранение per-machine настроек вашего инструмента.

Сравнение с Godot

Концепции 1:1:

  • [CustomEditor] ↔ Godot EditorInspectorPlugin._parse_property
  • EditorWindow ↔ Godot EditorPlugin.add_control_to_dock
  • [MenuItem] ↔ Godot EditorPlugin.add_tool_menu_item
  • OnDrawGizmos ↔ Godot EditorNode3DGizmoPlugin._redraw
  • [ExecuteAlways] ↔ Godot @tool

В Godot всё это пишется на GDScript. В Unity — на C# в отдельном assembly.