Search Unity

Property Drawer for objectRef

Discussion in 'UI Toolkit' started by Sibz9000, Aug 2, 2020.

  1. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    I have a serialized object reference, and I am wanting to make a property drawer that uses the target objects serialized data.

    So far I have got this working:
    Code (CSharp):
    1.  
    2. [CustomPropertyDrawer(typeof(Wall))]
    3. public class WallDrawer : PropertyDrawer
    4. {
    5.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    6.     {
    7.         VisualElement result = new VisualElement();
    8.         // This just calls Resources.Load or loads from cache.
    9.         SingleAssetLoader.Load<VisualTreeAsset>("Wall").CloneTree(result);
    10.         var so = new SerializedObject(property.objectReferenceValue as Wall);
    11.         result.Bind(so);
    12.         result.RegisterCallback((ChangeEvent<string> e) =>
    13.         {
    14.             so.ApplyModifiedProperties();
    15.         });
    16.         return result;
    17.     }
    18. }
    19.  
    However I can not make it save values changed in the new serialised object. I added in the register callback so see if that would help, but ApplyModifiedProperties returns false.

    Not sure how I can do this, any ideas?
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    You don't need:
    - new SerializedObject()
    - Bind()
    - ApplyModifiedProperties()

    simply returning "result" should be enough.

    All that work is being done for you by the UI Toolkit binding system and the Inspector window.

    But even if you created some custom EditorWindow with UI bound to your own SerializedObject, with UI Toolkit, you don't need to call ApplyModifiedProperties() anymore. This is called for you when the UI Toolkit binding system catches the ChangeEvents coming from UI fields.
     
  3. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    Thank you for your reply.

    I am not sure you understood the initial issue. The property in this case is an object field. So I am trying to override the the default display of an object field, and instead have fields that edit the target MonoBehaviour directly.
    If I try
    property.FindPropertyRelative
    it returns null for any properties on the target as expected, as this is a leaf property with only objectReferenceValue set.
    So here I was hoping to bind a new serialised object to the tree below result.
    I am basically wanting to insert the inspector for the target object so I don't have to switch to that objects inspector to edit it's fields.
     
  4. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    I see now. Ya, I did misunderstood.

    Inspectors within inspectors are a bit tricky with UI Toolkit. You might be able to use the InspectorElement element and bind it to property.objectReferenceValue. InspectorElement is also what the main Unity Inspector Window uses for each component on your object to auto-generate the UI from SerializedProperties.

    That said, you cannot do this directly in CreatePropertyGUI(), as that only gets executed once when you first select your main target object. So you only get the value of property.objectReferenceValue that one time and nothing will change when the value changes. This is because UI Toolkit, unlike IMGUI, is retained mode so the UI is an persisted object hierarchy that gets rendered behind the scenes by the system, not by you.

    To get around this, you should still use what I mentioned above - the PropertyField - to create your Object Field from the property. In addition to the PropertyField, create an InspectorElement and add it as the second child of "result". Finally, use `myPropertyField.RegisterCallback<ChangeEvent<Object>>(callback)` to register yourself to changes from the ObjectField.

    In this "callback()", you can call Bind() on your InspectorElement with the new value of the ObjectField. You'll need to also call this callback (maybe an override that takes in just an Object as a parameter instead of a ChangeEvent<Object>) on the first CreatePropertyGUI().

    So now you will need to manually call Bind() and new SerializedObject(), but only on the property.objectReferenceValue, and not the current property.serializedObject. But you still don't need to use ApplyModifiedProperties().

    If you have trouble with InspectorElement, you can also generate an inspector yourself by iterating the SerializedProperties. Here's a previous thread that went into some detail on this:
    https://forum.unity.com/threads/property-drawers.595369/#post-5426619
     
    Last edited: Aug 8, 2020
    Sibz9000 likes this.
  5. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    Whew, Thanks for the reply, looks tricky but I'll give it a go. For my current application, I ended up making a class to contain the variables on the target object, and a local list of that class, and I just been updating the target object manually.
     
  6. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    Currently got the adding inspector working, however this causes the inspector I am adding it to, to become empty. Looking at generating inspector myself that is almost what I was doing first off, only I put that inside the drawer, and I couldn't get the changes to save the the target object.
     
    Last edited: Aug 8, 2020
  7. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    You'll need to add more specifics before I can help. Try to simplify the code until it just shows the problem and post that maybe.
     
  8. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    Code (CSharp):
    1.  public override VisualElement CreatePropertyGUI(SerializedProperty property)
    2.         {
    3.             VisualElement result = new VisualElement();
    4.             var ie = new InspectorElement();
    5.             result.Add(ie);
    6.             return result;
    7.         }
    8.  
    That alone makes the inspector hosting the property field become empty. I tried adding a label and that did not appear either so not sure this approach will work. It seems the editor doesn't like inspectors inside inspectors.

    I'm probably going to continue to use my method of storing all the data on the gameobject with my custom inspector, and either setting the data on the target, or calling target methods directly from the parent game objects inspector.
    This way I can make a property drawer for the data enclosed in it's own class. And if have the data field on the target, that automatically gets the drawer too.

    This is the basic setup I have gone with:
    Code (CSharp):
    1.  
    2. [Serializable]
    3. public class MyTargetObjectDataSet
    4. {
    5.     public GameObject Target;
    6.     public string SomeString;
    7.     public float SomeFloatThatRequiresCustomFieldsToCalculate;
    8. }
    9.  
    10. public class MyObjectThatGetsSelectedBehaviour : MonoBehaviour
    11. {
    12.     public float SomeFloat;
    13.     public MyTargetObjectDataSet TargetData;
    14.  
    15.     private void OnValidate()
    16.     {
    17.         TargetData.Target.GetComponent<MyTargetObject>().Data = TargetData;
    18.     }
    19. }
    20.  
    21. public class MyTargetObject : MonoBehaviour
    22. {
    23.     private MyTargetObjectDataSet data;
    24.  
    25.     public MyTargetObjectDataSet Data
    26.     {
    27.         get => data;
    28.         set
    29.         {
    30.             data = value;
    31.             OnUpdateData();
    32.         }
    33.     }
    34.  
    35.     private void OnUpdateData()
    36.     {
    37.         // Do stuff when data gets updated here
    38.     }
    39. }
    40.  
    41. [CustomPropertyDrawer(typeof(MyTargetObject))]
    42. public class MyTargetObjectDrawer : PropertyDrawer
    43. {
    44.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    45.     {
    46.         var result = new VisualElement();
    47.         // Load/create the content here
    48.         return result;
    49.     }
    50. }
    51.  
    52. [CustomEditor(typeof(MyObjectThatGetsSelectedBehaviour))]
    53. public class MyObjectThatGetsSelectedInspector : Editor
    54. {
    55.     public override VisualElement CreateInspectorGUI()
    56.     {
    57.         var result = new VisualElement();
    58.         // Load/create custom inspector here
    59.         return result;
    60.     }
    61. }
    62.  
    One issue here is I need to manually update the MyTargetObject.data field which I do from OnValidate, however this has limitations as what you can do from there and will update the data field if anything else changes too. Ideally I wanted to catch a any-field-change event on the MyTargetObject property element, but I couldn't figure a way to do that so ended up with OnValidate.

    It still feels like a bit of a work, for just wanting to edit another game object in-situ without having to switch to that game object. In the end I can just keep all the data on the 'parent' gameobject and act from there, however would be nice to have the target objects be the primary store for their own data, and be able to edit them directly and from the 'parent' game object.
     
  9. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231