Skip to content

Instantly share code, notes, and snippets.

@v01pe
Last active Dec 17, 2021
Embed
What would you like to do?
A relative simple way to get the instance object from a custom property drawer that draws a serialized class inside a component
using UnityEngine;
using System.Collections;
public class Example : MonoBehaviour
{
public ExampleClass example;
void Start() {}
void Update() {}
}
using UnityEngine;
using System;
[Serializable]
public class ExampleClass
{
public string field = "example string";
public void Print()
{
Debug.Log("ExampleClass.field: " + field);
}
}
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer (typeof(ExampleClass))]
public class ExampleProperty : NestablePropertyDrawer
{
protected new ExampleClass propertyObject { get { return (ExampleClass)base.propertyObject; } }
private SerializedProperty stringField = null;
protected override void Initialize(SerializedProperty prop)
{
base.Initialize(prop);
if (stringField == null)
stringField = prop.FindPropertyRelative("field");
}
public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label)
{
base.OnGUI(position, prop, label);
EditorGUI.BeginProperty(position, label, prop);
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position, stringField);
if (EditorGUI.EndChangeCheck())
{
stringField.serializedObject.ApplyModifiedProperties();
propertyObject.Print();
}
EditorGUI.EndProperty();
}
}
using UnityEngine;
using UnityEditor;
using System;
using System.Collections;
using System.Reflection;
using System.Text.RegularExpressions;
public class NestablePropertyDrawer : PropertyDrawer
{
private bool initialized = false;
protected object propertyObject = null;
protected Type objectType = null;
private static readonly Regex matchArrayElement = new Regex(@"^data\[(\d+)\]$");
protected virtual void Initialize(SerializedProperty prop)
{
if (initialized)
return;
SerializedObject serializedObject = prop.serializedObject;
string path = prop.propertyPath;
propertyObject = serializedObject == null || serializedObject.targetObject == null ? null : serializedObject.targetObject;
objectType = propertyObject == null ? null : propertyObject.GetType();
if (!string.IsNullOrEmpty(path) && propertyObject != null)
{
string[] splitPath = path.Split('.');
Type fieldType = null;
//work through the given property path, node by node
for (int i = 0; i < splitPath.Length; i++)
{
string pathNode = splitPath[i];
//both arrays and lists implement the IList interface
if (fieldType != null && typeof(IList).IsAssignableFrom(fieldType))
{
//IList items are serialized like this: `Array.data[0]`
Debug.AssertFormat(pathNode.Equals("Array", StringComparison.Ordinal), serializedObject.targetObject, "Expected path node 'Array', but found '{0}'", pathNode);
//just skip the `Array` part of the path
pathNode = splitPath[++i];
//match the `data[0]` part of the path and extract the IList item index
Match elementMatch = matchArrayElement.Match(pathNode);
int index;
if (elementMatch.Success && int.TryParse(elementMatch.Groups[1].Value, out index))
{
IList objectArray = (IList)propertyObject;
bool validArrayEntry = objectArray != null && index < objectArray.Count;
propertyObject = validArrayEntry ? objectArray[index] : null;
objectType = fieldType.IsArray
? fieldType.GetElementType() //only set for arrays
: fieldType.GenericTypeArguments[0]; //type of `T` in List<T>
}
else
{
Debug.LogErrorFormat(serializedObject.targetObject, "Unexpected path format for array item: '{0}'", pathNode);
}
//reset fieldType, so we don't end up in the IList branch again next iteration
fieldType = null;
}
else
{
FieldInfo field;
Type instanceType = objectType;
BindingFlags fieldBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
do
{
field = instanceType.GetField(pathNode, fieldBindingFlags);
//b/c a private, serialized field of a subclass isn't directly retrievable,
fieldBindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
//if neccessary, work up the inheritance chain until we find it.
instanceType = instanceType.BaseType;
}
while (field == null && instanceType != typeof(object));
//store object info for next iteration or to return
propertyObject = field == null || propertyObject == null ? null : field.GetValue(propertyObject);
fieldType = field == null ? null : field.FieldType;
objectType = fieldType;
}
}
}
initialized = true;
}
public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label)
{
Initialize(prop);
}
}
@emileswain
Copy link

emileswain commented Apr 8, 2021

This was a great revive.
I had

MyClass
{
    List<MyClass> childItems;
}

