
1. 为什么需要定制Unity编辑器在Unity游戏开发过程中我们经常会遇到一些重复性工作比如批量修改资源参数、快速生成特定类型的游戏对象、自动化测试场景等。Unity自带的编辑器功能虽然强大但面对项目特有的需求时往往显得不够灵活。这就是为什么我们需要学习编辑器定制和插件开发。编辑器扩展的核心价值在于将重复操作自动化提升开发效率为特定游戏机制创建专用工具简化复杂系统的配置流程为团队提供统一的工作流程2. 编辑器脚本基础架构2.1 编辑器代码的组织方式Unity采用严格的运行时和编辑器代码分离机制。所有编辑器相关代码必须放在名为Editor的文件夹中这个文件夹可以位于项目的任何层级。我通常的做法是为每个功能模块创建对应的Editor子文件夹例如Assets/ └── AI/ ├── Editor/ # AI相关的编辑器代码 ├── Behaviors/ # 运行时AI行为脚本 └── Utilities/ # AI工具类这种组织方式的好处是相关功能的代码集中管理避免命名空间污染编译顺序明确Editor目录下的脚本最后编译2.2 必备的命名空间和特性所有编辑器脚本都需要引入UnityEditor命名空间using UnityEditor;常用的编辑器特性包括[MenuItem]添加菜单项[CustomEditor]自定义Inspector面板[ExecuteInEditMode]在编辑模式下执行脚本[ContextMenu]为组件添加上下文菜单3. 创建自定义编辑器窗口3.1 基本窗口创建流程创建一个继承自EditorWindow的类是实现自定义窗口的基础。以下是创建简单窗口的完整代码using UnityEditor; using UnityEngine; public class MyCustomWindow : EditorWindow { [MenuItem(Tools/My Custom Window)] public static void ShowWindow() { GetWindowMyCustomWindow(Custom Window); } void OnGUI() { GUILayout.Label(This is my custom window, EditorStyles.boldLabel); if(GUILayout.Button(Click Me)) { Debug.Log(Button clicked!); } } }关键点说明[MenuItem]特性定义了菜单项的路径和名称GetWindow方法创建或获取窗口实例OnGUI是绘制窗口内容的核心方法3.2 窗口布局与控件Unity提供了两种主要的GUI布局系统GUI绝对定位系统GUILayout自动布局系统对于编辑器扩展我推荐使用GUILayout结合EditorGUILayout后者提供了许多编辑器专用的控件float sliderValue EditorGUILayout.Slider(Intensity, 0.5f, 0, 1); Color color EditorGUILayout.ColorField(Object Color, Color.red); GameObject obj EditorGUILayout.ObjectField(Target, null, typeof(GameObject), true) as GameObject;4. 扩展Inspector面板4.1 自定义Inspector基础为特定组件创建自定义Inspector需要以下步骤创建继承自Editor的类使用[CustomEditor]特性指定目标类型重写OnInspectorGUI方法示例代码[CustomEditor(typeof(MyComponent))] public class MyComponentEditor : Editor { public override void OnInspectorGUI() { // 绘制默认Inspector DrawDefaultInspector(); // 添加自定义控件 MyComponent myComp (MyComponent)target; if(GUILayout.Button(Do Something)) { myComp.DoSomething(); } } }4.2 高级Inspector技巧4.2.1 属性绘制器对于复杂的数据类型可以创建PropertyDrawer来定制其在Inspector中的显示方式[CustomPropertyDrawer(typeof(MySpecialData))] public class MySpecialDataDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // 自定义绘制逻辑 } }4.2.2 场景视图交互通过重写OnSceneGUI方法可以在Scene视图中添加交互元素public override void OnSceneGUI() { MyComponent myComp (MyComponent)target; Handles.color Color.red; Handles.DrawWireArc(myComp.transform.position, Vector3.up, Vector3.forward, 360, myComp.radius); EditorGUI.BeginChangeCheck(); float newRadius Handles.RadiusHandle(Quaternion.identity, myComp.transform.position, myComp.radius); if(EditorGUI.EndChangeCheck()) { Undo.RecordObject(myComp, Change Radius); myComp.radius newRadius; } }5. 创建向导式工具5.1 ScriptableWizard基础ScriptableWizard是创建简单向导工具的最快捷方式public class CreateExplosionWizard : ScriptableWizard { public float radius 5f; public int damage 100; public GameObject effectPrefab; [MenuItem(Tools/Create Explosion)] static void CreateWizard() { DisplayWizardCreateExplosionWizard(Create Explosion, Create); } void OnWizardCreate() { // 实际创建爆炸的逻辑 GameObject explosion new GameObject(Explosion); // 设置爆炸参数... } }5.2 高级向导功能5.2.1 动态更新通过重写OnWizardUpdate方法可以根据用户输入动态更新向导状态protected override void OnWizardUpdate() { helpString Set explosion parameters; if(effectPrefab null) { errorString Please select an effect prefab; isValid false; } else { errorString ; isValid true; } }5.2.2 多按钮支持可以通过otherButtonName参数添加额外按钮[MenuItem(Tools/Create Explosion)] static void CreateWizard() { var wizard DisplayWizardCreateExplosionWizard(Create Explosion, Create, Preview); wizard.otherButtonName Preview; } void OnWizardOtherButton() { // 预览爆炸效果 }6. 编辑器插件开发实战技巧6.1 资源处理最佳实践处理资源时需要注意的几个关键点使用AssetDatabase类而不是直接文件操作修改资源后调用EditorUtility.SetDirty批量操作时使用Undo.RecordObject支持撤销// 创建新资源 var material new Material(Shader.Find(Standard)); AssetDatabase.CreateAsset(material, Assets/Materials/NewMaterial.mat); // 修改现有资源 var texture AssetDatabase.LoadAssetAtPathTexture2D(Assets/Textures/Test.png); var importer AssetImporter.GetAtPath(Assets/Textures/Test.png) as TextureImporter; importer.textureType TextureImporterType.Sprite; AssetDatabase.ImportAsset(Assets/Textures/Test.png);6.2 性能优化技巧编辑器插件也需要考虑性能避免在OnGUI中进行复杂计算使用EditorGUIUtility.SetWantsMouseJumping改善大列表的滚动体验对于频繁刷新的窗口考虑实现EditorWindow.OnInspectorUpdatepublic class PerformanceOptimizedWindow : EditorWindow { private Vector2 scrollPos; private string[] items new string[1000]; void OnGUI() { EditorGUIUtility.SetWantsMouseJumping(1); scrollPos EditorGUILayout.BeginScrollView(scrollPos); for(int i 0; i items.Length; i) { items[i] EditorGUILayout.TextField($Item {i}, items[i]); } EditorGUILayout.EndScrollView(); } }6.3 插件分发与安装制作可分发插件的要点使用.unitypackage格式打包包含完整的文档和示例场景遵循Unity的插件目录结构Plugins/ ├── Editor/ # 编辑器代码 ├── Runtime/ # 运行时代码 └── Resources/ # 资源文件7. 调试与问题排查7.1 常见问题解决方案脚本编译错误确保编辑器脚本放在Editor文件夹中菜单项不显示检查MenuItem的路径是否正确重启Unity可能解决临时问题Inspector不更新调用EditorUtility.SetDirty或serializedObject.ApplyModifiedProperties7.2 调试技巧使用Debug.Log输出信息到Console在Visual Studio中附加到Unity编辑器进程进行调试使用EditorWindow.Focus()和EditorGUIUtility.PingObject帮助定位问题对象[MenuItem(Tools/Debug Selection)] static void DebugSelection() { var obj Selection.activeObject; if(obj ! null) { Debug.Log($Selected: {obj.name}); EditorGUIUtility.PingObject(obj); } }8. 高级主题与扩展思路8.1 自定义场景视图工具通过EditorTool类可以创建与Unity内置工具(移动、旋转等)同级别的场景工具[EditorTool(My Custom Tool)] public class MyCustomTool : EditorTool { public override void OnToolGUI(EditorWindow window) { Handles.BeginGUI(); // 绘制工具GUI Handles.EndGUI(); // 处理场景交互 } }8.2 编辑器协程使用EditorCoroutine可以在编辑器中实现协程功能IEnumerator TestCoroutine() { Debug.Log(Start); yield return new WaitForSeconds(1); Debug.Log(After 1 second); } [MenuItem(Tools/Start Coroutine)] static void StartCoroutine() { EditorCoroutineUtility.StartCoroutineOwnerless(TestCoroutine()); }8.3 编辑器窗口持久化使用EditorPrefs或ScriptableObject保存窗口状态public class PersistentWindow : EditorWindow { private string someText ; void OnEnable() { someText EditorPrefs.GetString(MyWindow_SomeText, ); } void OnGUI() { someText EditorGUILayout.TextField(Text Field, someText); } void OnDisable() { EditorPrefs.SetString(MyWindow_SomeText, someText); } }在实际项目中编辑器扩展可以极大地提升开发效率。我建议从小的工具开始逐步构建适合自己项目的工作流程。记住好的编辑器工具应该让重复工作自动化同时保持足够的灵活性以适应项目的变化。