-
-
Save v01pe/79db7566e2feff7ffab87676e220fd20 to your computer and use it in GitHub Desktop.
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); | |
} | |
} |
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.
Great stuff, partially solved my problem.
But there is still something that went wrong in my use case... Since all instance use the same property drawer, the initialization only triggered for the first instance and the other instance will not be initialized. I had to keep a dictionary to keep track of the initialization state in order to avoid reflection every frame.
Since all instance use the same property drawer, the initialization only triggered for the first instance and the other instance will not be initialized.
Hey, thanks for the report. Are you talking about multi object editing – which I don't think this solution will handle correctly – or more than one property (on the same GameObject
) using this drawer – which I also might not be handled correctly?
I think it is the 2nd case. I put them in a reorderable list so there will be multiple instances on the same game object. It is surprisingly troublesome to make it right. But actually I think even if they are not on the same game object, they will still share the same drawer. My final approach to reduce reflection was to have a Dictionary<int, Dictionary<string, Entry>> where the int is instance id (kind of an id for the component containing the property), and string is the property path, Entry is a struct containing initialization status, the referenced property instance, and everything else that is prominent. I had to invalidate this cache when there is a change to the reorderable list.
Yeah, I remember doing a similar thing for a project at some point. I think in my case it was about multi object editing, which also reuses the same drawer instance. But I guess I'll not update this gist to support that.
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:
I refactored the object retrieval and path parsing to general extension methods at some point: