From unity
Unity 6 editor scripting and custom tools guide. Use when creating custom inspectors, EditorWindows, PropertyDrawers, Gizmos, Handles, menu items, or editor extensions. Covers SerializedObject/SerializedProperty pattern, AssetDatabase, and editor-only code with #if UNITY_EDITOR. Based on Unity 6.3 LTS documentation.
npx claudepluginhub cdata/aria-skills --plugin unityThis skill uses the workspace's default tool permissions.
Editor scripts extend the Unity Editor with custom inspectors, windows, tools, and workflows. All editor code must either live in an `Editor/` folder or be wrapped in `#if UNITY_EDITOR` / `#endif` -- this ensures editor code is stripped from runtime builds.
Guides Unity editor scripting for custom inspectors, EditorWindows, PropertyDrawers; build pipelines, CI/CD with GameCI/GitHub Actions; Unity Test Framework; version control and packages.
Creates Unity editor tools to make content production safer, faster, and less dependent on tribal knowledge. Useful for manual workflows, validation, and safer artist/designer flows.
Unity 6 UI development guide. Use when building user interfaces, menus, HUD, buttons, or any UI elements. Covers UI Toolkit (recommended for new projects — USS, UXML, UI Builder, data binding), uGUI/Canvas (legacy runtime UI), and IMGUI. Based on Unity 6.3 LTS documentation.
Share bugs, ideas, or general feedback.
Editor scripts extend the Unity Editor with custom inspectors, windows, tools, and workflows. All editor code must either live in an Editor/ folder or be wrapped in #if UNITY_EDITOR / #endif -- this ensures editor code is stripped from runtime builds.
Unity 6.3 supports two GUI frameworks for editor extensions:
| Framework | Status | Entry Point |
|---|---|---|
| UI Toolkit | Recommended | CreateInspectorGUI() / CreateGUI() returning VisualElement |
| IMGUI | Legacy, fully functional | OnInspectorGUI() / OnGUI() |
Key base classes: Editor, EditorWindow, PropertyDrawer, DecoratorDrawer, ScriptableWizard.
Derive from Editor, apply [CustomEditor(typeof(TargetType))]. See references/custom-inspectors.md for full guide.
[CustomEditor(typeof(MyPlayer))]
public class MyPlayerEditor : Editor
{
public override VisualElement CreateInspectorGUI()
{
VisualElement root = new VisualElement();
var visualTree = Resources.Load("MyPlayerEditor") as VisualTreeAsset;
visualTree.CloneTree(root);
return root;
}
}
When CreateInspectorGUI() is overridden, OnInspectorGUI() is ignored.
[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
SerializedProperty lookAtPoint;
void OnEnable() { lookAtPoint = serializedObject.FindProperty("lookAtPoint"); }
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(lookAtPoint);
serializedObject.ApplyModifiedProperties();
}
}
| Member | Description |
|---|---|
target / targets | Inspected object(s) |
serializedObject | SerializedObject for the target(s) |
DrawDefaultInspector() | Renders built-in default inspector |
OnSceneGUI() | Draw interactive Handles in Scene View |
public void OnSceneGUI()
{
var t = (LookAtPoint)target;
EditorGUI.BeginChangeCheck();
Vector3 pos = Handles.PositionHandle(t.lookAtPoint, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Move look-at point");
t.lookAtPoint = pos;
}
}
Customize how serialized fields appear in the Inspector. See references/property-drawers.md for full guide.
1. Define attribute (runtime code, outside Editor folder):
public class MyRangeAttribute : PropertyAttribute
{
public readonly float min, max;
public MyRangeAttribute(float min, float max) { this.min = min; this.max = max; }
}
2. Implement drawer (Editor folder):
[CustomPropertyDrawer(typeof(MyRangeAttribute))]
public class MyRangeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
MyRangeAttribute range = (MyRangeAttribute)attribute;
if (property.propertyType == SerializedPropertyType.Float)
EditorGUI.Slider(position, property, range.min, range.max, label);
else if (property.propertyType == SerializedPropertyType.Integer)
EditorGUI.IntSlider(position, property, (int)range.min, (int)range.max, label);
}
}
Critical rules: Must be in Editor/ folder. Use EditorGUI (not EditorGUILayout). Use EditorGUI.BeginProperty()/EndProperty() for prefab overrides. Cannot mix UI Toolkit and IMGUI in one drawer.
Custom dockable editor windows. See references/editor-windows.md for full guide.
Lifecycle: OnEnable -> CreateGUI -> Update -> OnGUI -> OnDisable
public class MyToolWindow : EditorWindow
{
[MenuItem("Tools/My Tool Window")]
public static void ShowWindow()
{
var window = GetWindow<MyToolWindow>();
window.titleContent = new GUIContent("My Tool");
window.minSize = new Vector2(300, 200);
}
public void CreateGUI()
{
rootVisualElement.Add(new Label("Hello from UI Toolkit!"));
rootVisualElement.Add(new Button(() => Debug.Log("Clicked!")) { text = "Click Me" });
}
}
| Static Method | Description |
|---|---|
GetWindow<T>() | Get existing or create new |
CreateWindow<T>() | Always create new instance |
HasOpenInstances<T>() | Check if open |
| Display Mode | Behavior |
|---|---|
Show() | Standard dockable |
ShowUtility() | Floating, not dockable |
ShowModal() | Modal dialog |
ShowAsDropDown() | Dropdown popup |
The [MenuItem] attribute converts static methods into menu commands.
[MenuItem("Menu/Path/Item Name", isValidateFunction, priority)]
| Prefix | Placement |
|---|---|
"Tools/..." | Custom tools menu |
"Assets/..." | Assets menu + Project context menu |
"GameObject/..." | GameObject menu + Hierarchy context menu |
"CONTEXT/ComponentType/..." | Component context menu |
Shortcut modifiers: % = Cmd/Ctrl, # = Shift, & = Alt, ^ = Ctrl (all), _ = no modifier.
// Validation function controls when menu item is enabled
[MenuItem("Tools/Reset Position", true)]
static bool ValidateResetPosition() => Selection.activeTransform != null;
[MenuItem("Tools/Reset Position", false)]
static void ResetPosition()
{
Undo.RecordObject(Selection.activeTransform, "Reset Position");
Selection.activeTransform.position = Vector3.zero;
}
GameObject creation (use priority 10):
[MenuItem("GameObject/Custom/My Object", false, 10)]
static void CreateMyObject(MenuCommand menuCommand)
{
var go = new GameObject("My Object");
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
Undo.RegisterCreatedObjectUndo(go, "Create My Object");
Selection.activeObject = go;
}
All drawing inside OnDrawGizmos() (always) or OnDrawGizmosSelected() (when selected):
void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, radius);
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawSphere(transform.position, radius);
}
Methods: DrawLine, DrawRay, DrawSphere, DrawWireSphere, DrawCube, DrawWireCube, DrawMesh, DrawWireMesh, DrawFrustum, DrawIcon. Properties: color, matrix.
Used inside OnSceneGUI() on custom Editors:
[CustomEditor(typeof(CircleLayout))]
public class CircleLayoutEditor : Editor
{
public void OnSceneGUI()
{
var t = (CircleLayout)target;
Handles.color = new Color(1f, 0.8f, 0.4f, 1f);
Handles.DrawWireDisc(t.transform.position, t.transform.up, t.radius);
Handles.Label(t.transform.position, $"Radius: {t.radius:F1}");
EditorGUI.BeginChangeCheck();
float newRadius = Handles.RadiusHandle(Quaternion.identity, t.transform.position, t.radius);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(t, "Change Radius");
t.radius = newRadius;
}
}
}
Methods: PositionHandle, RotationHandle, ScaleHandle, FreeMoveHandle, RadiusHandle, DrawLine, DrawWireDisc, Label, BeginGUI/EndGUI. Properties: color, matrix.
The standard way to edit properties in editor scripts. Provides automatic Undo, multi-object editing, and prefab override tracking.
Three-step pattern:
serializedObject.Update(); // 1. Sync from target
EditorGUILayout.PropertyField(serializedObject.FindProperty("myField")); // 2. Draw/modify
serializedObject.ApplyModifiedProperties(); // 3. Apply with undo
| Member | Description |
|---|---|
Update() | Refresh from target |
ApplyModifiedProperties() | Commit changes with undo |
FindProperty(string) | Get SerializedProperty by name |
GetIterator() | Iterate all properties |
targetObject / targetObjects | Inspected object(s) |
hasModifiedProperties | True when unapplied changes exist |
For multi-object editing: add [CanEditMultipleObjects] and always use SerializedProperty instead of target.
Programmatic asset access in editor scripts.
// Create
var data = ScriptableObject.CreateInstance<MyData>();
AssetDatabase.CreateAsset(data, "Assets/Data/MyData.asset");
AssetDatabase.SaveAssets();
// Load
var loaded = AssetDatabase.LoadAssetAtPath<MyData>("Assets/Data/MyData.asset");
// Find by type
string[] guids = AssetDatabase.FindAssets("t:MyData");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<MyData>(path);
}
// Other operations
AssetDatabase.GetAssetPath(myObject);
AssetDatabase.DeleteAsset("Assets/Data/OldData.asset");
AssetDatabase.Refresh();
Key methods: CreateAsset, LoadAssetAtPath<T>, FindAssets, GetAssetPath, AssetPathToGUID/GUIDToAssetPath, SaveAssets, Refresh, DeleteAsset, CopyAsset, MoveAsset, RenameAsset, ImportAsset.
// Editor-only guard
#if UNITY_EDITOR
using UnityEditor;
// editor code
#endif
// Change detection
EditorGUI.BeginChangeCheck();
// ... controls ...
if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(target, "Change"); }
// ScriptableObject creation menu
[CreateAssetMenu(fileName = "NewConfig", menuName = "Game/Config Data", order = 1)]
public class ConfigData : ScriptableObject { public float moveSpeed = 5f; }
// Default inspector + extras
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Do Something")) { ((MyComponent)target).DoSomething(); }
}
}
| Anti-Pattern | Problem | Fix |
|---|---|---|
Editing target without Undo | No undo, no dirty flag | Use SerializedObject or Undo.RecordObject() |
target with [CanEditMultipleObjects] | Only modifies first object | Use SerializedProperty |
Editor code outside Editor/ without #if UNITY_EDITOR | Build failures | Use Editor/ folder or preprocessor guard |
EditorGUILayout in PropertyDrawer | Not supported | Use EditorGUI with Rect |
Missing serializedObject.Update() | Stale data | Always call before reading |
Missing ApplyModifiedProperties() | Changes lost | Always call after modifications |
| Mixing UI Toolkit + IMGUI in one PropertyDrawer | Not supported | Choose one framework |
Missing Undo.RegisterCreatedObjectUndo() | Cannot undo creation | Always register new objects |
Missing GameObjectUtility.SetParentAndAlign() | Wrong hierarchy parenting | Use in GameObject menu items |
Heavy work in OnInspectorGUI()/OnGUI() | Lag (called many times/frame) | Cache in OnEnable() |
Editor: [CustomEditor(typeof(T))], [CanEditMultipleObjects]
CreateInspectorGUI(), OnInspectorGUI(), OnSceneGUI()
DrawDefaultInspector(), target, targets, serializedObject
EditorWindow: GetWindow<T>(), CreateWindow<T>(), CreateGUI(), OnGUI()
rootVisualElement, titleContent, Show/ShowUtility/ShowModal
PropertyDrawer: [CustomPropertyDrawer(typeof(T))]
CreatePropertyGUI(), OnGUI(rect,prop,label), GetPropertyHeight()
attribute, fieldInfo, preferredLabel
SerializedObject: Update(), ApplyModifiedProperties(), FindProperty()
MenuItem: [MenuItem("Path", validate, priority)], % # & ^ _ shortcuts
Gizmos: DrawLine/Sphere/WireSphere/Cube/WireCube/Ray/Mesh/Icon, color, matrix
Handles: PositionHandle/RotationHandle/ScaleHandle/FreeMoveHandle/RadiusHandle
DrawLine/DrawWireDisc/Label, BeginGUI/EndGUI, color, matrix
AssetDatabase: CreateAsset, LoadAssetAtPath<T>, FindAssets, SaveAssets, Refresh