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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

SerializeReference, inheritance, UnityEvent and PropertyField()

Discussion in 'Editor & General Support' started by sunfl0wr, Nov 16, 2020.

  1. sunfl0wr

    sunfl0wr

    Joined:
    Mar 11, 2015
    Posts:
    26
    Not sure if this was right place to post this but I'm having some issues with EditorGUILayout.PropertyField() on a list containing polymorphic objects. I have a abstract base class - let's call it BaseType the child classes Type1 and Type2 both contain UnityEvents using different generic types. I've attached some sample code kind of explaining it bellow.

    I'm using a CustomEditor to display this. And using EditorGUILayout.PropertyField() to display the UnityEvent's but for some reason the PropertyField seem to ignore the type. It only display as "evt ()" and not "evt (int)" or "evt (float)" I've put breakpoints in the OnInspectorGUI() function and I've verified that the property type is correct.. so why isn't it working? I've verified that Manager.test2 is render properly by PropertyField.. so it seem to be related to SerializeReference and polymorphism. The property type is the same in both Manager.test2 and Manager.evens when i step through with the debugger.. but from some reason PropertyField is rendering them differently.

    Any clues? Is this a bug in unity? I'm using Unity 2019.4.14f1 (LTS), I was using 2019.4.10f1 (LTS) but upgraded when I discovered the issue.. was hoping it would work in the newer version.. but I still have the same issue.


    Code (CSharp):
    1.  
    2. [Serializable]
    3. abstract class BaseType
    4. {
    5. }
    6.  
    7. [Serializable]
    8. class Type1 : BaseType
    9. {
    10.   [Serializable]
    11.   class IntEvent : UnityEvent<int> {}
    12.  
    13.   [SerializeField]
    14.   IntEvent evt;
    15. }
    16.  
    17. [Serializable]
    18. class Type2: BaseType
    19. {
    20.   [Serializable]
    21.   class FloatEvent : UnityEvent<float> {}
    22.  
    23.   [SerializeField]
    24.   FloatEvent evt;
    25. }
    26.  
    27. class Manager
    28. {
    29.   [SerializeReference]
    30.   List<BaseType> events;
    31.  
    32.   [SerializeField]
    33.   Type2.FloatEvent test2;
    34. }
    35.  
     
  2. sunfl0wr

    sunfl0wr

    Joined:
    Mar 11, 2015
    Posts:
    26
    Updated to 2019.4.15f1, still an issue.
     
  3. sunfl0wr

    sunfl0wr

    Joined:
    Mar 11, 2015
    Posts:
    26
  4. JabukeGrada

    JabukeGrada

    Joined:
    Aug 14, 2015
    Posts:
    6
    Well, same for me. How it even possible?

    UPD: Ok, that's why:

    Code (CSharp):
    1. private static UnityEventBase GetDummyEvent(SerializedProperty prop)
    2. {
    3.     UnityEngine.Object targetObject = prop.serializedObject.targetObject;
    4.     if (targetObject == null)
    5.     {
    6.         return new UnityEvent();
    7.     }
    8.     UnityEventBase unityEventBase = null;
    9.     Type type = targetObject.GetType();
    10.  
    11.     BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    12.     do
    13.     {
    14.         unityEventBase = GetDummyEventHelper(prop.propertyPath, type, flags);
    15.         flags = (BindingFlags.Instance | BindingFlags.NonPublic);
    16.         type = type.BaseType;
    17.     }
    18.     while (unityEventBase == null && type != null);
    19.     return (unityEventBase == null) ? new UnityEvent() : unityEventBase;
    20. }
    UnityEvent trying to reproduce type by reflection using propertyPath, but get stuck because can't find such method based on only interface information. Then it returns some default impl e.g. UnityEvent(). So, in order to work correctly it must depend on SerializedProperty info rather than on reflection.
     
    Last edited: Dec 1, 2020
    SolidAlloy likes this.
  5. sunfl0wr

    sunfl0wr

    Joined:
    Mar 11, 2015
    Posts:
    26
    Nice to see at least someone noticing my post :)
    So you're saying the entire UnityEvent thing is broken due to using some homemade solution instead of reflection?

    I searched around and found the source for the helper:
    https://github.com/Unity-Technologi...ditor/Mono/Inspector/UnityEventDrawer.cs#L418

    Using the immediate window in visual studio I got this far (property is the "evt" variable that is a UnityEvent<> I'm trying to use):


    property.serializedObject.targetObject.GetType().GetField("events", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).FieldType.GetGenericArguments()[0]

    This returns the BaseType which is understandable as it's doing this weird thing to fetch the type... but that is wrong obviously as it's not checking the elements type but the lists type..

    Maybe I missed something, but that's what it looks like. And if that's the case:

    >UNITY<.. is this something you plan fixing, this seems like a bug?
     
    SolidAlloy likes this.
  6. sunfl0wr

    sunfl0wr

    Joined:
    Mar 11, 2015
    Posts:
    26
    Bump! Is anyone from unity reading this forum?
     
  7. sunfl0wr

    sunfl0wr

    Joined:
    Mar 11, 2015
    Posts:
    26
  8. sindrijo_

    sindrijo_

    Joined:
    Aug 15, 2016
    Posts:
    7
    I also ran headfirst into this issue, I actually fixed it myself copying the existing code (UnityEventDrawer), making the required changes and overriding the drawer used for a base abstract class inheriting from UnityEvent<T>. I don't have that code here, but I can share a repro of sorts.


    Code (CSharp):
    1.  
    2. using System;
    3. using UnityEngine;
    4. using UnityEngine.Events;
    5. using Random = UnityEngine.Random;
    6.  
    7. namespace SerializedReferenceAndUnityEvents
    8. {
    9.     public class UnityEventSerializedReferences : MonoBehaviour
    10.     {
    11.         [SerializeReference] private UnityEventContainer unityEventContainer = new StringEventContainer();
    12.  
    13. #if UNITY_EDITOR
    14.         private void Reset()
    15.         {
    16.             if (Random.Range(0, 2) == 1)
    17.             {
    18.                 var stringEventContainer = new StringEventContainer();
    19.                 stringEventContainer.AddPersistentListener(HandleStringEvent);
    20.                 unityEventContainer = stringEventContainer;
    21.  
    22.             }
    23.             else
    24.             {
    25.                 var floatEventContainer = new FloatEventContainer();
    26.                 floatEventContainer.AddPersistentListener(HandleFloatEvent);
    27.             }
    28.          
    29.             // To demonstrate that this code should work:
    30.             // Persistent listeners have been manually registered
    31.             // on the respective events, and are now invoking them.
    32.             // Result should be that the values are logged.
    33.             // It should work at runtime as well.
    34.             // The issue is that UnityEventDrawer doesn't
    35.             // handle the case when closed specialization
    36.             // of UnityEvent<T> is serialized as a or on a reference.
    37.             unityEventContainer.InvokeWithDefaultValue();
    38.         }
    39. #endif
    40.  
    41.         public void HandleFloatEvent(float value)
    42.         {
    43.             Debug.Log($"{nameof(HandleFloatEvent)}: Value was '{value}'");
    44.         }
    45.  
    46.         public void HandleStringEvent(string value)
    47.         {
    48.             Debug.Log($"{nameof(HandleStringEvent)}: Value was '{value}'");
    49.         }
    50.  
    51.         private void Start()
    52.         {
    53.             unityEventContainer.InvokeWithDefaultValue();
    54.         }
    55.     }
    56.  
    57.  
    58.     [System.Serializable]
    59.     public abstract class UnityEventContainer
    60.     {
    61.         public abstract void InvokeWithDefaultValue();
    62.     }
    63.  
    64.     [System.Serializable]
    65.     public abstract class UnityEventContainer<T, TUnityEvent> : UnityEventContainer
    66.         where TUnityEvent : UnityEvent<T>, new()
    67.     {
    68.         [SerializeField] protected T defaultArgument;
    69.         [SerializeField] private TUnityEvent concreteClosedTypedEvent = new TUnityEvent();
    70.  
    71.         public override void InvokeWithDefaultValue()
    72.         {
    73.             concreteClosedTypedEvent.Invoke(defaultArgument);
    74.         }
    75.      
    76.         public void AddPersistentListener(UnityAction<T> listener)
    77.         {
    78. #if UNITY_EDITOR
    79.             var insertionIndex = concreteClosedTypedEvent.GetPersistentEventCount();
    80.             UnityEditor.Events.UnityEventTools.AddPersistentListener(concreteClosedTypedEvent, listener);
    81.             concreteClosedTypedEvent.SetPersistentListenerState(insertionIndex, UnityEventCallState.EditorAndRuntime);
    82. #else
    83.             Debug.LogWarning("Persistent listeners can only be added at edit-time.");
    84. #endif
    85.         }
    86.     }
    87.  
    88.     [System.Serializable]
    89.     public class UnityStringEvent : UnityEvent<string> {}
    90.  
    91.     [System.Serializable]
    92.     public class StringEventContainer : UnityEventContainer<string, UnityStringEvent>
    93.     {
    94.         public StringEventContainer()
    95.         {
    96.             defaultArgument = "Hello, HAL do you read me?";
    97.         }
    98.     }
    99.  
    100.     [System.Serializable]
    101.     public class UnityFloatEvent : UnityEvent<float> {}
    102.  
    103.     [System.Serializable]
    104.     public class FloatEventContainer : UnityEventContainer<float, UnityFloatEvent>
    105.     {
    106.         public FloatEventContainer()
    107.         {
    108.             defaultArgument = 42.1337f;
    109.         }
    110.     }
    111. }
    112.  
     
  9. Jamez0r

    Jamez0r

    Joined:
    Jul 29, 2019
    Posts:
    200
    Hey @sindrijo_ , I just ran into the exact issue on this thread - I've been working on a State Machine system for a couple weeks now and just noticed this totally unexpected problem. If I can't get these Unity Events to work then I will have wasted all of this time, I'm very much relying on using them.

    Would it be possible for you to post your code that fixed the issue? I looked through the code you posted but I'm afraid I don't know enough about what is going on to understand it enough to fix the issue myself.

    Would REALLY appreciate it, thanks if you can!
     
  10. CroshB

    CroshB

    Joined:
    Jul 27, 2017
    Posts:
    1
    I think I've found the issue and made a fixed version of the UnityEventDrawer. It may have conflicts with other codes you might have that change the inspector for unity events though, but for a start, it works. Here is the code, just add it to your projects:

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Reflection;
    5. using UnityEditor;
    6. using UnityEditorInternal;
    7. using UnityEngine;
    8. using UnityEngine.Events;
    9.  
    10. [CustomPropertyDrawer(typeof(UnityEvent<>))]
    11. public class UnityEventDrawerFixed : UnityEventDrawer {
    12.  
    13.     private const string kDotString = ".";
    14.     private const string kArrayDataString = "Array.data[";
    15.     private const string kArrayDataEndString = "]";
    16.     private static readonly char[] kDotSeparator = { '.' };
    17.  
    18.     string m_Text { get => GetFieldValue<string>("m_Text"); set => SetFieldValue("m_Text", value); }
    19.     UnityEventBase m_DummyEvent { get => GetFieldValue<UnityEventBase>("m_DummyEvent"); set => SetFieldValue("m_DummyEvent", value); }
    20.     SerializedProperty m_Prop { get => GetFieldValue<SerializedProperty>("m_Prop"); set => SetFieldValue("m_Prop", value); }
    21.     SerializedProperty m_ListenersArray { get => GetFieldValue<SerializedProperty>("m_ListenersArray"); set => SetFieldValue("m_ListenersArray", value); }
    22.  
    23.     int m_LastSelectedIndex { get => GetFieldValue<int>("m_LastSelectedIndex"); set => SetFieldValue("m_LastSelectedIndex", value); }
    24.     ReorderableList m_ReorderableList { get => GetFieldValue<ReorderableList>("m_ReorderableList"); set => SetFieldValue("m_ReorderableList", value); }
    25.  
    26.     State RestoreState(SerializedProperty property) => CallMethod<State>("RestoreState", property);
    27.  
    28.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    29.     {
    30.         m_Prop = property;
    31.         m_Text = label.text;
    32.  
    33.         State state = RestoreState(property);
    34.  
    35.         OnGUI(position);
    36.         state.lastSelectedIndex = m_LastSelectedIndex;
    37.     }
    38.  
    39.     public new void OnGUI(Rect position) {
    40.         if (m_ListenersArray == null || !m_ListenersArray.isArray)
    41.             return;
    42.  
    43.         m_DummyEvent = GetDummyEvent(m_Prop);
    44.         if (m_DummyEvent == null)
    45.             return;
    46.  
    47.         if (m_ReorderableList != null)
    48.         {
    49.             var oldIdentLevel = EditorGUI.indentLevel;
    50.             EditorGUI.indentLevel = 0;
    51.             m_ReorderableList.DoList(position);
    52.             EditorGUI.indentLevel = oldIdentLevel;
    53.         }
    54.     }
    55.  
    56.     static UnityEventBase GetDummyEvent(SerializedProperty prop) {
    57.         object tgtobj = prop.serializedObject.targetObject;
    58.         if (tgtobj == null)
    59.             return new UnityEvent();
    60.  
    61.         UnityEventBase ret = GetDummyEventHelper(prop.propertyPath, tgtobj);
    62.         return (ret == null) ? new UnityEvent() : ret;
    63.     }
    64.  
    65.     private static UnityEventBase GetDummyEventHelper(string propPath, object targetObject)
    66.     {
    67.         var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    68.         var targetObjectType = targetObject.GetType();
    69.  
    70.         while (propPath.Length != 0)
    71.         {
    72.             string split;
    73.             if (propPath.StartsWith(kArrayDataString))
    74.             {
    75.                 var splitIndex = propPath.IndexOf(kArrayDataEndString) + 1;
    76.                 split = propPath.Substring(0, splitIndex);
    77.                 propPath = propPath.Substring(splitIndex + 1);
    78.             }
    79.             else
    80.             {
    81.                 var splits = propPath.Split(kDotSeparator, 2);
    82.                 split = splits[0];
    83.                 propPath = splits.Length == 2
    84.                     ? splits[1]
    85.                     : String.Empty;
    86.             }
    87.  
    88.             if (split.StartsWith(kArrayDataString))
    89.             {
    90.                 var index = Int32.Parse(split.Substring(kArrayDataString.Length, split.IndexOf(kArrayDataEndString) - kArrayDataString.Length));
    91.                 if (targetObjectType.IsArray)
    92.                 {
    93.                     Array targetObjectArray = (Array) targetObject;
    94.                     targetObject = targetObjectArray.GetValue(index);
    95.                 }
    96.                 else if (targetObjectType.IsGenericType && targetObjectType.GetGenericTypeDefinition() == typeof(List<>))
    97.                 {
    98.                     IList targetObjectList = (IList) targetObject;
    99.                     targetObject = targetObjectList[index];
    100.                 }
    101.                 else
    102.                 {
    103.                     // Unexpected serialized Array type
    104.                     return null;
    105.                 }
    106.             }
    107.             else
    108.             {
    109.                 FieldInfo field;
    110.                 do
    111.                 {
    112.                     field = targetObjectType.GetField(split, flags);
    113.  
    114.                     if (field == null)
    115.                     {
    116.                         targetObjectType = targetObjectType.BaseType;
    117.                         if (targetObjectType == null) // Reached top of hierarchy without finding field
    118.                             return null;
    119.                     }
    120.                 }
    121.                 while (field == null);
    122.  
    123.                 targetObject = field.GetValue(targetObject);
    124.             }
    125.             targetObjectType = targetObject.GetType();
    126.         }
    127.  
    128.         return targetObjectType.IsSubclassOf(typeof(UnityEventBase))
    129.             ? Activator.CreateInstance(targetObjectType) as UnityEventBase
    130.             : null;
    131.     }
    132.  
    133.     private TReturn GetFieldValue<TReturn>(string fieldName) {
    134.         return (TReturn) typeof(UnityEventDrawer)
    135.             .GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance)
    136.             .GetValue(this);
    137.     }
    138.  
    139.     private void SetFieldValue(string fieldName, object value) {
    140.         typeof(UnityEventDrawer)
    141.             .GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance)
    142.             .SetValue(this, value);
    143.     }
    144.  
    145.     private TReturn CallMethod<TReturn>(string methodName, params object[] parameters) {
    146.         return (TReturn) typeof(UnityEventDrawer)
    147.             .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)
    148.             .Invoke(this, parameters);
    149.     }
    150.  
    151. }
    152.  
    If anyone's interested, the issue is that Unity's GetDummyEventHelper in UnityEventDrawer doesn't take the object into account when navigating through the propertyPath, so instead of trying to find the fields in the actual objects, it tries to find the fields in the types, which sometimes are the base types and not the final types. Another issue is that Unity's propertyPath navigation ignores the array index.

    My code basically extends UnityEventDrawer and with reflection I tried to mimic the code from OnGUI until it reached GetDummyEventHelper where I've basically rewritten it. The implementation for UnityEventDrawer I had to recreate is in this link.
     
    Jamez0r likes this.