Unity Editor IMGUI Design Skill
WARNING: IMGUI is for Unity Editor extensions ONLY. For game UI, use unity-game-ugui-design or unity-game-ui-toolkit-design .
IMGUI vs Game UI
Aspect IMGUI (Editor) uGUI / UI Toolkit (Game)
Purpose Editor tools, inspectors In-game UI, HUD
Build inclusion Editor only Included in builds
Rendering Immediate mode Retained mode
Performance OK for editor Optimized for runtime
Styling Limited (GUISkin) Rich (USS, materials)
Quick Start
Create EditorWindow
// Create editor window script mcp__unity-mcp-server__create_class({ path: "Assets/Editor/MyToolWindow.cs", className: "MyToolWindow", namespace: "MyProject.Editor", baseType: "EditorWindow", usings: "UnityEditor" })
// Add window content mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/MyToolWindow.cs", symbolName: "MyToolWindow", operation: "insert_after", newText: ` [MenuItem("Tools/My Tool Window")] public static void ShowWindow() { GetWindow<MyToolWindow>("My Tool"); }
private void OnGUI()
{
GUILayout.Label("My Tool", EditorStyles.boldLabel);
if (GUILayout.Button("Do Something"))
{
Debug.Log("Button clicked!");
}
}
` })
Create Custom Inspector
// Create custom inspector script mcp__unity-mcp-server__create_class({ path: "Assets/Editor/MyComponentEditor.cs", className: "MyComponentEditor", namespace: "MyProject.Editor", baseType: "Editor", usings: "UnityEditor" })
// Add CustomEditor attribute and OnInspectorGUI mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/MyComponentEditor.cs", symbolName: "MyComponentEditor", operation: "insert_before", newText: "[CustomEditor(typeof(MyComponent))]\n" })
mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/MyComponentEditor.cs", symbolName: "MyComponentEditor", operation: "insert_after", newText: ` public override void OnInspectorGUI() { serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("myField"));
if (GUILayout.Button("Custom Button"))
{
((MyComponent)target).DoSomething();
}
serializedObject.ApplyModifiedProperties();
}
` })
Core Concepts
IMGUI Lifecycle
OnGUI() called every frame (or on repaint/input) ├── Layout Event: Calculate sizes ├── Repaint Event: Draw controls └── Input Events: Handle mouse/keyboard
Event Types
void OnGUI() { Event e = Event.current;
switch (e.type)
{
case EventType.Layout:
// Calculate layout
break;
case EventType.Repaint:
// Draw visuals
break;
case EventType.MouseDown:
// Handle click
break;
case EventType.KeyDown:
// Handle keyboard
break;
}
}
GUILayout vs EditorGUILayout
GUILayout EditorGUILayout
Works everywhere Editor only
Basic controls Rich editor controls
GUILayout.Button
EditorGUILayout.PropertyField
GUILayout.TextField
EditorGUILayout.ObjectField
GUILayout.Label
EditorGUILayout.HelpBox
EditorWindow Patterns
Basic Window Structure
using UnityEngine; using UnityEditor;
public class MyToolWindow : EditorWindow { // Serialized state (survives recompile) [SerializeField] private string searchText = ""; [SerializeField] private Vector2 scrollPosition;
// Non-serialized state
private GUIStyle headerStyle;
[MenuItem("Tools/My Tool %#t")] // Ctrl+Shift+T
public static void ShowWindow()
{
var window = GetWindow<MyToolWindow>();
window.titleContent = new GUIContent("My Tool", EditorGUIUtility.IconContent("d_Settings").image);
window.minSize = new Vector2(300, 200);
window.Show();
}
private void OnEnable()
{
// Initialize when window opens
}
private void OnDisable()
{
// Cleanup when window closes
}
private void OnGUI()
{
InitStyles();
DrawToolbar();
DrawContent();
}
private void InitStyles()
{
headerStyle ??= new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14,
alignment = TextAnchor.MiddleCenter
};
}
private void DrawToolbar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
Refresh();
}
GUILayout.FlexibleSpace();
searchText = EditorGUILayout.TextField(searchText, EditorStyles.toolbarSearchField, GUILayout.Width(200));
}
}
private void DrawContent()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
{
GUILayout.Label("Content Here", headerStyle);
}
EditorGUILayout.EndScrollView();
}
private void Refresh()
{
Repaint();
}
}
MCP Implementation
// Create the window class mcp__unity-mcp-server__create_class({ path: "Assets/Editor/Tools/AssetBrowserWindow.cs", className: "AssetBrowserWindow", namespace: "MyProject.Editor.Tools", baseType: "EditorWindow", usings: "UnityEditor,System.Collections.Generic,System.Linq" })
// Add complete implementation mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/Tools/AssetBrowserWindow.cs", symbolName: "AssetBrowserWindow", operation: "insert_after", newText: ` [SerializeField] private string searchFilter = ""; [SerializeField] private Vector2 scrollPos; private List<string> assetPaths = new List<string>();
[MenuItem("Tools/Asset Browser")]
public static void ShowWindow()
{
GetWindow<AssetBrowserWindow>("Asset Browser");
}
private void OnEnable()
{
RefreshAssets();
}
private void OnGUI()
{
DrawSearchBar();
DrawAssetList();
}
private void DrawSearchBar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
EditorGUI.BeginChangeCheck();
searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField);
if (EditorGUI.EndChangeCheck())
{
RefreshAssets();
}
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
RefreshAssets();
}
}
}
private void DrawAssetList()
{
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
foreach (var path in assetPaths)
{
using (new EditorGUILayout.HorizontalScope())
{
var icon = AssetDatabase.GetCachedIcon(path);
GUILayout.Label(icon, GUILayout.Width(20), GUILayout.Height(20));
if (GUILayout.Button(System.IO.Path.GetFileName(path), EditorStyles.linkLabel))
{
Selection.activeObject = AssetDatabase.LoadAssetAtPath<Object>(path);
EditorGUIUtility.PingObject(Selection.activeObject);
}
}
}
EditorGUILayout.EndScrollView();
}
private void RefreshAssets()
{
var filter = string.IsNullOrEmpty(searchFilter) ? "t:Object" : searchFilter;
assetPaths = AssetDatabase.FindAssets(filter)
.Select(AssetDatabase.GUIDToAssetPath)
.Take(100)
.ToList();
Repaint();
}
` })
Custom Inspector Patterns
Basic Inspector
using UnityEngine; using UnityEditor;
[CustomEditor(typeof(EnemySpawner))] public class EnemySpawnerEditor : Editor { private SerializedProperty spawnPrefab; private SerializedProperty spawnInterval; private SerializedProperty maxEnemies;
private void OnEnable()
{
spawnPrefab = serializedObject.FindProperty("spawnPrefab");
spawnInterval = serializedObject.FindProperty("spawnInterval");
maxEnemies = serializedObject.FindProperty("maxEnemies");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(spawnPrefab);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Spawn Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(spawnInterval);
EditorGUILayout.PropertyField(maxEnemies);
EditorGUILayout.Space();
// Custom button
if (GUILayout.Button("Spawn Test Enemy"))
{
var spawner = (EnemySpawner)target;
spawner.SpawnEnemy();
}
serializedObject.ApplyModifiedProperties();
}
}
MCP Implementation
// Create inspector class mcp__unity-mcp-server__create_class({ path: "Assets/Editor/Inspectors/WeaponDataEditor.cs", className: "WeaponDataEditor", namespace: "MyProject.Editor", baseType: "Editor", usings: "UnityEditor" })
// Add CustomEditor attribute mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/Inspectors/WeaponDataEditor.cs", symbolName: "WeaponDataEditor", operation: "insert_before", newText: "[CustomEditor(typeof(WeaponData))]\n" })
// Add inspector implementation mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/Inspectors/WeaponDataEditor.cs", symbolName: "WeaponDataEditor", operation: "insert_after", newText: ` private SerializedProperty weaponName; private SerializedProperty damage; private SerializedProperty attackSpeed; private SerializedProperty weaponType;
private bool showStats = true;
private void OnEnable()
{
weaponName = serializedObject.FindProperty("weaponName");
damage = serializedObject.FindProperty("damage");
attackSpeed = serializedObject.FindProperty("attackSpeed");
weaponType = serializedObject.FindProperty("weaponType");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header
EditorGUILayout.LabelField("Weapon Configuration", EditorStyles.boldLabel);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(weaponName);
EditorGUILayout.PropertyField(weaponType);
EditorGUILayout.Space();
// Foldout section
showStats = EditorGUILayout.Foldout(showStats, "Combat Stats", true);
if (showStats)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(damage);
EditorGUILayout.PropertyField(attackSpeed);
// Calculated DPS
float dps = damage.floatValue * attackSpeed.floatValue;
EditorGUILayout.HelpBox($"DPS: {dps:F1}", MessageType.Info);
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
` })
Foldout Groups
private bool showAdvanced = false;
public override void OnInspectorGUI() { serializedObject.Update();
// Basic properties
EditorGUILayout.PropertyField(serializedObject.FindProperty("basicField"));
// Foldout group
showAdvanced = EditorGUILayout.Foldout(showAdvanced, "Advanced Settings", true);
if (showAdvanced)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(serializedObject.FindProperty("advancedField1"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("advancedField2"));
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
Conditional Display
public override void OnInspectorGUI() { serializedObject.Update();
var enableFeature = serializedObject.FindProperty("enableFeature");
EditorGUILayout.PropertyField(enableFeature);
// Show only when enabled
if (enableFeature.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(serializedObject.FindProperty("featureValue"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("featureMode"));
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
Property Drawer Patterns
Basic Property Drawer
using UnityEngine; using UnityEditor;
[CustomPropertyDrawer(typeof(MinMaxRange))] public class MinMaxRangeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
var minProp = property.FindPropertyRelative("min");
var maxProp = property.FindPropertyRelative("max");
float min = minProp.floatValue;
float max = maxProp.floatValue;
// MinMax slider
var sliderRect = new Rect(position.x, position.y, position.width - 100, position.height);
var minRect = new Rect(position.x + position.width - 95, position.y, 45, position.height);
var maxRect = new Rect(position.x + position.width - 45, position.y, 45, position.height);
EditorGUI.MinMaxSlider(sliderRect, ref min, ref max, 0f, 100f);
min = EditorGUI.FloatField(minRect, min);
max = EditorGUI.FloatField(maxRect, max);
minProp.floatValue = min;
maxProp.floatValue = max;
EditorGUI.EndProperty();
}
}
MCP Implementation
// Create property drawer mcp__unity-mcp-server__create_class({ path: "Assets/Editor/PropertyDrawers/ColorRangeDrawer.cs", className: "ColorRangeDrawer", namespace: "MyProject.Editor", baseType: "PropertyDrawer", usings: "UnityEditor" })
// Add attribute and implementation mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/PropertyDrawers/ColorRangeDrawer.cs", symbolName: "ColorRangeDrawer", operation: "insert_before", newText: "[CustomPropertyDrawer(typeof(ColorRange))]\n" })
mcp__unity-mcp-server__edit_structured({ path: "Assets/Editor/PropertyDrawers/ColorRangeDrawer.cs", symbolName: "ColorRangeDrawer", operation: "insert_after", newText: ` public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
var startColor = property.FindPropertyRelative("startColor");
var endColor = property.FindPropertyRelative("endColor");
float halfWidth = position.width / 2 - 2;
var startRect = new Rect(position.x, position.y, halfWidth, position.height);
var endRect = new Rect(position.x + halfWidth + 4, position.y, halfWidth, position.height);
EditorGUI.PropertyField(startRect, startColor, GUIContent.none);
EditorGUI.PropertyField(endRect, endColor, GUIContent.none);
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
` })
Multi-Line Property Drawer
[CustomPropertyDrawer(typeof(DialogLine))] public class DialogLineDrawer : PropertyDrawer { public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { // 3 lines + spacing return EditorGUIUtility.singleLineHeight * 3 + 4; }
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
float lineHeight = EditorGUIUtility.singleLineHeight;
var speakerRect = new Rect(position.x, position.y, position.width, lineHeight);
var textRect = new Rect(position.x, position.y + lineHeight + 2, position.width, lineHeight * 2);
EditorGUI.PropertyField(speakerRect, property.FindPropertyRelative("speaker"));
EditorGUI.PropertyField(textRect, property.FindPropertyRelative("text"));
EditorGUI.EndProperty();
}
}
Attribute-Based Drawers
ReadOnly Attribute
// Runtime attribute public class ReadOnlyAttribute : PropertyAttribute { }
// Editor drawer [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] public class ReadOnlyDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { GUI.enabled = false; EditorGUI.PropertyField(position, property, label); GUI.enabled = true; } }
Range with Label Attribute
// Runtime attribute public class LabeledRangeAttribute : PropertyAttribute { public float Min { get; } public float Max { get; } public string MinLabel { get; } public string MaxLabel { get; }
public LabeledRangeAttribute(float min, float max, string minLabel, string maxLabel)
{
Min = min;
Max = max;
MinLabel = minLabel;
MaxLabel = maxLabel;
}
}
// Editor drawer [CustomPropertyDrawer(typeof(LabeledRangeAttribute))] public class LabeledRangeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var attr = (LabeledRangeAttribute)attribute;
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
// Min label
var minLabelRect = new Rect(position.x, position.y, 30, position.height);
GUI.Label(minLabelRect, attr.MinLabel);
// Slider
var sliderRect = new Rect(position.x + 35, position.y, position.width - 70, position.height);
property.floatValue = GUI.HorizontalSlider(sliderRect, property.floatValue, attr.Min, attr.Max);
// Max label
var maxLabelRect = new Rect(position.x + position.width - 30, position.y, 30, position.height);
GUI.Label(maxLabelRect, attr.MaxLabel);
EditorGUI.EndProperty();
}
}
SceneView GUI (Handles)
Gizmo Drawing
[CustomEditor(typeof(SpawnArea))] public class SpawnAreaEditor : Editor { private void OnSceneGUI() { var spawner = (SpawnArea)target;
// Draw spawn area bounds
Handles.color = new Color(0, 1, 0, 0.3f);
Handles.DrawSolidDisc(spawner.transform.position, Vector3.up, spawner.radius);
// Draw editable radius handle
Handles.color = Color.green;
EditorGUI.BeginChangeCheck();
float newRadius = Handles.RadiusHandle(Quaternion.identity, spawner.transform.position, spawner.radius);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(spawner, "Change Spawn Radius");
spawner.radius = newRadius;
}
// Draw label
Handles.Label(spawner.transform.position + Vector3.up * 2, $"Spawn Area\nRadius: {spawner.radius:F1}");
}
}
Position Handle
private void OnSceneGUI() { var waypoint = (WaypointPath)target;
for (int i = 0; i < waypoint.points.Count; i++)
{
EditorGUI.BeginChangeCheck();
Vector3 newPos = Handles.PositionHandle(waypoint.points[i], Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(waypoint, "Move Waypoint");
waypoint.points[i] = newPos;
}
// Draw connection lines
if (i > 0)
{
Handles.DrawLine(waypoint.points[i - 1], waypoint.points[i]);
}
// Draw index label
Handles.Label(waypoint.points[i], $"Point {i}");
}
}
Common UI Patterns
Horizontal Layout
using (new EditorGUILayout.HorizontalScope()) { GUILayout.Label("Name:", GUILayout.Width(60)); value = EditorGUILayout.TextField(value); if (GUILayout.Button("Clear", GUILayout.Width(50))) { value = ""; } }
Vertical Layout with Box
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { GUILayout.Label("Section Title", EditorStyles.boldLabel); EditorGUILayout.PropertyField(prop1); EditorGUILayout.PropertyField(prop2); }
Toolbar
private int selectedTab = 0; private string[] tabs = { "General", "Advanced", "Debug" };
private void OnGUI() { selectedTab = GUILayout.Toolbar(selectedTab, tabs);
switch (selectedTab)
{
case 0: DrawGeneralTab(); break;
case 1: DrawAdvancedTab(); break;
case 2: DrawDebugTab(); break;
}
}
Progress Bar
// Simple progress bar EditorGUI.ProgressBar(rect, progress, $"{progress * 100:F0}%");
// Progress bar in inspector public override void OnInspectorGUI() { var healthProp = serializedObject.FindProperty("health"); var maxHealthProp = serializedObject.FindProperty("maxHealth");
float ratio = healthProp.floatValue / maxHealthProp.floatValue;
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, ratio, $"Health: {healthProp.floatValue}/{maxHealthProp.floatValue}");
}
Drag and Drop
private void OnGUI() { var dropArea = GUILayoutUtility.GetRect(100, 100); GUI.Box(dropArea, "Drop Asset Here");
Event evt = Event.current;
switch (evt.type)
{
case EventType.DragUpdated:
case EventType.DragPerform:
if (!dropArea.Contains(evt.mousePosition))
break;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (var obj in DragAndDrop.objectReferences)
{
Debug.Log($"Dropped: {obj.name}");
}
}
break;
}
}
Undo Support
Recording Changes
// Single object Undo.RecordObject(target, "Change Value"); myComponent.value = newValue;
// Multiple objects Undo.RecordObjects(targets, "Change Values"); foreach (var t in targets) { ((MyComponent)t).value = newValue; }
// Create object with undo var newObj = new GameObject("New Object"); Undo.RegisterCreatedObjectUndo(newObj, "Create Object");
// Destroy with undo Undo.DestroyObjectImmediate(obj);
Property Change Callback
private void OnEnable() { Undo.undoRedoPerformed += OnUndoRedo; }
private void OnDisable() { Undo.undoRedoPerformed -= OnUndoRedo; }
private void OnUndoRedo() { Repaint(); }
EditorPrefs (Persistent Settings)
public class MyToolWindow : EditorWindow { private const string PREF_KEY = "MyTool_";
private bool showAdvanced;
private int selectedMode;
private void OnEnable()
{
// Load settings
showAdvanced = EditorPrefs.GetBool(PREF_KEY + "ShowAdvanced", false);
selectedMode = EditorPrefs.GetInt(PREF_KEY + "SelectedMode", 0);
}
private void OnDisable()
{
// Save settings
EditorPrefs.SetBool(PREF_KEY + "ShowAdvanced", showAdvanced);
EditorPrefs.SetInt(PREF_KEY + "SelectedMode", selectedMode);
}
}
Message Types
// Help boxes EditorGUILayout.HelpBox("Info message", MessageType.Info); EditorGUILayout.HelpBox("Warning message", MessageType.Warning); EditorGUILayout.HelpBox("Error message", MessageType.Error); EditorGUILayout.HelpBox("No icon message", MessageType.None);
// Dialog boxes if (EditorUtility.DisplayDialog("Confirm", "Are you sure?", "Yes", "No")) { // User clicked Yes }
// Progress dialog EditorUtility.DisplayProgressBar("Processing", "Please wait...", 0.5f); // ... do work ... EditorUtility.ClearProgressBar();
Best Practices
- Always Use SerializedProperty
// Good - supports undo, multi-edit, prefab overrides serializedObject.Update(); EditorGUILayout.PropertyField(serializedObject.FindProperty("myField")); serializedObject.ApplyModifiedProperties();
// Bad - loses undo support ((MyComponent)target).myField = EditorGUILayout.FloatField(((MyComponent)target).myField);
- Cache SerializedProperty References
private SerializedProperty myProp;
private void OnEnable() { myProp = serializedObject.FindProperty("myField"); }
public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.PropertyField(myProp); // Use cached reference serializedObject.ApplyModifiedProperties(); }
- Support Multi-Object Editing
[CustomEditor(typeof(MyComponent))] [CanEditMultipleObjects] // Enable multi-select editing public class MyComponentEditor : Editor { public override void OnInspectorGUI() { serializedObject.Update();
// This automatically handles multiple selection
EditorGUILayout.PropertyField(serializedObject.FindProperty("value"));
// For custom buttons, iterate targets
if (GUILayout.Button("Reset All"))
{
foreach (var t in targets)
{
Undo.RecordObject(t, "Reset");
((MyComponent)t).Reset();
}
}
serializedObject.ApplyModifiedProperties();
}
}
- Repaint on Changes
private void OnGUI() { EditorGUI.BeginChangeCheck();
// ... draw controls ...
if (EditorGUI.EndChangeCheck())
{
Repaint(); // Force immediate redraw
}
}
Common Mistakes
- Forgetting serializedObject.Update/ApplyModifiedProperties
// Wrong - changes won't be saved public override void OnInspectorGUI() { EditorGUILayout.PropertyField(serializedObject.FindProperty("value")); }
// Correct public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.PropertyField(serializedObject.FindProperty("value")); serializedObject.ApplyModifiedProperties(); }
- Using IMGUI for Game UI
// Wrong - OnGUI is for editor only public class GameHUD : MonoBehaviour { void OnGUI() { GUI.Label(new Rect(10, 10, 100, 20), "Health: 100"); } }
// Correct - use uGUI or UI Toolkit for game UI
- Not Caching GUIStyle
// Wrong - creates new style every frame void OnGUI() { var style = new GUIStyle(EditorStyles.boldLabel); style.fontSize = 14; GUILayout.Label("Title", style); }
// Correct - cache and reuse private GUIStyle titleStyle;
void OnGUI() { titleStyle ??= new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; GUILayout.Label("Title", titleStyle); }
Tool Selection Guide
Task Recommended Approach
Custom inspector [CustomEditor]
- Editor class
Reusable field UI [CustomPropertyDrawer]
- PropertyDrawer class
Standalone tool EditorWindow subclass
Scene visualization OnSceneGUI
- Handles
Menu command [MenuItem] attribute
Context menu [ContextMenu] / [ContextMenuItemAttribute]
Project settings SettingsProvider
Reference
Useful Classes
-
EditorGUILayout
-
Layout-based editor controls
-
EditorGUI
-
Position-based editor controls
-
GUILayout
-
Layout-based basic controls
-
GUI
-
Position-based basic controls
-
Handles
-
Scene view 3D handles
-
EditorStyles
-
Built-in editor styles
-
EditorGUIUtility
-
Editor utilities
-
AssetDatabase
-
Asset operations
-
SerializedObject / SerializedProperty
-
Serialization access
MenuItem Shortcuts
[MenuItem("Tools/My Tool %#t")] // Ctrl+Shift+T [MenuItem("Tools/My Tool %&t")] // Ctrl+Alt+T [MenuItem("Tools/My Tool #t")] // Shift+T [MenuItem("Tools/My Tool _t")] // T only [MenuItem("Tools/My Tool %LEFT")] // Ctrl+Left Arrow