Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question How to Update Array Elements in Custom Property Drawers

Discussion in 'UI Toolkit' started by nucky9th, Jun 28, 2023.

  1. nucky9th

    nucky9th

    Joined:
    May 16, 2023
    Posts:
    6
    I'm trying to update how the elements in one entry of an array are displayed, depending on the elements in that array, using the UIToolkit Custom property drawer.

    A simplified version of my SO, with an Entry[]:

    Code (CSharp):
    1. public class EquipmentCardSO : CardSO
    2.     {
    3.         [Serializable]
    4.         public enum Effect
    5.         {
    6.             None, Heal
    7.         }
    8.          
    9.         [Serializable]
    10.         public class Entry
    11.         {
    12.             public Effect effect;
    13.             public int healAmount;
    14.         }
    15.  
    16.         public Entry[] entries;
    17.     }
    What I want to do is create a Custom Property Drawer such that the healAmount property for an entry only displays if the effect is set to Heal.

    I've gotten most of the way there with the following:

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(EquipmentCardSO.Entry))]
    2.     public class EquipmentCardEntryPropertyDrawer : PropertyDrawer
    3.     {
    4.         private VisualElement _healAmount;
    5.  
    6.         public override VisualElement CreatePropertyGUI(SerializedProperty property)
    7.         {
    8.             var root = new VisualElement();
    9.             var effect = new PropertyField(property.FindPropertyRelative("effect"));
    10.             effect.RegisterValueChangeCallback(EffectChanged);
    11.             root.Add(effect);
    12.  
    13.             _healAmount = new PropertyField(property.FindPropertyRelative("healAmount"));
    14.             root.Add(_healAmount);
    15.  
    16.             RemoveAllFields();
    17.            
    18.             return root;
    19.         }
    20.  
    21.  
    22.         private void EffectChanged(SerializedPropertyChangeEvent evt)
    23.         {
    24.             RemoveAllFields();
    25.             switch ((EquipmentCardSO.Effect)evt.changedProperty.intValue)
    26.             {
    27.                 case EquipmentCardSO.Effect.Heal:
    28.                     _healAmount.style.display = DisplayStyle.Flex;
    29.                     break;
    30.             }
    31.         }
    32.  
    33.         private void RemoveAllFields()
    34.         {
    35.             _healAmount.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.None);
    36.         }
    37.     }
    But the problem is, in the EffectChanged callback, I can't find a way to determine if it is this instance of the inspector whose effect property changed. So, if there are several array elements added, and one effect is changed to Heal, it may or may not cause the healAmount field to appear on that particular entry in the array - instead, it might appear elsewhere. Likewise, changing an array elements effect to none, may cause another entry to lose its healAmount field. Adding or removing elements also has this effect.

    On the other hand if I use this:

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(EquipmentCardSO.Entry))]
    2.     public class EquipmentCardEntryPropertyDrawer : PropertyDrawer
    3.     {
    4.         private VisualElement _healAmount;
    5.  
    6.         public override VisualElement CreatePropertyGUI(SerializedProperty property)
    7.         {
    8.             var root = new VisualElement();
    9.             var effect = new PropertyField(property.FindPropertyRelative("effect"));
    10.             root.Add(effect);
    11.  
    12.             _healAmount = new PropertyField(property.FindPropertyRelative("healAmount"));
    13.  
    14.             root.Add(_healAmount);
    15.  
    16.             RemoveAllFields();
    17.             switch ((EquipmentCardSO.Effect)property.FindPropertyRelative("effect").intValue)
    18.             {
    19.                 case EquipmentCardSO.Effect.Heal:
    20.                     _healAmount.style.display = DisplayStyle.Flex;
    21.                     break;
    22.             }
    23.             return root;
    24.         }
    25.  
    26.         private void RemoveAllFields()
    27.         {
    28.             _healAmount.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.None);
    29.         }
    30.     }
    Adding or removing elements updates each entry as would be expected for its assigned effect, but changing an individual entry's effect doesn't cause an update.

    Is there a good way to solve this issue?
     
  2. nucky9th

    nucky9th

    Joined:
    May 16, 2023
    Posts:
    6
    Playing around a bit more and this seems to be a bug. When an entry in the `entries` array is changed, the EffectChanged callback only fires once, but seems to fire for a property other than the one that was changed.
     
  3. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Hi. I don't think it's a bug. Property Drawers are reused by Unity. It uses a single Drawer to generate the UI for all your entries. So, the _healAmount field ends up storing the healAmount PropertyField from the last entry in the UI.

    One solution you could use is to make the _healAmount a local variable, and to make the EffectChanged and RemoveAllFields local methods or lambdas inside CreatePropertyGUI.Something like this:

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(EquipmentCardSO.Entry))]
    2. public class EquipmentCardEntryPropertyDrawer : PropertyDrawer
    3. {
    4.     private VisualElement _healAmount;
    5.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    6.     {
    7.         var root = new VisualElement();
    8.         var effect = new PropertyField(property.FindPropertyRelative("effect"));
    9.         effect.RegisterValueChangeCallback(EffectChanged);
    10.         root.Add(effect);
    11.         var healAmount = new PropertyField(property.FindPropertyRelative("healAmount"));
    12.         root.Add(healAmount);
    13.         RemoveAllFields();
    14.  
    15.         return root;
    16.  
    17.         void EffectChanged(SerializedPropertyChangeEvent evt)
    18.         {
    19.             RemoveAllFields();
    20.             switch ((EquipmentCardSO.Effect)evt.changedProperty.intValue)
    21.             {
    22.                 case EquipmentCardSO.Effect.Heal:
    23.                     healAmount.style.display = DisplayStyle.Flex;
    24.                     break;
    25.             }
    26.         }
    27.  
    28.         void RemoveAllFields()
    29.         {
    30.             healAmount.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.None);
    31.         }
    32.     }
    33. }
    34.  
    Another solution would be to make a custom VisualElement to implement the PropertyDrawer's code. It could receive the SerializedProperty in its constructor. That custom element would be able to store a unique _healAmount field, because a new instance of it would be created everytime CreatePropertyGUI is called.
     
    nucky9th likes this.
  4. nucky9th

    nucky9th

    Joined:
    May 16, 2023
    Posts:
    6
    Thanks for the help, this worked perfectly, although the initialization of healAmount needed to come before registering the callback.
     
    oscarAbraham likes this.