Search Unity

Question Building a Custom Property Drawer for Serializable Class

Discussion in 'Scripting' started by ClearRoseOfWar, Feb 24, 2022.

  1. ClearRoseOfWar

    ClearRoseOfWar

    Joined:
    Sep 6, 2015
    Posts:
    89
    Greetings,

    I have been researching how to construct a custom property drawer for this Class:

    public class EnemyPreset : ScriptableObject
    {
    public DropTable[] dropTable;
    }


    [System.Serializable]
    public class DropTable{
    public GameObject item;
    [Range(0.1f,100)]
    public float chance;
    }


    When adding to the drop table, it says "Element0" -> Id like it to say "item.name + "/ "chance" instead

    I tried using execute in edit mode with update trying to set the name of a string, but that didn't work. I then started looking into property drawers but for the life of me I can't understand how to make heads or tales.

    I know that based on what I seen while doing research it shouldn't be as complicated as it seems... can anyone offer some assistance here?

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [CustomPropertyDrawer(typeof(DropTable))]
    5. public class DropTablePropertyDrawer : PropertyDrawer
    6. {
    7.     // Draw the property inside the given rect
    8.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    9.     {
    10.         EditorGUI.BeginProperty(position, label, property);
    11.        
    12.         position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
    13.  
    14.         EditorGUI.EndProperty();
    15.     }
    16. }

    Thank you!
     
    Last edited: Feb 24, 2022
  2. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    Hi,

    I didn't read your code, but what exactly isn't working?

    Using property drawers isn't that difficult, main difference between drawing fields (compared to custom editors for example) is the fact that you'll have to use non-automatic layout versions of GUI elements, so you'll have to define rects for elements you want to draw.

    And did you check the documentation? It has some pretty nice examples.
     
    Last edited: Feb 24, 2022
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Usually when you create a custom property drawer for such a small class, the intention is to inline the content. Have a look at this example:

    Code (CSharp):
    1.     [CustomPropertyDrawer(typeof(DropTable))]
    2.     public class DropTableDrawer : PropertyDrawer
    3.     {
    4.         public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    5.         {
    6.             return EditorGUIUtility.singleLineHeight;
    7.         }
    8.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    9.         {
    10.             var itemProp = property.FindPropertyRelative("item");
    11.             var chanceProp = property.FindPropertyRelative("chance");
    12.             if (itemProp.objectReferenceValue is GameObject go && go != null)
    13.                 label = new GUIContent(go.name + " / " + chanceProp.floatValue);
    14.             Rect pos = EditorGUI.PrefixLabel(position, label);
    15.             pos.width *= 0.5f;
    16.             Rect p1 = pos,p2 = pos;
    17.             p2.x += pos.width;
    18.             EditorGUI.PropertyField(p1, itemProp, GUIContent.none);
    19.             EditorGUI.PropertyField(p2, chanceProp, GUIContent.none);
    20.         }
    21.     }
    22.  
    It does what you wanted as it changes the prefix lable to the item name + the chance value if an item has been assigned. If not it would have its default label. Furthermore the property is drawn as a single line. The remaining space (pos) that is returned by the PrefixLabel method is split in half. So the the object field for the item field would take the first half, the chance field would take the second half. I removed the labels of each field of those fields (by passing GUIContent.none as label) to have more space.

    Technically you would not need the prefix label at all and could only show the object field and the chance field in line. Though when you want the label, you could simply reduce the size of the object field to a small fix width to just allow the user to drop the reference. The name would be shown in the label anyways.

    Personally I would go with

    Code (CSharp):
    1.  
    2.             Rect pos = position;
    3.             pos.width *= 0.5f;
    4.             Rect p1 = pos,p2 = pos;
    5.             p2.x += pos.width;
    6.             EditorGUI.PropertyField(p1, property.FindPropertyRelative("item"), GUIContent.none);
    7.             EditorGUI.PropertyField(p2, property.FindPropertyRelative("chance"), GUIContent.none);
    8.  
    or as I said reduce the size of the object field so there's more room for the chance:

    Code (CSharp):
    1.             var itemProp = property.FindPropertyRelative("item");
    2.             var chanceProp = property.FindPropertyRelative("chance");
    3.             if (itemProp.objectReferenceValue is GameObject go && go != null)
    4.                 label = new GUIContent(go.name + " / " + chanceProp.floatValue);
    5.             Rect pos = EditorGUI.PrefixLabel(position, label);
    6.             Rect p1 = pos,p2 = pos;
    7.             p1.width = 32f;
    8.             p2.xMin += p1.width;
    9.             EditorGUI.PropertyField(p1, itemProp, GUIContent.none);
    10.             EditorGUI.PropertyField(p2, chanceProp, GUIContent.none);
    11.  
    This would make the item object field only 32 pixels wide and the rest is left for the chance slider.
     
    eses likes this.
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    I would have added the images to my previous post, however for some reason I'm not allowed to save the changes when I upload images in the edit -.-

    With prefix label and object field width of 32:
    PropertyDrawerDropTable.png

    Without prefix lable and just a 50:50 split
    PropertyDrawerDropTable2.png
     
    Xepherys, ClearRoseOfWar and eses like this.
  5. ClearRoseOfWar

    ClearRoseOfWar

    Joined:
    Sep 6, 2015
    Posts:
    89
    @eses I did read the documentation and I couldn't quite make sense of what it was doing lol. I watched a few tutorials and read some forums. Seeing it in action in context made it much easier to understand.

    @Bunny83 Thank you very much for your long and very detailed answer!
    This has taught me so much. The working example allows me to see exactly how this works right off the hop which is super helpful, so now I see how i'll be able to use it in othe scripts..

    But I've also already implemented the ability to detect if the drop is arrows, and if so it shows the dropdown for the type..
    I can't thank you enough Bunny. You seem to be around here a lot, and I am very grateful for it!
     
  6. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @Bunny83 sorry but I can't resist asking this since it is related to formatting this kind of lines of information :). Is there any way to add "padding" around/after IMGUI UI elements (other than what I've used, see below)? Like the slider in your images. By default those elements seem to touch or even overlap each other very easily when using proportion of inspector width (position rect) for the width of the element.

    Because of this, I've simply done something like this:
    Code (CSharp):
    1. p2.x += pos.width + 4;

    Which results in non-overlapping elements.
    upload_2022-2-24_17-45-43.png
     
    Bunny83 likes this.
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Well, the non layouted versions (which we have to use in property drawers) do draw the elements exactly as specified. They should never overlap unless you let the rects overlap. Note that a GUIStyle could in deed specify a contentOffset or overflow which may influence if a style can actually draw outside the specified rect. While the GUIStyle class does have a margin and padding setting, those are mainly used by the layout system. I'm not sure if the padding actually influences the drawing of the GUIStyle or if it's only used by the layout system. Though changing the GUIStyle itself or creating a new style may not necessarily be a better solution.

    Be warned when changing x, y, width or height. The x and y values are essentially absolute coordinates while width and height are relative values. A Rect also has the xMin, yMin, xMax and yMax values. Those represent absolute coordinates and keep the others unchanged. So for example increasing xMin by 4 would increment x by 4 and would also decrease width by 4 so xMax stays the same. You may also want to use xMax of the first element as a reference where to start the next element. Another way to split a Rect into two regions would be to set the width of the first rect and then adjust xMin of the second to be the same as xMax of the first rect here you may add an additional margin but it would keep the end of the rect unchanged.

    Also an often overlooked useful tool is the RectOffset struct. It represents relative offsets for a rect. You can use the Add and Remove methods to "use" those offset values on a given Rect and return a modified Rect with those offsets applied. This can be useful in more complex setups where the same relative adjustments are required several times.
     
    ClearRoseOfWar and eses like this.
  8. ClearRoseOfWar

    ClearRoseOfWar

    Joined:
    Sep 6, 2015
    Posts:
    89
    How would I go about changing the title of the drop table? So that instead, it displays the names of each property
    Im currently looking into it, but I see that label is passed through, which can change the name of each property, but I fail to see a way (yet) to change the title to display this for example:

    "Drop Table: [Heart][Arrows][Sword]"


    This way, I can collapse it but still see the contents if I'd like. I'll post back if / when I get the answer.
     
    Last edited: Feb 25, 2022
    Bunny83 likes this.
  9. ClearRoseOfWar

    ClearRoseOfWar

    Joined:
    Sep 6, 2015
    Posts:
    89
    So far, I haven't been too successful.

    I messed around with the IntelliSense trying various ideas, but no success.

    I have been googling but all I have found so far that seems to lead in a positive direction is this:
    http://answers.unity.com/answers/628793/view.html

    But the above does not work in this context;
    and so as foolish as this attempt will appear, I tried this (lol):
    Code (CSharp):
    1. SerializedObject obj = property.serializedObject;
    2. EditorGUILayout.PropertyField(obj.FindProperty("DropTable"), new GUIContent("aDifferentLabel"));
    It did compile, but of course... it didn't work XD

    I will find that answer..
     
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Do you mean you want to show the first names of the items in the dropTable field name when it's collapsed? Unfortunately you can not use property drawers on an array or List field directly. However what you could do is wrap your drop table array into a seperate class and create a property drawer for that class. If you want to do that, I would highly recommend to rename your DropTable class into something more descriptive like "DropItem" and use "DropTable" for the wrapping class which actually represents a table. Currently your DropTable class does not represent a table.

    Note that I would not recommend such an extension of the array field. One point of collapsing an array field (besides having more space for other things) is to increase performance of the inspector. If the array field property drawer also crawls through all items in the table it would lower the performance. While I can see such a feature to be useful in some cases, you really should think about if it's worth it. What if you have 20 or 100 items in the table? You would have to restrict the display to just a hand full of items. So the information is not really that useful.

    Since you use scriptable objects for presets (which is great) you may think about seperating your droptable into a seperate scriptable object that can be reused by your EnemyPresets. So an EnemyPreset may just have a reference to a seperate DropTable. Now you are free to name your drop table as you want.
     
    ClearRoseOfWar likes this.
  11. ClearRoseOfWar

    ClearRoseOfWar

    Joined:
    Sep 6, 2015
    Posts:
    89
    Thank you for your reply.

    Yes, that was the idea but you've made some valid points and I can live without the items being drawn in the title like that. I have personally run into a performance drop due to a busy inspector in a different project, and the enemy scriptable object is already getting busy enough..

    Side Question: Can a property drawer fix performance issues related to the inspector?

    Although I like the idea, each enemy will more than likely drop a different combination of items, so it does make sense to keep them on the enemies preset in this sense. It just iterates through each item and picks one, then checks if you get the item using the chance field.

    I will say, you're absolutely right about it being a DropItem, and not a DropTable. The array name is fine, but I'm going to go change that class name cause now its bugging me lol.

    So after all that tinkering and wondering - In the end - All I have to do is click on a drop-down every now and again - it's not going to destroy my workflow.
     
    Last edited: Feb 25, 2022