Unity检视面板的继承方法研究 (一)
对于检视面板 Inspector 的面板继承方式对项目来说是很有必要的, 比如一个基类, 写了一个很好看的检视面板[CustomEditor(typeof(XXX))],
可是所有子类的面板无法直接继承这个CustomEditor, 有些人的解决方案是把子类写检视面板的代码独立出来, 然后子类面板直接去调用这些Layout,
非常浪费人力物力.
最近发现有个 DecoratorEditor 脚本, 它实现了对 Unity 自带检视面板的扩展, 看到它实现某个类型的面板Inspector的方法, 就是使用Editor.CreateEditor 这个API
创建了一个相应的Editor, 然后调用它的OnInspectorGUI() 方法来绘制原有面板的, 于是可以从这个地方着手.
从设计上来说 [CustomEditor(typeof(XXX))] 在耦合上并没有太多的耦合, Unity 开发组的想法应该就是一个Editor对应一个组件, 它们对应类型之间的继承关系不应该
互相影响, 保证泛用性和独立性.
原始的Editor脚本和类型是这样的:
基类 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class TestBaseClass : MonoBehaviour { public float abc; } // 检视面板 [CustomEditor( typeof (TestBaseClass))] public class TestBaseClassInspector : Editor { TestBaseClass _target; private void OnEnable() { _target = target as TestBaseClass; } public override void OnInspectorGUI() { _target.abc = EditorGUILayout.FloatField( "基类变量 : " , _target.abc); } } |
子类1 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class TestCamera : TestBaseClass { public Camera cam; } // 检视面板 [CustomEditor( typeof (TestCamera))] public class TestCameraInspector : Editor { TestCamera _target = null ; private void OnEnable() { _target = target as TestCamera; } public override void OnInspectorGUI() { base .OnInspectorGUI(); _target.cam = EditorGUILayout.ObjectField( "一次继承变量 : " , _target.cam, typeof (Camera), true ) as Camera; } } |
子类2 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class TestUntiy : TestCamera { public int hahah; } // 检视面板 [CustomEditor( typeof (TestUntiy))] public class TestUntiyInspector : Editor { TestUntiy _target = null ; private void OnEnable() { _target = target as TestUntiy; } public override void OnInspectorGUI() { base .OnInspectorGUI(); _target.hahah = EditorGUILayout.IntField( "二次继承变量 : " , _target.hahah); } } |
1 | TestBaseClass |
1 | TestCamera : TestBaseClass |
1 | TestUntiy : TestCamera<br><br>非常简单的继承关系, TestUnity的检视面板如下, 没有继承关系 |
那么在继承的设计上, 也应该遵循Unity的设计原则, 在继承类型 : Editor 以及 base.OnInspectorGUI(); 绘制基类方法上做文章.
如果使用简单的继承比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [CustomEditor( typeof (TestCamera))] public class TestCameraInspector : TestBaseClassInspector { TestCamera _target = null ; private void OnEnable() { _target = target as TestCamera; } public override void OnInspectorGUI() { base .OnInspectorGUI(); _target.cam = EditorGUILayout.ObjectField( "一次继承变量 : " , _target.cam, typeof (Camera), true ) as Camera; } } |
会出现很多问题, 如基类的OnEnable方法没有被触发, 基类面板报错等, 所有生命周期都要写虚方法, 每个重写方法都要注意, 很麻烦.
而Editor.CreateEditor创建的Editor是有生命周期的. 创建一个中间类型 InspectorDecoratorEditor, 大家都继承它就可以了, 而绘制基类对象的方法就改为
DrawBaseInspectorGUI<T>(), 这样就能方便地自己定义需要绘制的基类了.
public class InspectorDecoratorEditor : Editor { public static readonly System.Type EndType = typeof (UnityEngine.MonoBehaviour); // end type dont need show in inspector public static readonly System.Type BaseEditorType = typeof (UnityEditor.Editor); // CustomEditor must inherit from it, filter public static readonly BindingFlags CustomEditorFieldFlags = BindingFlags.NonPublic | BindingFlags.Instance; // flag // type cache[Assembly, [scriptType, customEditorType]] protected static Dictionary<Assembly, Dictionary<System.Type, System.Type>> ms_editorReferenceScript = new Dictionary<Assembly, Dictionary<System.Type, System.Type>>(); protected List<Editor> m_inheritEditors = null ; // cached editors // ctor, use ctor instead Mono life circle, more user friendly public InspectorDecoratorEditor() { CacheEditorReferenceScript(); } #region Main Funcs /// <summary> /// Cache all CustomEditor in current Assembly /// </summary> protected void CacheEditorReferenceScript() { var editorAssembly = Assembly.GetAssembly( this .GetType()); // editor may in diferent assemblies if (ms_editorReferenceScript.ContainsKey(editorAssembly) == false ) { Dictionary<System.Type, System.Type> cachedData = new Dictionary<System.Type, System.Type>(); var types = editorAssembly.GetExportedTypes(); foreach ( var editorType in types) { if (editorType.IsSubclassOf(BaseEditorType)) { var scriptType = GetTypeFormCustomEditor(editorType); if (scriptType != null ) { cachedData[scriptType] = editorType; } } } ms_editorReferenceScript[editorAssembly] = cachedData; } } /// <summary> /// Draw a Target Type Inspector, call OnInspectorGUI /// </summary> /// <typeparam name="T"></typeparam> protected virtual void DrawBaseInspectorGUI<T>() where T : InspectorDecoratorEditor { if (m_inheritEditors == null ) { m_inheritEditors = new List<Editor>(); Dictionary<System.Type, System.Type> scriptEditorCache = null ; if (ms_editorReferenceScript.TryGetValue(Assembly.GetAssembly( this .GetType()), out scriptEditorCache) && scriptEditorCache != null ) { var baseType = target.GetType().BaseType; while (baseType != null && baseType != EndType) { System.Type editorType = null ; if (scriptEditorCache.TryGetValue(baseType, out editorType) && editorType != null ) { m_inheritEditors.Add(Editor.CreateEditor(targets, editorType)); } baseType = baseType.BaseType; } } } if (m_inheritEditors.Count > 0) { for ( int i = m_inheritEditors.Count - 1; i >= 0; i--) { var drawTarget = m_inheritEditors[i]; if (drawTarget && drawTarget.GetType() == typeof (T)) { drawTarget.OnInspectorGUI(); // draw target type only, avoid endless loop break ; } } } } #endregion #region Help Funcs public static System.Type GetTypeFormCustomEditor(System.Type editorType) { var attributes = editorType.GetCustomAttributes( typeof (CustomEditor), false ) as CustomEditor[]; if (attributes != null && attributes.Length > 0) { var attribute = attributes[0]; var type = attribute.GetType().GetField( "m_InspectedType" , CustomEditorFieldFlags).GetValue(attribute) as System.Type; return type; } return null ; } #endregion } |
修改后的Editor代码如下, 修改的只有继承类以及DrawBaseInspectorGUI<T>函数, 注意这里对于T来说是没有类型检查的, 可是在函数中是有类型匹配的,
就算传入错误类型也是安全的 :
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 | [CustomEditor( typeof (TestBaseClass))] public class TestBaseClassInspector : InspectorDecoratorEditor { TestBaseClass _target; private void OnEnable() { _target = target as TestBaseClass; } public override void OnInspectorGUI() { _target.abc = EditorGUILayout.FloatField( "基类变量 : " , _target.abc); } } [CustomEditor( typeof (TestCamera))] public class TestCameraInspector : InspectorDecoratorEditor { TestCamera _target = null ; private void OnEnable() { _target = target as TestCamera; } public override void OnInspectorGUI() { DrawBaseInspectorGUI<TestBaseClassInspector>(); _target.cam = EditorGUILayout.ObjectField( "一次继承变量 : " , _target.cam, typeof (Camera), true ) as Camera; } } [CustomEditor( typeof (TestUntiy))] public class TestUntiyInspector : InspectorDecoratorEditor { TestUntiy _target = null ; private void OnEnable() { _target = target as TestUntiy; } public override void OnInspectorGUI() { DrawBaseInspectorGUI<TestCameraInspector>(); _target.hahah = EditorGUILayout.IntField( "二次继承变量 : " , _target.hahah); } } |
然后看看检视面板现在的样子, 完美绘制了基类面板:
DrawBaseInspectorGUI<T>() 这个绘制基类的请求强大的地方就是可以选择从哪个类型开始绘制, 比如
DrawBaseInspectorGUI<TestCameraInspector>();
换成
DrawBaseInspectorGUI<TestBaseClassInspector>();
那么 TestCameraInspector 这个检视面板就被跳过去了 :
虽然有很多方式能够绘制或者继承子类检视面板, 不过这个应该是个泛用度很高的方案. Over.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律