Search Unity

Serialize C# Properties (how-to with code)

Discussion in 'Scripting' started by arkano22, Nov 24, 2017.

  1. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    Hi all!

    There's one particular C# feature I use a lot in my projects, and that is properties: They allow you to execute arbitrary code whenever a variable is read/written. Which is nice and leads to quite clean code.

    However, Unity does not serialize properties by default. Which makes sense, since calling arbitrary code when serializing/deserializing objects probably is not a good idea.

    Calling the getter/setter in editor automatically when tinkering with the inspector without the need to write a custom Editor has proven extremely useful though. I made my own implementation using PropertyDrawers. Here it is, feel free to copy and paste to your project:

    Code (CSharp):
    1. using System;
    2. using System.Reflection;
    3. using UnityEngine;
    4. #if UNITY_EDITOR
    5. using UnityEditor;
    6. #endif
    7.  
    8.      [System.AttributeUsage(System.AttributeTargets.Field)]
    9.     public class SerializeProperty : PropertyAttribute
    10.     {
    11.         public string PropertyName { get; private set; }
    12.  
    13.         public SerializeProperty(string propertyName)
    14.         {
    15.             PropertyName = propertyName;
    16.         }
    17.     }
    18.  
    19.     #if UNITY_EDITOR
    20.     [CustomPropertyDrawer(typeof(SerializeProperty))]
    21.     public class SerializePropertyAttributeDrawer : PropertyDrawer
    22.     {
    23.         private PropertyInfo propertyFieldInfo = null;
    24.  
    25.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    26.         {
    27.             UnityEngine.Object target = property.serializedObject.targetObject;
    28.  
    29.             // Find the property field using reflection, in order to get access to its getter/setter.
    30.             if (propertyFieldInfo == null)
    31.                 propertyFieldInfo = target.GetType().GetProperty(((SerializeProperty)attribute).PropertyName,
    32.                                                      BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    33.  
    34.             if (propertyFieldInfo != null){
    35.  
    36.                   // Retrieve the value using the property getter:
    37.                 object value = propertyFieldInfo.GetValue(target,null);
    38.  
    39.                 // Draw the property, checking for changes:
    40.                 EditorGUI.BeginChangeCheck();
    41.                 value = DrawProperty(position,property.propertyType,propertyFieldInfo.PropertyType,value,label);
    42.      
    43.                 // If any changes were detected, call the property setter:
    44.                 if (EditorGUI.EndChangeCheck() && propertyFieldInfo != null){
    45.      
    46.                     // Record object state for undo:
    47.                     Undo.RecordObject(target, "Inspector");
    48.      
    49.                     // Call property setter:
    50.                     propertyFieldInfo.SetValue(target,value,null);
    51.                 }
    52.  
    53.             }else{
    54.                 EditorGUI.LabelField(position,"Error: could not retrieve property.");
    55.             }
    56.         }
    57.  
    58.         private object DrawProperty(Rect position, SerializedPropertyType propertyType, Type type, object value, GUIContent label)
    59.         {
    60.             switch (propertyType) {
    61.                 case SerializedPropertyType.Integer:
    62.                     return EditorGUI.IntField(position,label,(int)value);
    63.                 case SerializedPropertyType.Boolean:
    64.                     return EditorGUI.Toggle(position,label,(bool)value);
    65.                 case SerializedPropertyType.Float:
    66.                     return EditorGUI.FloatField(position,label,(float)value);
    67.                 case SerializedPropertyType.String:
    68.                     return EditorGUI.TextField(position,label,(string)value);
    69.                 case SerializedPropertyType.Color:
    70.                     return EditorGUI.ColorField(position,label,(Color)value);
    71.                 case SerializedPropertyType.ObjectReference:
    72.                     return EditorGUI.ObjectField(position,label,(UnityEngine.Object)value,type,true);
    73.                 case SerializedPropertyType.ExposedReference:
    74.                     return EditorGUI.ObjectField(position,label,(UnityEngine.Object)value,type,true);
    75.                 case SerializedPropertyType.LayerMask:
    76.                     return EditorGUI.LayerField(position,label,(int)value);
    77.                 case SerializedPropertyType.Enum:
    78.                     return EditorGUI.EnumPopup(position,label,(Enum)value);
    79.                 case SerializedPropertyType.Vector2:
    80.                     return EditorGUI.Vector2Field(position,label,(Vector2)value);
    81.                 case SerializedPropertyType.Vector3:
    82.                     return EditorGUI.Vector3Field(position,label,(Vector3)value);
    83.                 case SerializedPropertyType.Vector4:
    84.                     return EditorGUI.Vector4Field(position,label,(Vector4)value);
    85.                 case SerializedPropertyType.Rect:
    86.                     return EditorGUI.RectField(position,label,(Rect)value);
    87.                 case SerializedPropertyType.AnimationCurve:
    88.                     return EditorGUI.CurveField(position,label,(AnimationCurve)value);
    89.                 case SerializedPropertyType.Bounds:
    90.                     return EditorGUI.BoundsField(position,label,(Bounds)value);
    91.                 default:
    92.                     throw new NotImplementedException("Unimplemented propertyType "+propertyType+".");
    93.             }
    94.         }
    95.  
    96.     }
    97.     #endif
    98.  
    Usage:

    Code (CSharp):
    1.  
    2. class PropertySerializationTest : MonoBehaviour{
    3.  
    4.         [SerializeProperty("Test")] // pass the name of the property as a parameter
    5.         public float test;
    6.  
    7.         public float Test{
    8.             get{
    9.                 Debug.Log("Getter called");
    10.                 return test;
    11.             }
    12.             set{
    13.                 Debug.Log("Setter called");
    14.                 test = value;
    15.             }
    16.         }
    17. }
     
    Last edited: Nov 24, 2017
    halley, chaikadev, Wolfos and 16 others like this.
  2. KosmoDED

    KosmoDED

    Joined:
    May 22, 2017
    Posts:
    8
    Its working. I'm so happy about that ). Thank you @arkano22
     
  3. arutyunef

    arutyunef

    Joined:
    May 30, 2019
    Posts:
    3
    With C# version > 7.3, there is a very neat trick to expose auto-properties. All auto properties have a backing field, so all you have to do is mark that field with SerializeField like this:
    Code (CSharp):
    1. [field: SerializeField]
    2. public float Speed { get; set; }
    Note: u have to use "field: ". This syntax tells compiler that all attributes in that block refer to backing field.
     
  4. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    Neat indeed! However, functionally speaking this is basically the same as having a public field, am I right? No custom set/get code called during serialization...
     
    VitruvianStickFigure likes this.
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    I agree that being able to expose properties is extremely useful!

    One common use case is exposing settings properties, and applying their effects to all affected systems whenever their value is changed.

    It also makes it trivial to expose EditorPrefs and PlayerPrefs in the inspector as if they were normal fields.

    [field: SerializeField] is pretty useful, as it can be used to serialize properties and expose them in the inspector with minimal boilerplate code.

    The main benefit it offers over using normal fields is that you can restrict the accessibility of the set accessor.

    Code (CSharp):
    1.  
    2. // You can look, but you can't touch!
    3. [field: SerializeField]
    4. public int Value
    5. {
    6.     get;
    7.     private set;
    8. }
    So functionally it is very similar to having this:

    Code (CSharp):
    1. [SerializeField]
    2. private int _value;
    3.  
    4. // You can look, but you can't touch!
    5. public int Value
    6. {
    7.     get
    8.     {
    9.         return _value;
    10.     }
    11.  
    12.     private set
    13.     {
    14.         _value = value;
    15.     }
    16. }
    Unfortunately the property backing field will have a very ugly label in the inspector by default, which is something you'll need to fix before it really becomes a usable solution.

    This is how it looks in the inspector by default (the one on the left):

    serialized-property.png
     
    Rallix, Bunny83 and 1000Nettles like this.
  6. old_pilgrim

    old_pilgrim

    Joined:
    Nov 15, 2019
    Posts:
    7
    Any idea how to get this to work with a custom struct?
     
  7. Deleted User

    Deleted User

    Guest

    upload_2020-5-30_10-27-32.png

    Found that it's not working like this, because
    can't found property, any suggestion how to fix?
     
  8. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    row and column aren’t properties, nor backing fields of a property. They’re just public variables, you don’t need to do anything special for them to be serialized.

    This method is intended to deal with properties.
     
    Ziplock9000 likes this.
  9. Hypertectonic

    Hypertectonic

    Joined:
    Dec 16, 2016
    Posts:
    75
    Interesting. @arkano22 does this need the backing field to be set to public? Doesn't this defeat part of the point of using properties, which is keeping our fields private and controlling their access?

    EDIT: Nevermind, I figured out I can just have my field private and add the [SerializeField] attribute together with the [SerializeProperty()] one.

    Great stuff!
     
    Last edited: Jul 8, 2020
  10. reinfeldx

    reinfeldx

    Joined:
    Nov 23, 2013
    Posts:
    164
    @arkano22 do the SerializeProperty and SerializePropertyAttributeDrawer classes need to be in the same script as the class in which you'd like to use SerializeProperty()?

    I created a script with your first code block, then I created a second script following your PropertySerializationTest. The problem is I can't use [SerializeProperty()] in that second script; I'm getting a "missing type or namespace" error for SerializePropertyAttribute and SerializeProperty.

    I feel like I'm missing something basic here, but I'm at the edge of my programming knowledge with this one.
     
    DragonCoder likes this.
  11. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    @reinfeldx Scripts placed inside a folder named Editor are not visible to other scripts, that is probably the source of your issues.

    Good practice would be to split the code into two separate files: one containing just the SerializeProperty class and another file one containing just the SerializePropertyAttributeDrawer class. The SerializePropertyAttributeDrawer.cs file should then be placed inside an Editor folder while the SerializeProperty.cs file should not be inside an Editor folder. This is because the drawer is only needed in the editor while the attribute might be used within classes that exist in builds too.

    There's an example in the manual under Customize the GUI of script members using Property Attributes.
     
    reinfeldx likes this.
  12. anisimovdev

    anisimovdev

    Joined:
    Mar 4, 2013
    Posts:
    22
    In Unity 2020 you can use
    Code (CSharp):
    1. [field: SerializeField]


     
  13. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    I guess they fixed the backing-field name issue above?
    When did they do that?
     
  14. IC_

    IC_

    Joined:
    Jan 27, 2016
    Posts:
    64
    Why does
    private set
    is required? Resharper says it is redundant but unity won't parse it without that (However, in versions before 2020 it worked). I had to switch that inspection off, pretty silly
     
    Last edited: Sep 28, 2020
  15. Malkalypse

    Malkalypse

    Joined:
    Jan 13, 2017
    Posts:
    14
    Is there a way to use your SerializeProperty class with lists or arrays?
     
  16. CommandDan

    CommandDan

    Joined:
    Feb 23, 2019
    Posts:
    3
    Thanks very much. Super useful!

    Edit: @arkano22 I can't get it working with int :/ I might be doing something wrong though.

    Code (CSharp):
    1. [SerializeProperty("Min")] public int minimum;
    2.        
    3.         public int Min
    4.         {
    5.             get => _min;
    6.             set => Set(value, _max);
    7.         }
    it just returns the error of not being able to retrieve. I am using Unity 2019.4.14f1
     
    Last edited: Jan 19, 2021
  17. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Bunny83 likes this.
  18. p3k07

    p3k07

    Joined:
    Mar 30, 2014
    Posts:
    10
    I had a similar issue. I had a scriptable object that was referencing properties from a namespaced script.
    I thought it was the fact the properties where not in classes and instead just in namespaces, that that was the issue.
    As others have said, it turned out to be just simply adding [field: SerializeField] to each property did the trick.
    In the inspector, the label will look ugly, but just make a custom editor for that.

    Also note that if your scriptable object is stored as a .asset file, don't forget to set the object as dirty and save the asset database.
     
    Last edited: Mar 18, 2021
  19. TheVirtualMunk

    TheVirtualMunk

    Joined:
    Sep 6, 2019
    Posts:
    150
    Has anyone found a way to use this together with validation code? Can't seem to get this working when "expanding" the getter/setter methods.

    For instance, I've tried to make a small class that makes sure an AudioClip is ambisonic (as that sometimes takes us by surprise during importing);
    Code (CSharp):
    1. [Serializable]
    2. public class AmbisonicAudioClip : ISerializationCallbackReceiver
    3. {
    4.     [SerializeField]
    5.     private AudioClip clip;
    6.  
    7.     public AudioClip Clip
    8.     {
    9.         get => clip;
    10.         set
    11.         {
    12.             clip = value;
    13.             ValidateClip();
    14.         }
    15.     }
    16.  
    17.     public void OnAfterDeserialize()
    18.     {
    19.        
    20.     }
    21.  
    22.     public void OnBeforeSerialize()
    23.     {
    24.         ValidateClip();
    25.     }
    26.  
    27.     private void ValidateClip()
    28.     {
    29.         if(clip)
    30.             if (!Clip.ambisonic)
    31.                 Debug.LogError("Clip " + clip.name + " is not ambisonic!");
    32.     }
    33. }
    This works fine but looks bad in the editor, and I can't serialize the public property...
     
    VitruvianStickFigure likes this.
  20. Thiem

    Thiem

    Joined:
    Jun 30, 2014
    Posts:
    20
    I just autocompleted my way to this thing here:

    [field: SerializeField] public int CurrentVoxelCount { get; private set; }

    That is exposed in the inspector.
     
  21. mitchmunro

    mitchmunro

    Joined:
    Jun 20, 2019
    Posts:
    15
    This is cool, thanks @arkano22 !

    I got it working on a regular script - very nice indeed.

    I tried to add it to a [serializable] class that is being displayed as part of a larger class in the inspector, but it didn't like that.
    Looks like this line in the code is getting confused, since the the target here is returning the larger class, rather than the initial serialized class with the property on it.

    Code (CSharp):
    1. UnityEngine.Object target = property.serializedObject.targetObject;
    If anyone has thought about how to fix this and come up with a solution I would love to hear it! I might do some thinking on it later.
     
  22. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    This only works for auto properties, as all it does is serialize their backing field (handy when you want to have a read-only property, though). You can't insert arbitrary code in the getter/setter, which is what the solution presented in this thread is intended for and the whole point of using properties.
     
    Last edited: Sep 14, 2021
    Eristen likes this.
  23. AntonioOne

    AntonioOne

    Joined:
    Jan 25, 2021
    Posts:
    2
    Is there a way to do this without creating a field? I wanna add a property that does not have a backing field. With this method, I can add a fake field for this property to show in the inspector. Also, the field has to be public, which is another problem.
     
  24. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    What do you mean you don't want a backing field? all properties have a backing field, even auto-properties (though in their case, the compiler automatically creates the field under the hood).

    Properties are just syntactic sugar for getters/setters. You need to store their value somewhere, it's just not possible to have a property with no backing field. If you meant an auto property, you can use [field:SerializeField] as pointed out above.
     
    Last edited: Oct 5, 2021
    KReguiegReply likes this.
  25. AntonioOne

    AntonioOne

    Joined:
    Jan 25, 2021
    Posts:
    2
    Auto-properties are just a subtype of properties. Some properties can be just accessors to something.
    upload_2021-10-5_17-30-9.png
    The example is not ideal.
    Does anyone know if there is a way to make Unity serialize properties?
     
  26. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    Oh, I see. You can only serialize data. A property that basically acts as a getter/setter for your own data, still has a “backing field” of sorts, even if it’s more complex than a single string/integer/etc. So you either serialize that data, or all you have is a couple methods. In your example, this data is a reference to the TextMeshPro component.

    So you want a property that shows up in the inspector, isn’t serialized (saved), and all you need is the getter/setter to be called when its data is changed in the inspector via UI. Correct? In that case you’ll need to use a custom editor I think, and manually set/get your property there using editor fields.
     
    Last edited: Oct 5, 2021
  27. unity_RJs1M1mPBz0Pxg

    unity_RJs1M1mPBz0Pxg

    Joined:
    Apr 29, 2021
    Posts:
    2
    this is pretty cool, One problem though : doesnt work with the undo function in the editor.
     
  28. unity_RJs1M1mPBz0Pxg

    unity_RJs1M1mPBz0Pxg

    Joined:
    Apr 29, 2021
    Posts:
    2
    what I mean is that this does not call the setter on Undo/Redo.
     
  29. Northmaen

    Northmaen

    Joined:
    Jun 23, 2015
    Posts:
    17
    Now that Unity officially support most of the C# 9 features like records, does someone knows how to serialize properties with a init only setter?

    The trick with [field: SerializeField] does not seem to work; it doesn't work on properties with no setter or a implicit private setter either, but works if the setter is explicitly flagged private.
     
  30. VitruvianStickFigure

    VitruvianStickFigure

    Joined:
    Jun 28, 2017
    Posts:
    38
    This is real progress, but most of my usage of properties is to call validating and housekeeping code related to the change in the value.
    [field: SerializeField]
    doesn't support this.

    As an example, I'm working with a GameObject right now that has multiple layers, with procedural properties to their scale and rendering, controlled by the parent; it would be easy, from a coding perspective, to monitor the number-of-layers property for changes and bolster or trim the child objects automatically; but the moment I add anything other than autoproperty code, the inspector stops showing the property.

    That basically means I've got to disregard the entire inspector to do it like this, even though it keeps my code much cleaner (and depending on the chosen workaround, more efficient, too).

    So, I would politely argue that the feature is ultimately half-finished.
     
    Northmaen likes this.
  31. OnatKorucu

    OnatKorucu

    Joined:
    Apr 18, 2018
    Posts:
    8
    I noticed that following code does not work in 2021.3.12

    Code (CSharp):
    1. #if UNITY_EDITOR
    2.  
    3. [CustomEditor(typeof(SceneGroupList))]
    4. public class SceneGroupListEditor : Editor
    5. {
    6.     private SceneGroupList _sceneGroupList;
    7.     void OnEnable()
    8.     {
    9.         _sceneGroupList = target as SceneGroupList;
    10.     }
    11.  
    12. [System.Serializable]
    13. public class SceneGroup
    14. {
    15.     [field: SerializeField] public uint Idx { get; set; }
    16. }
    17.  
    18. public override void OnInspectorGUI()
    19. {
    20.     serializedObject.Update();
    21.     for (var i = 0; i < len; i++)
    22.     {
    23.         var arrayElement = property.GetArrayElementAtIndex(i);
    24.         DrawSceneGroup(arrayElement, _sceneGroupList.SceneGroups[i]);
    25.     }
    26. }
    27.  
    28. private void DrawSceneGroup(SerializedProperty arrayElement, SceneGroup sceneGroup)
    29. {
    30.     EditorGUILayout.Space();
    31.     EditorGUILayout.BeginVertical(GUI.skin.box);
    32.  
    33.     EditorGUILayout.PropertyField(arrayElement.FindPropertyRelative("Idx"));
    34.  
    35.     EditorGUILayout.EndVertical();
    36. }
    37.  
    38. }
    39.  
    40. #endif
    But changing "[field: SerializeField] public uint Idx { get; set; }" line into

    Code (CSharp):
    1. public uint Idx;
    works.
     
  32. Tom_Olsen

    Tom_Olsen

    Joined:
    Oct 11, 2020
    Posts:
    10
    Wow, thanks for this solution.
    However, when trying to use it combined with the [Range(min,max)] attribute it doesn't work anymore.

    Code (CSharp):
    1.  
    2.         [Range(0, 3), SerializeField, SerializeProperty("Channel")]
    3.         private int m_channel = 0;
    4.         public int Channel
    5.         {
    6.             set
    7.             {
    8.                 if (m_channel != value)
    9.                 {
    10.                     m_channel = value;
    11.                     m_propertyBlock.SetInt("_Channel", m_channel);
    12.                 }
    13.             }
    14.             get { return m_channel; }
    15.         }
    The above example shows the range attribute in the inspector, but the setter isn't called anymore when changing the "Channel" value. When moving the Range attribute to the end "[SerializeField, SerializeProperty("Channel"),Range(0, 3)]", the setter still gets called, but the slider does not show up in the inspector.

    Any idea how to fix this?



     
  33. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    You can't combine multiple property drawers on a single member variable. One attribute will take precedence and the others will be ignored, otherwise you'd be drawing the same property multiple times.

    As with pretty anything in Unity, you can work around this limitation by writing your own drawer system. See the following thread for more info on an approach that composes multiple property drawers: https://forum.unity.com/threads/drawing-a-field-using-multiple-property-drawers.479377/
     
    SisusCo likes this.
  34. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,445
    Hey, just came across this old thread today, and it solved a problem for me. Rather than just hitting Like and moving on, I thought I would share my example usage.



    Code (CSharp):
    1.         // Let the user choose an enum value by name in Inspector.
    2.         [Tooltip("This enum corresponds with highlight color values below.")]
    3.         public InterestType highlightLegend = InterestType.Interactive;
    4.  
    5.         // Let the user see/set the corresponding color in the list of colors.
    6.         [SerializeProperty("HighlightSample")] public Color highlightSample;
    7.         public Color HighlightSample
    8.         {
    9.             get
    10.             {
    11.                 if ((int)highlightLegend < 0 ||
    12.                     (int)highlightLegend >= highlightColors.Length)
    13.                         return Color.clear;
    14.                 return highlightColors[(int)highlightLegend];
    15.             }
    16.             set
    17.             {
    18.                 if ((int)highlightLegend < 0 ||
    19.                     (int)highlightLegend >= highlightColors.Length)
    20.                         return;
    21.                 highlightColors[(int)highlightLegend] = value;
    22.             }
    23.         }
    24.  
    25.         public enum InterestType
    26.         {
    27.             Impossible=0,
    28.             Interactive=1, Friendly=2, Neutral=3, Enemy=4, Vehicle=5,
    29.         };
    30.  
    31.         // Actual list of colors, so you can see/compare/set them at once.
    32.         public Color[] highlightColors;
    33.  
    The alternative would have been to make an array of serializable structs, each with a useless name/enumvalue and a color, or a canned tooltip to explain the built-in purpose of each element in the color list.
     
  35. DmytroSerebrennikov

    DmytroSerebrennikov

    Joined:
    May 28, 2022
    Posts:
    16
    with generics (T) it does not work(