Unity檢視面板的繼承方法研究

  對於檢視面板 Inspector 的面板繼承方式對項目來講是頗有必要的, 好比一個基類, 寫了一個很好看的檢視面板[CustomEditor(typeof(XXX))],安全

但是全部子類的面板沒法直接繼承這個CustomEditor, 有些人的解決方案是把子類寫檢視面板的代碼獨立出來, 而後子類面板直接去調用這些Layout,less

很是浪費人力物力.ide

  最近發現有個 DecoratorEditor 腳本, 它實現了對 Unity 自帶檢視面板的擴展, 看到它實現某個類型的面板Inspector的方法, 就是使用Editor.CreateEditor 這個API函數

建立了一個相應的Editor, 而後調用它的OnInspectorGUI() 方法來繪製原有面板的, 因而能夠從這個地方着手.oop

   從設計上來講 [CustomEditor(typeof(XXX))] 在耦合上並無太多的耦合, Unity 開發組的想法應該就是一個Editor對應一個組件, 它們對應類型之間的繼承關係不該該this

互相影響, 保證泛用性和獨立性. 設計

  原始的Editor腳本和類型是這樣的:orm

基類 :對象

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 :blog

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 : 

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);
    }
}

  

TestBaseClass
TestCamera : TestBaseClass
TestUntiy : TestCamera

很是簡單的繼承關係, TestUnity的檢視面板以下, 沒有繼承關係

 

  那麼在繼承的設計上, 也應該遵循Unity的設計原則, 在繼承類型 : Editor 以及 base.OnInspectorGUI(); 繪製基類方法上作文章.

若是使用簡單的繼承好比:

[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來講是沒有類型檢查的, 但是在函數中是有類型匹配的,

就算傳入錯誤類型也是安全的 :

[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.

相關文章
相關標籤/搜索