Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mrpropellers/b8bf537a03f31f00cb53ff7138a9d9b0 to your computer and use it in GitHub Desktop.
Save mrpropellers/b8bf537a03f31f00cb53ff7138a9d9b0 to your computer and use it in GitHub Desktop.
Unity editor extension providing value get/set methods for SerializedProperty. This simplifies writing PropertyDrawers against non-trivial objects.
using System.Collections;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
// Provide simple value get/set methods for SerializedProperty. Can be used with
// any data types and with arbitrarily deeply-pathed properties.
public static class SerializedPropertyExtensions
{
/// (Extension) Get the value of the serialized property.
public static object GetValue(this SerializedProperty property)
{
string propertyPath = property.propertyPath;
object value = property.serializedObject.targetObject;
int i = 0;
while (NextPathComponent(propertyPath, ref i, out var token))
value = GetPathComponentValue(value, token);
return value;
}
/// (Extension) Set the value of the serialized property.
public static void SetValue(this SerializedProperty property, object value)
{
Undo.RecordObject(property.serializedObject.targetObject, $"Set {property.name}");
SetValueNoRecord(property, value);
EditorUtility.SetDirty(property.serializedObject.targetObject);
property.serializedObject.ApplyModifiedProperties();
}
/// (Extension) Set the value of the serialized property, but do not record the change.
/// The change will not be persisted unless you call SetDirty and ApplyModifiedProperties.
public static void SetValueNoRecord(this SerializedProperty property, object value)
{
string propertyPath = property.propertyPath;
object container = property.serializedObject.targetObject;
int i = 0;
NextPathComponent(propertyPath, ref i, out var deferredToken);
while (NextPathComponent(propertyPath, ref i, out var token))
{
container = GetPathComponentValue(container, deferredToken);
deferredToken = token;
}
Debug.Assert(!container.GetType().IsValueType, $"Cannot use SerializedObject.SetValue on a struct object, as the result will be set on a temporary. Either change {container.GetType().Name} to a class, or use SetValue with a parent member.");
SetPathComponentValue(container, deferredToken, value);
}
// Union type representing either a property name or array element index. The element
// index is valid only if propertyName is null.
struct PropertyPathComponent
{
public string propertyName;
public int elementIndex;
}
static Regex arrayElementRegex = new Regex(@"\GArray\.data\[(\d+)\]", RegexOptions.Compiled);
// Parse the next path component from a SerializedProperty.propertyPath. For simple field/property access,
// this is just tokenizing on '.' and returning each field/property name. Array/list access is via
// the pseudo-property "Array.data[N]", so this method parses that and returns just the array/list index N.
//
// Call this method repeatedly to access all path components. For example:
//
// string propertyPath = "quests.Array.data[0].goal";
// int i = 0;
// NextPropertyPathToken(propertyPath, ref i, out var component);
// => component = { propertyName = "quests" };
// NextPropertyPathToken(propertyPath, ref i, out var component)
// => component = { elementIndex = 0 };
// NextPropertyPathToken(propertyPath, ref i, out var component)
// => component = { propertyName = "goal" };
// NextPropertyPathToken(propertyPath, ref i, out var component)
// => returns false
static bool NextPathComponent(string propertyPath, ref int index, out PropertyPathComponent component)
{
component = new PropertyPathComponent();
if (index >= propertyPath.Length)
return false;
var arrayElementMatch = arrayElementRegex.Match(propertyPath, index);
if (arrayElementMatch.Success)
{
index += arrayElementMatch.Length + 1; // Skip past next '.'
component.elementIndex = int.Parse(arrayElementMatch.Groups[1].Value);
return true;
}
int dot = propertyPath.IndexOf('.', index);
if (dot == -1)
{
component.propertyName = propertyPath.Substring(index);
index = propertyPath.Length;
}
else
{
component.propertyName = propertyPath.Substring(index, dot - index);
index = dot + 1; // Skip past next '.'
}
return true;
}
static object GetPathComponentValue(object container, PropertyPathComponent component)
{
if (component.propertyName == null)
return ((IList)container)[component.elementIndex];
else
return GetMemberValue(container, component.propertyName);
}
static void SetPathComponentValue(object container, PropertyPathComponent component, object value)
{
if (component.propertyName == null)
((IList)container)[component.elementIndex] = value;
else
SetMemberValue(container, component.propertyName, value);
}
static object GetMemberValue(object container, string name)
{
if (container == null)
return null;
var type = container.GetType();
var members = type.GetMember(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
for (int i = 0; i < members.Length; ++i)
{
if (members[i] is FieldInfo field)
return field.GetValue(container);
else if (members[i] is PropertyInfo property)
return property.GetValue(container);
}
return null;
}
static void SetMemberValue(object container, string name, object value)
{
var type = container.GetType();
var members = type.GetMember(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
for (int i = 0; i < members.Length; ++i)
{
if (members[i] is FieldInfo field)
{
field.SetValue(container, value);
return;
}
else if (members[i] is PropertyInfo property)
{
property.SetValue(container, value);
return;
}
}
Debug.Assert(false, $"Failed to set member {container}.{name} via reflection");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment