Search Unity

Setting a SerializedProperty to a struct

Discussion in 'Scripting' started by Afropenguinn, Mar 14, 2018.

  1. Afropenguinn

    Afropenguinn

    Joined:
    May 15, 2013
    Posts:
    305
    I have the SerializedProperty "prop" that's an array of structs. I'm trying to set the element at "insertIndex" to the struct "newPlatform". It's for an editor window GUI.
    Code (CSharp):
    1. prop.GetArrayElementAtIndex(insertIndex).objectReferenceValue = newPlatform;
    The problem is that, as far as I can tell, the SerializedProperty will only take a UnityEngine.Object, and newPlatform is just a plain old struct. How would I store my struct? It must be possible somehow, because it can display the ones already stored in it.
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,195
    Did you make the struct [Serializable]? I'm not sure whether that matters or not in this case...
     
  3. Afropenguinn

    Afropenguinn

    Joined:
    May 15, 2013
    Posts:
    305
    I did. It really is puzzling. I might have to do some Debug.Logs to find out what it's storing the data already in there as.
     
    soonL likes this.
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    SerializedObject is a garbage interface, so things like "setting the value of an array element" isn't possible out of the box.

    If you know the fields of the struct, it's doable, although the code's a pain. Essentially, you have to copy over fields one-by-one. Here's an example:
    Code (csharp):
    1. var valueToSet = new TestStruct {
    2.     i = 15,
    3.     s = "hello",
    4.     mb = this,
    5.     f = 13.4556f
    6. };
    7.  
    8. //assume prop is an array of TestStruct
    9. var element = prop.GetArrayElementAtIndex(insertIndex);
    10. element.FindPropertyRelative("i").intValue = valueToSet.i;
    11. element.FindPropertyRelative("s").stringValue = valueToSet.s;
    12. element.FindPropertyRelative("mb").objectReferenceValue = valueToSet.mb;
    13. element.FindPropertyRelative("f").floatValue = valueToSet.f;
    14.  
    15. //Like look at the above garbage what the hell.
    Now, if you want to have this work in general - ie. make a method for overwriting a property with a value, you have to do a bunch of really, really cumbersome property iteration and reflection and other garbage.
     
    WidmerNoel, ModLunar and hadynlander like this.
  5. Afropenguinn

    Afropenguinn

    Joined:
    May 15, 2013
    Posts:
    305
    Yeah, I ended up doing the first one. It was only 5 fields, so it wasn't too much of a pain, just kind of weird that it can't do it out of the box.
     
  6. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Serializable Structs are treated as being SerializedPropertyType.Generic so the Editor is already capable of iterating the property and its child fields in the inspector, and the function is accessible to us too.

    You could just use PropertyField(includeChildren=true) instead and have the UnityEditor do all that garbage for you.
     
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    OP wanted to set the value, not create a drawer for the value.

    That would be usefull for eg. if you're creating a new array element, and want to provide default values.
     
  8. Afropenguinn

    Afropenguinn

    Joined:
    May 15, 2013
    Posts:
    305
    Yep, I already have them displaying just fine. I was inserting an element into an alphabetically sorted list. As mentioned, I ended up just setting each field manually. Kind of annoying, but not the worst thing in the world.
     
  9. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    If you are writing in an Editor class you can simply set the value via the target field, just know if you want Undo you'll have to record it manually.

    Of course this isn't really possible if you are writing in a PropertyDrawer, or if the Editor class doesn't directly target the class with the field you want to change...so how about this Extension Method?
    Code (CSharp):
    1. public static void SetValueDirect(this SerializedProperty property, object value)
    2. {
    3.     if (property == null)
    4.         throw new System.NullReferenceException("SerializedProperty is null");
    5.  
    6.     object obj = property.serializedObject.targetObject;
    7.     string propertyPath = property.propertyPath;
    8.     var flag = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
    9.     var paths = propertyPath.Split('.');
    10.     FieldInfo field = null;
    11.  
    12.     for (int i = 0; i < paths.Length; i++)
    13.     {
    14.         var path = paths[i];
    15.         if (obj == null)
    16.             throw new System.NullReferenceException("Can't set a value on a null instance");
    17.              
    18.         var type = obj.GetType();
    19.         if (path == "Array")
    20.         {
    21.             path = paths[++i];
    22.             var iter = (obj as System.Collections.IEnumerable);
    23.             if (iter == null)
    24.                 //Property path thinks this property was an enumerable, but isn't. property path can't be parsed
    25.                 throw new System.ArgumentException("SerializedProperty.PropertyPath [" + propertyPath + "] thinks that [" + paths[i-2] + "] is Enumerable.");
    26.                      
    27.             var sind = path.Split('[', ']');
    28.             int index = -1;
    29.                  
    30.             if (sind == null || sind.Length < 2)
    31.                 // the array string index is malformed. the property path can't be parsed
    32.                 throw new System.FormatException("PropertyPath [" + propertyPath + "] is malformed");
    33.  
    34.             if (!Int32.TryParse(sind[1], out index))
    35.                 //the array string index in the property path couldn't be parsed,
    36.                 throw new System.FormatException("PropertyPath [" + propertyPath + "] is malformed");
    37.  
    38.             obj = iter.ElementAtOrDefault(index);
    39.             continue;
    40.         }
    41.  
    42.         field = type.GetField(path, flag);
    43.         if (field == null)
    44.             //field wasn't found
    45.             throw new System.MissingFieldException("The field ["+path+"] in ["+propertyPath+"] could not be found");
    46.  
    47.         if(i< paths.Length-1)
    48.             obj = field.GetValue(obj);
    49.  
    50.     }
    51.  
    52.     var valueType = value.GetType();
    53.     if (!valueType.Is(field.FieldType))
    54.         // can't set value into field, types are incompatible
    55.         throw new System.InvalidCastException("Cannot cast ["+ valueType + "] into Field type ["+ field.FieldType + "]");
    56.  
    57.     field.SetValue(obj, value);
    58. }
    Don't worry so much about all the exceptions you see that it can throw, most of them you should never see thrown (unless the UnityTeam publishes a glaring mistake with SerializeProperty in a later version...which is very unlikely).

    Just focus on making sure the passed SerializedProperty is not null and that the value you are setting with can fit in the field (and avoid polymorphism if the value is not a UnityObject, it would set ok but it'll likely break during serializing).

    Also, it uses the following other custom extension methods

    Code (CSharp):
    1. public static System.Object ElementAtOrDefault(this System.Collections.IEnumerable collection, int index)
    2. {
    3.     var enumerator = collection.GetEnumerator();
    4.     int j =0;
    5.     for(; enumerator.MoveNext(); j++)
    6.     {
    7.         if(j == index) break;
    8.     }
    9.  
    10.     System.Object element = (j == index)
    11.         ?enumerator.Current
    12.         :default(System.Object);
    13.  
    14.     var disposable = enumerator as System.IDisposable;
    15.     if(disposable!= null) disposable.Dispose();
    16.  
    17.     return element;
    18. }
    Code (CSharp):
    1. public static bool Is(this Type type, Type baseType)
    2. {
    3.     if(type == null) return false;
    4.     if(baseType == null) return false;
    5.  
    6.     return baseType.IsAssignableFrom(type);
    7. }
    8.  
    9. public static bool Is<T>(this Type type)
    10. {
    11.     if(type == null) return false;
    12.     Type baseType = typeof(T);
    13.  
    14.     return baseType.IsAssignableFrom(type);
    15. }
     
    ModLunar, Afropenguinn and Baste like this.
  10. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    ;)
     
  11. WidmerNoel

    WidmerNoel

    Joined:
    Jun 3, 2014
    Posts:
    66
    Instead of specifying the relative field names using string literals you could also use compiler generated literals with
    nameof()
    . At least it won't break that easly this way:
    element.FindPropertyRelative(nameof(TestStruct.mb))
     
    darbotron likes this.
  12. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    Yeah, as long as they're public.

    I'd love to be able to nameof(Foo.bar) when bar is a private variable in Foo, both for this and for reflection purposes, but for some reason C# doesn't allow that.
     
  13. WidmerNoel

    WidmerNoel

    Joined:
    Jun 3, 2014
    Posts:
    66
    I had the same issue. Now I just expose the private field's name by a string property which is using the
    nameof()
    behind the scenes. It is a bit annoying but it does what I intend it to do.

    Code (CSharp):
    1. private int _privateInt;
    2. public string PrivateIntSerializedFieldName => nameof(_privateInt);
     
    darbotron likes this.