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 Please help me make sense of this serialization mess with abstract classes and generic types.

Discussion in 'Scripting' started by haxic, May 21, 2023.

  1. haxic

    haxic

    Joined:
    Jul 12, 2021
    Posts:
    20
    I'm trying to build a simple "trigger" system similar to the warcraft/starcraft map editors, that I want to be able to edit through the inspector. The actual logic/data structure for the trigger system is build up through polymorphism (abstract classes and generic types) and doesn't need any Monobehaviour/ScriptableObject classes to function (during runtime). However, I want to be able to create triggers in the inspector and have them persist, and so far I haven't been able to achieve that without refactoring my code in ways that are completely unnecessary for its runtime execution. Below is a very basic example (pseudo-isch code, it may not work out of the box) of how some of the trigger components are strung together:

    Code (CSharp):
    1.   public class TestUnit : MonoBehaviour {
    2.   }
    Code (CSharp):
    1.   public class UnitSystem : MonoBehaviour {
    2.     [SerializeField]
    3.     public Comparer unitComparer;
    4.     private void OnValidate() {
    5.       unitComparer ??= new UnitComparer();
    6.     }
    7.     public void Update() {
    8.       Debug.Log(unit.Check());
    9.     }
    10.   }
    Code (CSharp):
    1.   [Serializable]
    2.   public abstract class Comparer {
    3.     public abstract bool Check();
    4.   }
    Code (CSharp):
    1.   [Serializable]
    2.   public class UnitComparer : Comparer {
    3.     [SerializeField]
    4.     public Selector<TestUnit> selector;
    5.     public override bool Check() {
    6.       return EqualityComparer<TestUnit>.Default.Equals(selector.selection1.GetValue(), selector.selection2.GetValue());
    7.     }
    8.   }
    Code (CSharp):
    1.   [Serializable]
    2.   public class Selector<DataType> {
    3.     [SerializeField]
    4.     public Selection<DataType> selection1;
    5.     [SerializeField]
    6.     public Selection<DataType> selection2;
    7.   }
    Code (CSharp):
    1.   [Serializable]
    2.   public class Selection<DataType> {
    3.     [SerializeField]
    4.     public SelectionType<DataType> selectionType;
    5.     public virtual DataType GetValue() {
    6.       return selectionType.GetValue();
    7.     }
    8.   }
    Code (CSharp):
    1.   [Serializable]
    2.   public abstract class SelectionType<DataType> {
    3.     public abstract DataType GetValue();
    4.   }
    Code (CSharp):
    1.  [Serializable]
    2.   public class ValueSelectionType<DataType> : SelectionType<DataType> {
    3.     [SerializeField]
    4.     public DataType value;
    5.     public override DataType GetValue() {
    6.       return value;
    7.     }
    8.   }
    Code (CSharp):
    1.   [Serializable]
    2.   public class VariableSelectionType<DataType> : SelectionType<DataType> {
    3.     [SerializeField]
    4.     public Variable<DataType> variable;
    5.     public override DataType GetValue() {
    6.       return variable.value;
    7.     }
    8.   }
    Code (CSharp):
    1.   [CustomPropertyDrawer(typeof(Selection<>))]
    2.   public class SelectionDrawer : PropertyDrawer {
    3.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
    4.       EditorGUI.BeginProperty(position, label, property);
    5.       SerializedObject serializedObject = property.serializedObject;
    6.       SerializedProperty selectionType = serializedObject.FindProperty("selectionType");
    7.       Debug.Log("selectionType=" + selectionType);
    8.       EditorGUI.EndProperty();
    9.       selectionType = property.FindPropertyRelative("selectionType");
    10.       Debug.Log("selectionType=" + selectionType);
    11.     }
    12.  
    13.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
    14.       return EditorGUIUtility.singleLineHeight * 2;
    15.     }
    16.   }
    First off, my CustomEditor (not included here) for 'UnitSystem' works. My PropertyDrawer for 'UnitComparer' works as well. Even my PropertyDrawer for 'Selector' works. Tthe issue start in 'SelectionDrawer' when I'm trying to get the property for "selectionType" - both serializedObject.FindProperty("selectionType") and property.FindPropertyRelative("selectionType") returns null. The reason for this appears to be due to that 'SelectionType' is an abstract class and generic at that. However, 'Comparer' is also abstract, but serialization for 'UnitComparer' works fine. 'Selector' is generic, but serialization still works fine as well.

    I have some stupid work arounds to get it to work, but I'm afraid I may run into similar problems sooner or later anyway, so I hope to get a proper fix for this - what can I do to be able to serialize and display 'Selector' and it's abstract generic "selectionType" property?
     
  2. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,913
    A non-elegant thing which will work for sure is hand-made serializing. Everyone gets a read() and write() function, written by you (f.writeLine(name+"/"+code1+"/"... and string[] W=f.readLine().split('/'); name=W[0]; ... . As the first line they get a unique class code. Someone else reads that code, creates the appropriate class object and calls c.read(). It's a bit of a pain, but doesn't take all that long, and it doesn't risk being completely invalidated by any new classes the way a more clever solution might be.
     
  3. haxic

    haxic

    Joined:
    Jul 12, 2021
    Posts:
    20
    Quickly made diagram ('UnitSystem' should point at the 'Comparer', not directly at 'UnitComparer'):
    upload_2023-5-21_23-0-59.png
     
  4. haxic

    haxic

    Joined:
    Jul 12, 2021
    Posts:
    20
    That is indeed not elegant :D But it may be a last resort if I can't find a more elegant way. The issue is also that, I have deeper nested classes, that I fear will also fall victim to the serialization mess.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    I think the first issue is that you're expecting polymorphism where Unity's default serialisation doesn't support that. You can use SerializeReference, which does support polymorphism, but doesn't support serialising fields with a generic parameter pre Unity 2023, and always needs a concrete type without a generic parameter (it can inherit from a type with a generic parameter though).

    The alternative is to use the Odin serialiser, which can support serialising all this.
     
    Last edited: May 22, 2023
    Bunny83 and Ryiah like this.
  6. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,082
    If you want an elegant solution (personally I wouldn't call any of that mess you've written elegant but to each their own) like @spiney199 mentioned just install Odin Serializer.

    https://github.com/TeamSirenix/odin-serializer
     
    spiney199 likes this.