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-эквивалент гораздо более глубокий — может менять сам редактор.
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 ваши 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 полями.
MenuItem — пункты меню
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.
Скрипт в редакторе вызывает 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]↔ GodotEditorInspectorPlugin._parse_propertyEditorWindow↔ GodotEditorPlugin.add_control_to_dock[MenuItem]↔ GodotEditorPlugin.add_tool_menu_itemOnDrawGizmos↔ GodotEditorNode3DGizmoPlugin._redraw[ExecuteAlways]↔ Godot@tool
В Godot всё это пишется на GDScript. В Unity — на C# в отдельном assembly.