Search Unity

Feedback SerializeReference & primitive types

Discussion in 'Scripting' started by OndrejP, Sep 11, 2020.

  1. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    I've just tested [SerializeReference] and it's pretty awesome, you can do a lot of crazy things with it.

    I use it for storing dynamic parameters in components / assets.

    Example:
    Code (CSharp):
    1. public class SomeComponent
    2. {    
    3.     [Serializable]
    4.     public struct Parameter
    5.     {
    6.         public string Id;
    7.  
    8.         [SerializeReference]
    9.         public object Value;
    10.  
    11.         public Object Reference;
    12.     }
    13.  
    14.     [SerializeField]
    15.     Parameter[] m_parameters;
    16. }
    ...this way I can store and push parameters of any complexity into other systems.
    There's usually custom inspector. It doesn't have to do much, just decide what type should be stored in "Value" and put there default instance, editing works well with default inspector.

    This works well, even on structs (they get boxed ofc).
    However it does not work on primitive types.

    It seems that serialization system cannot store for example 'float' into managed reference.
    There's workaround (e.g. struct FloatValue { public float value; }), but that's no straightforward.

    Would it be hard to add support for storing primitive types into managed references of 'object' type?

    This is how it looks now in serialized asset, I've stored this:
    00000008: null
    00000009: 7.7f
    0000000A: new TestStruct { Name = "TestName", Value = 7.7f }

    Code (CSharp):
    1.     00000008:
    2.       type: {class: , ns: , asm: }
    3.     00000009:
    4.       type: {class: Single, ns: System, asm: mscorlib}
    5.     0000000A:
    6.       type: {class: TestStruct, ns: , asm: Assembly-CSharp}
    7.       data:
    8.         Name: TestName
    9.         Value: 7.7
    It seems to me that primitives could be stored simply like this:
    Code (CSharp):
    1.     00000009:
    2.       type: {class: Single, ns: System, asm: mscorlib}
    3.       data: 7.7
    That would be consistent with the rest of the serialization.
    What do you think?
     
  2. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    Since there's no response from Unity or anyone else, I've created a wrapper struct which allows to serialize anything, including primitives, string and UnityEngine.Object as [SerializeReference].

    It uses OnBeforeSerialize and OnAfterDeserialize. Before serialization it packs primitives, strings or UnityEngine.Object into wrapper class. Since it's serialized as reference, it cannot use OnAfterDeserialize to unpack it, because it's likely to be null. So it unpacks it on first use. OnAfterDeserialize simply resets "unpacked" object to null to signal that on next access unpacking should happen.

    I've added simple property drawer which allows comfortable editing of referenced value.
    As with normal use of [SerializeReference], user code is responsible for assigning proper value, which can be later edited with this property drawer.

    NOTE: Value field itself should not be marked as [SerializeReference]. Object inside is serialized as reference.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Runtime.CompilerServices;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7. using UnityEngine;
    8. using Object = UnityEngine.Object;
    9.  
    10. [Serializable]
    11. public struct Value : ISerializationCallbackReceiver
    12. {
    13.     interface IValue
    14.     {
    15.         object Object { get; }
    16.     }
    17.  
    18.     class Ref<T> : IValue
    19.     {
    20.         public T Value;
    21.         public object Object { get { return Value; } }
    22.     }
    23.  
    24.     class Int8Value : Ref<sbyte> { }
    25.     class Int16Value : Ref<short> { }
    26.     class Int32Value : Ref<int> { }
    27.     class Int64Value : Ref<long> { }
    28.  
    29.     class UInt8Value : Ref<byte> { }
    30.     class UInt16Value : Ref<ushort> { }
    31.     class UInt32Value : Ref<uint> { }
    32.     class UInt64Value : Ref<ulong> { }
    33.  
    34.     class FloatValue : Ref<float> { }
    35.     class DoubleValue : Ref<double> { }
    36.     class BoolValue : Ref<bool> { }
    37.     class CharValue : Ref<char> { }
    38.     class StringValue : Ref<string> { }
    39.  
    40.     class UnityObjectValue : Ref<Object> { }
    41.  
    42.     public const string SerializedPropertyName = nameof(m_serialized);
    43.     public const string InternalValueName = nameof(Ref<object>.Value);
    44.  
    45.     [SerializeReference]
    46.     private object m_serialized;
    47.  
    48.     private object m_object;
    49.  
    50.     public object Object
    51.     {
    52.         get
    53.         {
    54.             if (m_object == null && m_serialized != null)
    55.             {
    56.                 m_object = Unpack(m_serialized);
    57.                 m_serialized = null;
    58.             }
    59.             return m_object;
    60.         }
    61.         set
    62.         {
    63.             m_serialized = null;
    64.             m_object = value;
    65.         }
    66.     }
    67.  
    68.     public Value(object obj)
    69.     {
    70.         m_serialized = null;
    71.         m_object = obj;
    72.     }
    73.  
    74.     void ISerializationCallbackReceiver.OnBeforeSerialize()
    75.     {
    76.         // Pack primitive types into struct
    77.         if (m_object != null || m_serialized == null)
    78.         {
    79.             m_serialized = Pack(m_object);
    80.         }
    81.     }
    82.  
    83.     void ISerializationCallbackReceiver.OnAfterDeserialize()
    84.     {
    85.         // Reference are deserialized in unspecified order
    86.         m_object = null;
    87.     }
    88.  
    89.     public static object Pack(object obj)
    90.     {
    91.         if (obj != null)
    92.         {
    93.             if (obj is Object unityObj)
    94.             {
    95.                 return new UnityObjectValue { Value = unityObj };
    96.             }
    97.             else if (obj is string str)
    98.             {
    99.                 return new StringValue { Value = str };
    100.             }
    101.             else if (obj.GetType().IsPrimitive)
    102.             {
    103.                 if (obj is sbyte i8) return new Int8Value { Value = i8 };
    104.                 if (obj is short i16) return new Int16Value { Value = i16 };
    105.                 if (obj is int i32) return new Int32Value { Value = i32 };
    106.                 if (obj is long i64) return new Int64Value { Value = i64 };
    107.  
    108.                 if (obj is byte ui8) return new UInt8Value { Value = ui8 };
    109.                 if (obj is ushort ui16) return new UInt16Value { Value = ui16 };
    110.                 if (obj is uint ui32) return new UInt32Value { Value = ui32 };
    111.                 if (obj is ulong ui64) return new UInt64Value { Value = ui64 };
    112.  
    113.                 if (obj is float f) return new FloatValue { Value = f };
    114.                 if (obj is double d) return new DoubleValue { Value = d };
    115.                 if (obj is bool b) return new BoolValue { Value = b };
    116.                 if (obj is char c) return new CharValue { Value = c };
    117.             }
    118.         }
    119.         return obj;
    120.     }
    121.  
    122.     public static object Unpack(object obj)
    123.     {
    124.         return obj.TryCast(out IValue value) ? value.Object : obj;
    125.     }
    126. }
    127.  
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Text;
    5. using System.Threading.Tasks;
    6. using UnityEditor;
    7. using UnityEngine;
    8.  
    9. [CustomPropertyDrawer(typeof(Value))]
    10. public class ValueDrawer : PropertyDrawer
    11. {
    12.     // Name.Space Value/FloatValue
    13.     static readonly string PrefixString = typeof(Value).Assembly.GetName().Name + " " + nameof(Value) + "/";
    14.  
    15.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    16.     {
    17.         return EditorGUI.GetPropertyHeight(GetDisplayProperty(property), label, true);
    18.     }
    19.  
    20.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    21.     {
    22.         EditorGUI.PropertyField(position, GetDisplayProperty(property), label, true);
    23.     }
    24.  
    25.     static SerializedProperty GetDisplayProperty(SerializedProperty valuePropertry)
    26.     {
    27.         var managedRefProperty = valuePropertry.FindPropertyRelative(Value.SerializedPropertyName);
    28.         if (managedRefProperty.managedReferenceFullTypename.StartsWith(PrefixString))
    29.         {
    30.             // It's wrapped primitive
    31.             return managedRefProperty.FindPropertyRelative(Value.InternalValueName);
    32.         }
    33.         return managedRefProperty;
    34.     }
    35. }
    36.  
     
  3. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,311
    Oh goodie. I've hit this as well.

    Why can't unity just store the primitive type directly?
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    I mean the documentation says it doesn't support value types. It is called SerializeReference after all. It is to be used with reference types.

    Value types can be serialised normally and there's no point for them to be serialised in this manner.
     
    Kurt-Dekker likes this.
  5. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    There IS point for serializing them in this manner and the point is "generalization". Sometimes you might want to serialize data of any type. There are valid cases, but it's rare.

    I use that for storing Behavior Graph parameters. I have a component which references Behavior Graph (Scriptable Object asset). The component provides parameter values for the graph (e.g. visibility distance, NavMeshAgent reference, etc...). Parameters are primitive types, strings, Unity Objects and even custom objects (SerializeReference). The Graph asset defines the parameters.

    I could use a struct, with all the possible fields (float, int, Unity Object, string, [SerializeReference] object...), but wrapper solution is more straightforward and supports ANY type. Direct support for primitives and Unity Objects would be nice.
     
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    That's good and all but it's a type of serialisation Unity is never going to support. Reason being it's not performant. Unity's serialisation is meant to quick, SerializeReference being notably less performant than regular serialisation.

    Even alternative serialisers like Odin are always going to be slower than Unity's serialisation.

    I agree it would be nice but it's also a bit of a trap if you don't know what you're doing. I see too many people just serialising everything with alternatives like Odin and wonder their performance is shot.
     
  7. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    That's a good point, it would be a trap for a lot of people. I've seen overuse of Odin as well (without understanding the actual problem).