概述 Unity 编辑器本身就是一个强大的工具,但重复性操作——创建资源、配置组件、批量处理——如果每次都手动做,效率低下且容易出错。
自定义编辑器扩展 就是利用 Unity Editor API 来编写工具脚本,将重复劳动自动化。所有扩展代码都放在 Editor/ 目录下,不会影响运行时逻辑。
1. 编辑器脚本基础 1.1 目录规则 1 2 3 Assets/ Editor/ # 编辑器脚本放在这里,不会被编译到运行时 Scripts/ # 运行时脚本
编辑器脚本必须引用 UnityEditor 命名空间:
1 2 using UnityEditor;using UnityEngine;
1.2 关键特性标记
特性
作用
[CustomEditor(typeof(T))]
自定义 Inspector 面板
[CustomPropertyDrawer(typeof(T))]
自定义属性绘制
[MenuItem(“Tools/xxx”)]
添加菜单项
[InitializeOnLoadMethod]
编辑器启动或脚本编译后自动执行
2. 自定义 Inspector 默认的 Inspector 显示所有 public 和 [SerializeField] 字段。通过自定义 Inspector,可以控制布局、分组、可视化预览。
2.1 基础自定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Weapon : MonoBehaviour { public string WeaponName; public int Damage; public float AttackRange; public GameObject ModelPrefab; } [CustomEditor(typeof(Weapon)) ] public class WeaponEditor : Editor { public override void OnInspectorGUI () { var weapon = (Weapon)target; EditorGUILayout.LabelField("=== 武器配置 ===" , EditorStyles.boldLabel); weapon.WeaponName = EditorGUILayout.TextField("武器名称" , weapon.WeaponName); weapon.Damage = EditorGUILayout.IntSlider("伤害" , weapon.Damage, 1 , 999 ); weapon.AttackRange = EditorGUILayout.Slider("攻击范围" , weapon.AttackRange, 0f , 100f ); weapon.ModelPrefab = EditorGUILayout.ObjectField("模型" , weapon.ModelPrefab, typeof (GameObject), false ) as GameObject; EditorGUILayout.Space(); if (GUILayout.Button("预览武器" )) { weapon.GetComponent<Renderer>().enabled = true ; } } }
3. 自定义 PropertyDrawer PropertyDrawer 控制 单个属性 在 Inspector 中的绘制方式,无需写整个 Editor 类。
3.1 范围滑条 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MinMaxSliderAttribute : PropertyAttribute { public float Min; public float Max; public MinMaxSliderAttribute (float min, float max ) { Min = min; Max = max; } } [CustomPropertyDrawer(typeof(MinMaxSliderAttribute)) ] public class MinMaxSliderDrawer : PropertyDrawer { public override void OnGUI (Rect position, SerializedProperty property, GUIContent label ) { if (property.propertyType == SerializedPropertyType.Vector2) { var attr = attribute as MinMaxSliderAttribute; EditorGUI.BeginProperty(position, label, property); var value = property.vector2Value; EditorGUI.MinMaxSlider(position, label, ref value .x, ref value .y, attr.Min, attr.Max); property.vector2Value = value ; EditorGUI.EndProperty(); } else { EditorGUI.LabelField(position, label.text, "请将 MinMaxSlider 用于 Vector2" ); } } }
使用:
1 2 3 4 5 6 7 8 public class Player : MonoBehaviour { [MinMaxSlider(0f, 100f) ] public Vector2 AttackRange = new (10f , 50f ); [MinMaxSlider(0f, 10f) ] public Vector2 SpeedRange = new (1f , 5f ); }
3.2 只读属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ReadOnlyAttribute : PropertyAttribute { }[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 ; } } public class Player : MonoBehaviour { [ReadOnly ] public string PlayerId = System.Guid.NewGuid().ToString(); }
4. EditorWindow 工具窗口 当需要独立工具窗口(而非 Inspector 面板)时,继承 EditorWindow。
4.1 批量重命名工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class BatchRenameWindow : EditorWindow { private string _prefix = "" ; private string _suffix = "" ; private int _startNumber = 1 ; [MenuItem("Tools/Batch Rename" ) ] public static void ShowWindow () { GetWindow<BatchRenameWindow>("批量重命名" ); } private void OnGUI () { EditorGUILayout.LabelField("批量重命名工具" , EditorStyles.boldLabel); EditorGUILayout.Space(); _prefix = EditorGUILayout.TextField("前缀" , _prefix); _suffix = EditorGUILayout.TextField("后缀" , _suffix); _startNumber = EditorGUILayout.IntField("起始编号" , _startNumber); EditorGUILayout.Space(); if (GUILayout.Button("重命名选中物体" )) { RenameSelected(); } } private void RenameSelected () { var selected = Selection.gameObjects; if (selected.Length == 0 ) { EditorUtility.DisplayDialog("提示" , "请先在场景中选择要重命名的物体" , "确定" ); return ; } Undo.RecordObjects(selected, "Batch Rename" ); for (int i = 0 ; i < selected.Length; i++) { selected[i].name = _prefix + (_startNumber + i) + _suffix; } Debug.Log("已重命名 " + selected.Length + " 个物体" ); } }
4.2 资源检查工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class AssetCheckerWindow : EditorWindow { [MenuItem("Tools/Asset Checker" ) ] public static void ShowWindow () { GetWindow<AssetCheckerWindow>("资源检查" ); } private Vector2 _scrollPos; private void OnGUI () { EditorGUILayout.LabelField("资源检查工具" , EditorStyles.boldLabel); EditorGUILayout.Space(); if (GUILayout.Button("检查缺失引用的预制体" )) { CheckMissingReferences(); } } private void CheckMissingReferences () { var guids = AssetDatabase.FindAssets("t:Prefab" ); var missingCount = 0 ; _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path); var components = prefab.GetComponentsInChildren<Component>(true ); foreach (var component in components) { if (component == null ) { EditorGUILayout.LabelField("缺失组件: " + path); missingCount++; continue ; } var so = new SerializedObject(component); var prop = so.GetIterator(); while (prop.NextVisible(true )) { if (prop.propertyType == SerializedPropertyType.ObjectReference && prop.objectReferenceValue == null ) { if (prop.name != "m_Script" ) { EditorGUILayout.LabelField(path + "/" + component.name + " -> " + prop.displayName + " 为空" ); missingCount++; } } } } } EditorGUILayout.EndScrollView(); if (missingCount == 0 ) { EditorGUILayout.LabelField("未发现缺失引用" ); } } }
5. 编辑器工具开发建议 5.1 性能注意事项
OnGUI 每帧调用多次,避免在里面做 AssetDatabase 等开销大的操作
使用 EditorApplication.delayCall 延迟执行非 UI 操作
长时间任务使用 EditorUtility.DisplayProgressBar 显示进度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void BatchProcess (){ var assets = GetAssets(); for (int i = 0 ; i < assets.Length; i++) { if (EditorUtility.DisplayCancelableProgressBar( "处理中" , "正在处理 " + assets[i].name + " (" + (i + 1 ) + "/" + assets.Length + ")" , (float )i / assets.Length)) { break ; } } EditorUtility.ClearProgressBar(); }
5.2 Undo 支持 任何修改对象的操作都应该支持 Undo:
1 2 3 Undo.RecordObject(target, "修改描述" ); PrefabUtility.RecordPrefabInstancePropertyModifications(target);
总结
Custom Editor :控制 MonoBehaviour 的 Inspector 显示
PropertyDrawer :控制单个属性的绘制方式,复用性高
EditorWindow :独立工具窗口,适合批处理工具
MenuItem :注册菜单入口
自定义编辑器扩展是 Unity 高级开发的分水岭——能从”用工具的人”变成”造工具的人”。
下一篇将探讨 进阶对象池与 Addressables 资源管理 ,解决内存和加载效率问题。