#if UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.Assertions; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; using Type = System.Type; using UnityEditor; namespace Jovian.Utilities.Editor { /// /// Helper class for serializing objects in the editor /// public static class EditorSerializationUtility { /// /// Returns the property type for cases when it is needed, like in the case of trying to get the type of SerializedPropertyType.ObjectReference /// Solution found on https://answers.unity.com/questions/929293/get-field-type-of-serializedproperty.html /// /// /// public static Type GetTypeFromProperty(SerializedProperty property) { //gets parent type info string[] slices = property.propertyPath.Split('.'); Type type = property.serializedObject.targetObject.GetType(); for (int i = 0; i < slices.Length; i++) { string slice = slices[i]; if (slice == "Array") { i++; //skips "data[x]" if (type.IsArray) // e.g Type[] { type = type.GetElementType(); } else if (type.IsGenericType) // e.g. List { type = type.GetGenericArguments()[0]; } else { throw new NotSupportedException("Unsupported array type. Type[] or List are only supported array types"); } } else { //gets info on field and its type type = type.GetField(slice, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance)?.FieldType; } if (type == null) { throw new NullReferenceException( $"Type is null, something is not working correctly. Path={property.propertyPath}, Slice={slice}"); } } //type is now the type of the property return type; } private static readonly Regex isArrayElementRegex = new Regex("Array.data\\[\\d+\\]$"); /// /// Checks if the property is an array element /// /// /// public static bool IsPropertyAnArrayElement(SerializedProperty property) { return isArrayElementRegex.IsMatch(property.propertyPath); } /// /// Returns the parent array property for the given property, which is a member of the array /// /// /// public static SerializedProperty GetArrayPropertyWithElementProperty(SerializedProperty property) { SerializedObject serializedObject = property.serializedObject; string propertyPath = property.propertyPath; int arrayDataIndex = propertyPath.LastIndexOf(".Array.data", StringComparison.Ordinal); string propertyPathWithoutArray = propertyPath.Substring(0, arrayDataIndex); int pathDividerIndex = propertyPathWithoutArray.LastIndexOf(".", StringComparison.Ordinal); string parentPropertyName = propertyPathWithoutArray; if (pathDividerIndex != -1) { parentPropertyName = propertyPathWithoutArray.Substring(pathDividerIndex); } return serializedObject.FindProperty(parentPropertyName); } /// /// Will save specifically the property args passed in the form new object[] { "propertyName1", "propertyName2", etc. } to the specified targetObject /// /// /// /// /// public static void SaveObjectProperties(Object targetObject, params object[] args) { SerializedObject serializedObject = new SerializedObject(targetObject); for (int i = 0; i < args.Length; i += 2) { object keyArg = args[i]; Type keyType = keyArg.GetType(); if ((keyType == typeof(string)) == false) { throw new NotSupportedException($"Key must be string. {args[i]} is {keyType}"); } SerializedProperty property = serializedObject.FindProperty((string)keyArg); object argValue = args[i + 1]; if (property == null) { throw new Exception($"No property found for key {keyArg}"); } if (argValue == null) { property.objectReferenceValue = null; } else { SetSerializedPropertyValue(property, argValue); } } serializedObject.ApplyModifiedProperties(); } /// /// Will auto-detect the value type and save it to the property. It only supports the serializable types that the Unity serialization system supports /// /// /// /// public static void SetSerializedPropertyValue(SerializedProperty fromProperty, object value) { if (fromProperty == null) { Debug.LogError("SetSerializedPropertyValue failed, property is null"); return; } // Strings are counted as arrays but should be handled separately if (fromProperty.isArray && fromProperty.propertyType != SerializedPropertyType.String) { fromProperty.arraySize = 0; var argArray = (IEnumerable)value; var enumerator = argArray.GetEnumerator(); int index = 0; while (enumerator.MoveNext()) { fromProperty.InsertArrayElementAtIndex(index); var arrayProperty = fromProperty.GetArrayElementAtIndex(index); SetSerializedPropertyValue(arrayProperty, enumerator.Current); index++; } } else { switch (fromProperty.propertyType) { case SerializedPropertyType.AnimationCurve: fromProperty.animationCurveValue = (AnimationCurve)value; break; case SerializedPropertyType.Boolean: fromProperty.boolValue = (bool)value; break; case SerializedPropertyType.BoundsInt: fromProperty.boundsIntValue = (BoundsInt)value; break; case SerializedPropertyType.Character: fromProperty.intValue = (int)(char)value; break; case SerializedPropertyType.Color: { if (value is Color32 color32) { fromProperty.colorValue = (Color)color32; } else { fromProperty.colorValue = (Color)value; } break; } case SerializedPropertyType.ExposedReference: case SerializedPropertyType.ObjectReference: fromProperty.objectReferenceValue = (Object)value; break; case SerializedPropertyType.Float: fromProperty.floatValue = (float)value; break; case SerializedPropertyType.Integer: fromProperty.intValue = (int)value; break; case SerializedPropertyType.LayerMask: fromProperty.intValue = ((LayerMask)value).value; break; case SerializedPropertyType.Quaternion: fromProperty.quaternionValue = (Quaternion)value; break; case SerializedPropertyType.Rect: fromProperty.rectValue = (Rect)value; break; case SerializedPropertyType.RectInt: fromProperty.rectIntValue = (RectInt)value; break; case SerializedPropertyType.String: fromProperty.stringValue = (string)value; break; case SerializedPropertyType.Vector2: fromProperty.vector2Value = (Vector2)value; break; case SerializedPropertyType.Vector2Int: fromProperty.vector2IntValue = (Vector2Int)value; break; case SerializedPropertyType.Vector3: fromProperty.vector3Value = (Vector3)value; break; case SerializedPropertyType.Vector3Int: fromProperty.vector3IntValue = (Vector3Int)value; break; case SerializedPropertyType.Vector4: fromProperty.vector4Value = (Vector4)value; break; case SerializedPropertyType.Enum: fromProperty.enumValueIndex = Array.IndexOf(Enum.GetValues(value.GetType()), value); // flags??? break; case SerializedPropertyType.Generic: SaveGenericProperty(fromProperty, value); break; default: throw new NotSupportedException($"PropertyType {fromProperty.propertyType} is not supported - yet. Array? {fromProperty.isArray} Path: {fromProperty.propertyPath}"); } } } /// /// Will copy fromProperty to toProperty. It only supports the serializable types that the Unity serialization system supports /// /// /// /// public static void CopyPropertyValue(SerializedProperty fromProperty, SerializedProperty toProperty) { Assert.IsNotNull(fromProperty, "fromProperty == null"); Assert.IsNotNull(toProperty, "toProperty == null"); Assert.AreEqual(fromProperty.propertyType, toProperty.propertyType, $"Properties do not match types. fromType={fromProperty.propertyType}, toType={toProperty.propertyType}"); switch (fromProperty.propertyType) { case SerializedPropertyType.AnimationCurve: toProperty.animationCurveValue = fromProperty.animationCurveValue; break; case SerializedPropertyType.Boolean: toProperty.boolValue = fromProperty.boolValue; break; case SerializedPropertyType.BoundsInt: toProperty.boundsIntValue = fromProperty.boundsIntValue; break; case SerializedPropertyType.Character: toProperty.intValue = fromProperty.intValue; break; case SerializedPropertyType.Color: { toProperty.colorValue = fromProperty.colorValue; break; } case SerializedPropertyType.ExposedReference: case SerializedPropertyType.ObjectReference: toProperty.objectReferenceValue = fromProperty.objectReferenceValue; break; case SerializedPropertyType.Float: toProperty.floatValue = fromProperty.floatValue; break; case SerializedPropertyType.Integer: toProperty.intValue = fromProperty.intValue; break; case SerializedPropertyType.LayerMask: toProperty.intValue = fromProperty.intValue; break; case SerializedPropertyType.Quaternion: toProperty.quaternionValue = fromProperty.quaternionValue; break; case SerializedPropertyType.Rect: toProperty.rectValue = fromProperty.rectValue; break; case SerializedPropertyType.RectInt: toProperty.rectIntValue = fromProperty.rectIntValue; break; case SerializedPropertyType.String: toProperty.stringValue = fromProperty.stringValue; break; case SerializedPropertyType.Vector2: toProperty.vector2Value = fromProperty.vector2Value; break; case SerializedPropertyType.Vector2Int: toProperty.vector2IntValue = fromProperty.vector2IntValue; break; case SerializedPropertyType.Vector3: toProperty.vector3Value = fromProperty.vector3Value; break; case SerializedPropertyType.Vector3Int: toProperty.vector3IntValue = fromProperty.vector3IntValue; break; case SerializedPropertyType.Vector4: toProperty.vector4Value = fromProperty.vector4Value; break; case SerializedPropertyType.Enum: toProperty.intValue = fromProperty.intValue; break; case SerializedPropertyType.Generic: CopyGenericProperty(fromProperty, toProperty); break; case SerializedPropertyType.ManagedReference: toProperty.managedReferenceValue = Activator.CreateInstance(fromProperty.managedReferenceValue.GetType()); CopyGenericProperty(fromProperty, toProperty); break; default: throw new NotSupportedException($"PropertyType {fromProperty.propertyType} is not supported - yet. Array? {fromProperty.isArray} Path: {fromProperty.propertyPath}"); } } private static void CopyGenericProperty(SerializedProperty fromProperty, SerializedProperty toProperty) { IEnumerator fromPropertyEnumerator = fromProperty.GetEnumerator(); IEnumerator toPropertyEnumerator = toProperty.GetEnumerator(); while (toPropertyEnumerator.MoveNext() && fromPropertyEnumerator.MoveNext()) { if (toPropertyEnumerator.Current is SerializedProperty toChildProperty && fromPropertyEnumerator.Current is SerializedProperty fromChildProperty) { CopyPropertyValue(fromChildProperty, toChildProperty); } } } private static void SaveGenericProperty(SerializedProperty property, object instance) { Type type = instance.GetType(); IEnumerable fields = type.GetRuntimeFields(); foreach (FieldInfo field in fields) { if (field.IsNotSerialized || field.IsStatic) { continue; } SerializedProperty fieldProperty = property.FindPropertyRelative(field.Name); if (fieldProperty != null) { SetSerializedPropertyValue(fieldProperty, field.GetValue(instance)); } else { Debug.Log($"SaveGenericProperty cannot field serializedProperty named '{field.Name}'"); } } } public static bool TryGetAttribute( this SerializedProperty serializedProperty, out TAttribute attribute, bool includeAttributesFromParentProperties = false, bool includeInheritedAttributes = true ) where TAttribute : Attribute { if (serializedProperty == null) { throw new ArgumentNullException(nameof(serializedProperty)); } Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType(); if (targetObjectType == null) { throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}"); } string[] pathSegments = includeAttributesFromParentProperties ? serializedProperty.propertyPath.Split('.') : new[] { serializedProperty.propertyPath }; foreach (string pathSegment in pathSegments) { FieldInfo fieldInfo = targetObjectType.GetField(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (fieldInfo != null) { attribute = fieldInfo.GetCustomAttribute(includeInheritedAttributes); if (attribute != null) { return true; } } PropertyInfo propertyInfo = targetObjectType.GetProperty(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (propertyInfo != null) { attribute = propertyInfo.GetCustomAttribute(includeInheritedAttributes); if (attribute != null) { return true; } } } attribute = null; return false; } public static bool TryGetAttributes( SerializedProperty serializedProperty, out List attributes, bool includeAttributesFromParentProperties = false, bool includeInheritedAttributes = true ) where TAttribute : Attribute { if (serializedProperty == null) { throw new ArgumentNullException(nameof(serializedProperty)); } Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType(); if (targetObjectType == null) { throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}"); } attributes = new List(); string[] pathSegments = includeAttributesFromParentProperties ? serializedProperty.propertyPath.Split('.') : new[] { serializedProperty.propertyPath }; foreach (string pathSegment in pathSegments) { FieldInfo fieldInfo = targetObjectType.GetField(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (fieldInfo != null) { IEnumerable foundAttributes = fieldInfo.GetCustomAttributes(includeInheritedAttributes); foreach (TAttribute foundAttribute in foundAttributes) { attributes.Add(foundAttribute); } } PropertyInfo propertyInfo = targetObjectType.GetProperty(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (propertyInfo != null) { IEnumerable foundAttributes = propertyInfo.GetCustomAttributes(includeInheritedAttributes); foreach (TAttribute attribute in foundAttributes) { attributes.Add(attribute); } } } return attributes.Count > 0; } } } #endif