关于 unity-editor-tools
unity-editor-tools 是面向 Unity 开发者的编辑器扩展技能指南,涵盖自定义 Inspector、窗口、属性绘制器和菜单命令。内容也介绍场景视图交互、资源数据库及编辑器脚本的安全模式。
主要功能
- 01自定义 Inspector 与编辑器窗口
- 02UI Toolkit 和 IMGUI 扩展
- 03属性绘制器与菜单命令
- 04场景视图 Handles 和 Gizmos
- 05SerializedObject 撤销与多对象编辑
- 06AssetDatabase 资源操作
适用场景
- 01为组件构建专用 Inspector
- 02创建可停靠的编辑器工具窗口
- 03在场景视图中编辑对象属性
- 04批量查找或管理项目资源
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-editor-tools安装后在 Claude Code 中输入/unity-editor-tools确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/custom-inspectors.mdeditor-windows.mdproperty-drawers.md详细介绍
Unity 编辑器工具
编辑器脚本概述
编辑器脚本可通过自定义检查器、窗口、工具和工作流扩展 Unity Editor。所有编辑器代码都必须位于 Editor/ 文件夹中,或使用 #if UNITY_EDITOR / #endif 包裹,以确保编辑器代码会从运行时构建中剥离。
Unity 6.3 支持两种用于编辑器扩展的 GUI 框架:
| 框架 | 状态 | 入口点 |
|---|---|---|
| UI Toolkit | 推荐 | CreateInspectorGUI() / CreateGUI(),返回 VisualElement |
| IMGUI | 旧版,功能完整 | OnInspectorGUI() / OnGUI() |
主要基类:Editor、EditorWindow、PropertyDrawer、DecoratorDrawer、ScriptableWizard。
自定义检查器(Editor)
继承 Editor,并应用 [CustomEditor(typeof(TargetType))]。完整指南请参阅 references/custom-inspectors.md。
UI Toolkit(推荐)
[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;
}
}
覆盖 CreateInspectorGUI() 后,OnInspectorGUI() 将被忽略。
使用 SerializedObject 模式的 IMGUI
[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();
}
}
主要成员
| 成员 | 描述 |
|---|---|
target / targets | 正在检查的对象 |
serializedObject | 目标对象的 SerializedObject |
DrawDefaultInspector() | 渲染内置的默认检查器 |
OnSceneGUI() | 在 Scene 视图中绘制可交互的 Handles |
Scene 视图集成
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;
}
}
PropertyDrawer 和 PropertyAttribute
自定义序列化字段在 Inspector 中的显示方式。完整指南请参阅 references/property-drawers.md。
1. 定义特性(运行时代码,位于 Editor 文件夹之外):
public class MyRangeAttribute : PropertyAttribute
{
public readonly float min, max;
public MyRangeAttribute(float min, float max) { this.min = min; this.max = max; }
}
2. 实现 Drawer(Editor 文件夹):
[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);
}
}
关键规则: 必须位于 Editor/ 文件夹中。使用 EditorGUI(而非 EditorGUILayout)。对于预制件覆盖,请使用 EditorGUI.BeginProperty()/EndProperty()。不能在同一个 Drawer 中混用 UI Toolkit 和 IMGUI。
EditorWindow
自定义可停靠的编辑器窗口。完整指南请参阅 references/editor-windows.md。
生命周期: 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" });
}
}
| 静态方法 | 描述 |
|---|---|
GetWindow<T>() | 获取现有窗口或创建新窗口 |
CreateWindow<T>() | 始终创建新实例 |
HasOpenInstances<T>() | 检查窗口是否已打开 |
| 显示模式 | 行为 |
|---|---|
Show() | 标准可停靠窗口 |
ShowUtility() | 浮动窗口,不可停靠 |
ShowModal() | 模态对话框 |
ShowAsDropDown() | 下拉弹出窗口 |
MenuItem
[MenuItem] 特性可将静态方法转换为菜单命令。
[MenuItem("Menu/Path/Item Name", isValidateFunction, priority)]
| 前缀 | 位置 |
|---|---|
"Tools/..." | 自定义工具菜单 |
"Assets/..." | Assets 菜单 + Project 上下文菜单 |
"GameObject/..." | GameObject 菜单 + Hierarchy 上下文菜单 |
"CONTEXT/ComponentType/..." | 组件上下文菜单 |
快捷键修饰符: % = Cmd/Ctrl,# = Shift,& = Alt,^ = Ctrl(全部),_ = 无修饰符。
// 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(使用优先级 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;
}
Gizmos 和 Handles
Gizmos —— Scene/Game 视图中的可视化调试
所有绘制操作都在 OnDrawGizmos()(始终执行)或 OnDrawGizmosSelected()(选中时执行)中进行:
void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, radius);
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawSphere(transform.position, radius);
}
方法: DrawLine、DrawRay、DrawSphere、DrawWireSphere、DrawCube、DrawWireCube、DrawMesh、DrawWireMesh、DrawFrustum、DrawIcon。属性: color、matrix。
Handles —— 可交互的 3D 控件(仅限编辑器)
在自定义 Editor 的 OnSceneGUI() 中使用:
[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;
}
}
}
方法: PositionHandle、RotationHandle、ScaleHandle、FreeMoveHandle、RadiusHandle、DrawLine、DrawWireDisc、Label、BeginGUI/EndGUI。属性: color、matrix。
SerializedObject 模式
在编辑器脚本中编辑属性的标准方式。可自动支持撤销、多对象编辑和预制件覆盖跟踪。
三步模式:
serializedObject.Update(); // 1. Sync from target
EditorGUILayout.PropertyField(serializedObject.FindProperty("myField")); // 2. Draw/modify
serializedObject.ApplyModifiedProperties(); // 3. Apply with undo
| 成员 | 描述 |
|---|---|
Update() | 从目标对象刷新 |
ApplyModifiedProperties() | 提交更改并支持撤销 |
FindProperty(string) | 按名称获取 SerializedProperty |
GetIterator() | 遍历所有属性 |
targetObject / targetObjects | 正在检查的对象 |
hasModifiedProperties | 存在尚未应用的更改时为 True |
要进行多对象编辑:添加 [CanEditMultipleObjects],并始终使用 SerializedProperty,而不是 target。
AssetDatabase
在编辑器脚本中以编程方式访问资源。
// 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();
主要方法: 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(); }
}
}
反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
未使用 Undo 编辑 target | 无法撤销,也不会标记为已修改 | 使用 SerializedObject 或 Undo.RecordObject() |
在 [CanEditMultipleObjects] 中使用 target | 只会修改第一个对象 | 使用 SerializedProperty |
在 Editor/ 外编写编辑器代码,且未使用 #if UNITY_EDITOR | 构建失败 | 使用 Editor/ 文件夹或预处理器保护 |
在 PropertyDrawer 中使用 EditorGUILayout | 不受支持 | 使用带 Rect 的 EditorGUI |
缺少 serializedObject.Update() | 数据过时 | 读取前务必调用 |
缺少 ApplyModifiedProperties() | 更改丢失 | 修改后务必调用 |
| 在一个 PropertyDrawer 中混用 UI Toolkit 和 IMGUI | 不受支持 | 选择一个框架 |
缺少 Undo.RegisterCreatedObjectUndo() | 无法撤销创建操作 | 始终为新对象注册撤销 |
缺少 GameObjectUtility.SetParentAndAlign() | 层级中的父子关系错误 | 在 GameObject 菜单项中使用 |
在 OnInspectorGUI()/OnGUI() 中执行繁重操作 | 导致卡顿(每帧会调用多次) | 在 OnEnable() 中缓存 |
关键 API 速查
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
相关技能
- unity-foundations -- 项目结构、程序集定义、Editor 文件夹
- unity-scripting -- MonoBehaviour 生命周期、SerializeField、ScriptableObject
- unity-ui -- UI Toolkit 基础、VisualElement、UXML、USS