(ok, i've had to actually subclass MyClass because i couldn't be bothered to implement the custom serialisation required for a child to nest itself. But the scenario of having a List is still the same.)

After a bit of work, i got this this. So i can get access to the Item in the list and cast it as a Type T. I can then have an AddItem() method on the class to manage the list items. Which is great because adding items to serialised lists has a nasty side effect of cloning the last item when adding an item with InsertElementAtIndex().

 public static T GetPropertyClass<T>(SerializedProperty prop)
        {
        
            string[] path = prop.propertyPath.Split('.');
            Object  propertyObject = prop.serializedObject.targetObject;
            for  (int i=0; i < path.Length; i++)
            {
                string pathNode = path[i];
                if (pathNode.Equals("Array"))
                {
                    i++;
                    pathNode = path[i];
                    //var name : String = 'data[1]';
                    var foundS1 = pathNode.IndexOf("[");
                    var index = pathNode.Substring(foundS1+1, pathNode.Length - 2 - foundS1);
                    var intIndex = int.Parse(index);
                    //propertyObject = ((List<PrimaryTask>)propertyObject)[intIndex];
                    //propertyObject = ((List<T>) propertyObject)[intIndex];
                      propertyObject = ((IList)propertyObject)[intIndex];
                }
                else
                {
                    propertyObject = propertyObject.GetType().GetField(pathNode).GetValue(propertyObject);
                }
                
            }

            return (T)propertyObject;

        }

@v01pe
Copy link
Author

v01pe commented Apr 9, 2021

So i can get access to the Item in the list and cast it as a Type T

Now that you mention that, I remembered, that I ran into this issue as well down the road. I dug up my old code – not 100% sure we're doing the same thing, but I think so.

Here's my version:

protected virtual void Initialize(SerializedProperty prop)
{
	if (propertyObject == null)
		propertyObject = prop.GetSerializedObject();
}

I refactored the object retrieval and path parsing to general extension methods at some point:

public static object GetSerializedObject(this SerializedProperty property)
{
	return property.serializedObject.GetChildObject(property.propertyPath);
}

private static readonly Regex matchArrayElement = new Regex(@"^data\[(\d+)\]$");
public static object GetChildObject(this SerializedObject serializedObject, string path)
{
	object propertyObject = serializedObject.targetObject;

	if (path != "" && propertyObject != null)
	{
		string[] splitPath = path.Split('.');
		FieldInfo field = null;

		foreach (string pathNode in splitPath)
		{
			if (field != null && field.FieldType.IsArray)
			{
				if (pathNode.Equals("Array"))
					continue;

				Match elementMatch = matchArrayElement.Match(pathNode);
				int index;
				if (elementMatch.Success && int.TryParse(elementMatch.Groups[1].Value, out index))
				{
					field = null;
					object[] objectArray = (object[])propertyObject;
					if (objectArray != null && index < objectArray.Length)
						propertyObject = ((object[])propertyObject)[index];
					else
						return null;
				}
			}
			else
			{
				field = propertyObject.GetType().GetField(pathNode, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				propertyObject = field.GetValue(propertyObject);
			}
		}
	}
}

@emileswain
Copy link

emileswain commented Apr 9, 2021

Yeah, i saw another post somewhere else that deatl with the fieldType.IsArray in a similar matter. It might have even been your code lol.
Good stuff though, thanks.

This has been invaluable in resolving how to add new items to a List in a serialised Object that doesn't clone the previous item.

@v01pe
Copy link
Author

v01pe commented Aug 4, 2021

FYI: I incorporated the changes discussed here into the gist. Now the nested property can be gathered from within an array as well.

@v01pe
Copy link
Author

v01pe commented Sep 30, 2021

Since I need that functionality in a current project, I updated it, made it more robust and added some documentation. Biggest improvements:

  • Now I also store the type of the nested object on the way, since there seems no easy way to retrieve the System.Type of a SerializedProperty, and I need it in the routine anyways. Maybe that's also related to what you needed @nukadelic?
  • The script now correctly deals with nested objects within Arrays and List<>s. This includes your solution, using the IList interface, that both data structures support @emileswain.
  • Another addition that I needed is, to also find private fields with the [SerializeField] attribute in any base class of the nested object by iterating through its inheritance chain if necessary.

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