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); | |
} | |
} |
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);
}
}
}
}
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.
FYI: I incorporated the changes discussed here into the gist. Now the nested property can be gathered from within an array as well.
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 aSerializedProperty
, 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
Array
s andList<>
s. This includes your solution, using theIList
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.
This was a great revive.
I had
(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().