Last active
November 20, 2024 04:10
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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"); | |
} | |
} |
This is awesome 😄
Does this come with an open source license?
Hi Kellojo, you can consider this public domain and relicense it as you see fit.
Awesome, thank you. Any chance you can add the license to the gist?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is brilliant, thank you! I'm forever surprised this isn't in the engine.