Skip to content

Instantly share code, notes, and snippets.

@aholkner
Last active July 17, 2023 13:58
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save aholkner/214628a05b15f0bb169660945ac7923b to your computer and use it in GitHub Desktop.
Save aholkner/214628a05b15f0bb169660945ac7923b 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.
/* MIT License
Copyright (c) 2022 Alex Holkner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
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");
}
}
@tomkail
Copy link

tomkail commented Sep 27, 2022

This is brilliant, thank you! I'm forever surprised this isn't in the engine.

@Kellojo
Copy link

Kellojo commented Nov 30, 2022

This is awesome 😄

Does this come with an open source license?

@aholkner
Copy link
Author

Hi Kellojo, you can consider this public domain and relicense it as you see fit.

@Kellojo
Copy link

Kellojo commented Nov 30, 2022

Awesome, thank you. Any chance you can add the license to the gist?

@zuohu
Copy link

zuohu commented Jan 4, 2023

GetMemberValue and SetMemberValue need to add a condition to skip Obsolete function
fix some property or field conflicts with unity componet
bool isDef = Attribute.IsDefined(members[i], typeof(ObsoleteAttribute)); if (isDef) { continue; }
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment