[Unity]用PropertyDrawer自定義struct/class的外觀


一般來說,當我們要擴展編輯器時,我們會從Editor類繼承,為自己的MonoBehaviour實現不同的外觀。
但是如果有一個struct/class,在許多地方被使用,Unity默認的外觀又不夠好看,此時想修改它的外觀,就需要使用PropertyDrawer了。


上圖是一個Monobehaviour中包含一個簡單的struct(TileCoord類),包含兩個int,但是顯示效果十分別扭。


實現對應的PropertyDrawer后

相對於Editor類可以修改MonoBehaviour的外觀,我們可以簡單的理解PropertyDrawer為修改struct/class的外觀的Editor類。
實現上面的效果的代碼如下

[CustomPropertyDrawer(typeof(TileCoord))]
public class TileCoordEditor : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        var x = property.FindPropertyRelative("x");
        var y = property.FindPropertyRelative("y");
        float LabelWidth = EditorGUIUtility.labelWidth;
        var labelRect = new Rect(position.x, position.y, LabelWidth, position.height);
        var xRect = new Rect(position.x + LabelWidth, position.y, (position.width - LabelWidth) / 2 - 20, position.height);
        var yRect = new Rect(position.x + LabelWidth + (position.width - LabelWidth) / 2 - 20 , position.y, (position.width - LabelWidth) / 2 - 20, position.height);
        
        EditorGUIUtility.labelWidth = 12.0f;
        EditorGUI.LabelField(labelRect, label);
        EditorGUI.PropertyField(xRect, x);
        EditorGUI.PropertyField(yRect, y);
        EditorGUIUtility.labelWidth = LabelWidth;
    }
//需要自定義高度
  //  public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
  //      return 
  //  }
}

可以看到跟繼承Editor的操作很相似。不過額外在OnGUI提供了三個參數,依次解釋一下:
position:該屬性在Editor中被分配到的位置、大小。注意這里的x,y對應的是左上角,跟游戲中的左下角不同(因為Inspector是從上到下繪制)。大小的寬度由Inspector的寬度決定,而高度需要通過在類中override一個方法來自定義高度,否則默認為一行高。

property:待繪制的屬性本身。Unity在編輯器的API中大部分的實際的值都是用一個SerializedProperty表示的,實際上就是對值的一個包裝。通過這個包裝,當我們修改值的時候,Unity可以知道這次操作,類似刷新界面、Undo、prefab修改之類的信息都可以幫我們處理好。壞處在於我們得通過類似FindPropertyRelative的方法,用字符串去尋找內部的值(SerializedProperty是個嵌套結構,內部的數據也是SerializedProperty)。在Unity升級C#來支持nameof之前,我們只能盡量避免修改字段的名字了。同時,我們繪制這些property的時候可以直接用EditorGUI.PropertyField(property),而不用類似的 x = EditorGUI.IntField(x)這樣的調用。

label:這個值在MonoBehaviour里的字段名。

另外,在PropertyDrawer中不能使用帶Layout的類,即EditorGUILayout、GUILayout。( http://answers.unity3d.com/questions/661360/finally-a-solution-cant-use-guilayout-stuff-in-pro.html )用了的話會報個迷之錯誤。不過似乎並不是bug,而是設計如此(不允許PropertyDrawer用Layout)。

最后說一下EditorGUIUtility.labelWidth的使用。

我在最開始實現這個PropertyDrawer時,代碼如下

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        var x = property.FindPropertyRelative("x");
        var y = property.FindPropertyRelative("y");
        float LabelWidth = 50;
        var labelRect = new Rect(position.x, position.y, LabelWidth, position.height);
        var xRect = new Rect(position.x + LabelWidth, position.y, (position.width - LabelWidth) / 2 , position.height);
        var yRect = new Rect(position.x + LabelWidth + (position.width - LabelWidth) / 2  , position.y, (position.width - LabelWidth) / 2 , position.height);
        
        EditorGUI.LabelField(labelRect, label);
        EditorGUI.PropertyField(xRect, x);
        EditorGUI.PropertyField(yRect, y);
    }

最后效果如下

可以看到,int的輸入框被兩個label擠到了右邊。而我想要的效果是類似Box Collider2D里的那種樣式。

而我們用PropertyField繪制的時候,並沒有設置Label寬度的辦法。

隨后找到資料,發現EditorGUIUtility.labelWidth這個屬性。代表的是Label的寬度。比較奇葩的是它是一個可寫的屬性,修改之后,之后繪制的label的寬度就變成了寫進去的值了。不得不說,包括indentLevel在內,這些API設計的都很有想法。

最后解決辦法就是在PropertyField繪制之前,先把labelWidth改小,這樣繪制出來的PropertyField前面的Label寬度就變小了。繪制完之后調回去即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM