- Home /
Using Generic List with serializedProperty Inspector
I haven't found a simple answer and example for working with generic lists. Say I have a class with public List things
and I want to make my inspector work with multiple objects selected in the Editor.
[I'll update this example with any replies]
public class CompoundTargetTrackerInspector : Editor
{
private SerializedProperty things
private void OnEnable()
{
this.thingsProperty = this.serializedObject.FindProperty("things");
}
public override void OnInspectorGUI()
{
this.serializedObject.Update();
// 1. How do I cast back to a generic list?
List<Something> things = (List<Something>)this.thingsProperty; // <--Doesn't work
// 2. How do I iterate over the contents as "Something" types
bool clicked = GUILayout.Button("Click Me");
if (clicked)
{
for or foreach ?
{
...???...
Debug.Log(thing.name);
}
}
serializedObject.ApplyModifiedProperties();
}
}
Answer by guavaman · Apr 09, 2014 at 09:08 AM
this.thingsProperty is not the List variable. Its returning a SerializedProperty object. You will have to get the contents of your list from the SerializedProperty object. Generally, SerializedProperty will store your data in one of its many data fields based on the type of the data. For example, bool data values are stored in the SerializedProperty.boolValue field. There are other types for ints, floats, etc. References to objects are stored in .objectReferenceValue So the basic way of working with SerializedProperties is to get the property, then get the value from the appropriate field depending on the data type you're expecting.
SerializedProperty stores arrays in a special way (and it just treats a list as an array). It doesn't just store the array as an object which you can easily grab and work with. Instead, it stores a bunch of data for the array in various fields. Basically, you test if the field is an array with isArray, then step through the fields with SerializedProperty.Next(true) to get to the parts of the array like the length, and finally the data fields. Once you get the data, you still have to know its type and access it through SerializedProperty.boolValue or whatever type it is.
Here is an example of one to get the data from an array/list in a SerialzedObject:
[CustomEditor(typeof(MyTestType))]
public class SOListtest : Editor {
private SerializedProperty thingsProperty;
private void OnEnable() {
this.thingsProperty = this.serializedObject.FindProperty("_things");
}
public override void OnInspectorGUI() {
this.serializedObject.Update();
SerializedProperty sp = thingsProperty.Copy(); // copy so we don't iterate the original
if(sp.isArray) {
int arrayLength = 0;
sp.Next(true); // skip generic field
sp.Next(true); // advance to array size field
// Get the array size
arrayLength = sp.intValue;
sp.Next(true); // advance to first array index
// Write values to list
List<int> values = new List<int>(arrayLength);
int lastIndex = arrayLength - 1;
for(int i = 0; i < arrayLength; i++) {
values.Add(sp.intValue); // copy the value to the list
if(i < lastIndex) sp.Next(false); // advance without drilling into children
}
// iterate over the list displaying the contents
for(int i = 0; i < values.Count; i++) {
EditorGUILayout.LabelField(i + " = " + values[i]);
}
}
this.DrawDefaultInspector(); // show the default inspector so we can set some values
}
}
And here is the MonoBehaviour that the inspector inspects:
public class MyTestType : MonoBehaviour {
public List<int> things {
get {
return _things;
}
set {
_things = value;
}
}
[SerializeField]
private List<int> _things;
}
FYI, there are also other methods you can use to access and work with arrays in SerializedProperty:
SerializedProperty.ClearArray, SerializedProperty.DeleteArrayElementAtIndex, SerializedProperty.GetArrayElementAtIndex, SerializedProperty.InsertArrayElementAtIndex, SerializedProperty.MoveArrayElement
Additionally, you can directly access the contents of an array via SerializedObject.FindProperty by passing it a string to the path using this syntax:
"fieldName.Array.data[0].fieldName"
Below are some methods I made to help with working with SerializedProperties. You can see how arrays are stored and how to iterate through all the properties to discover the structure.
public static void LogProperties(SerializedObject so, bool includeChildren = true) {
// Shows all the properties in the serialized object with name and type
// You can use this to learn the structure
so.Update();
SerializedProperty propertyLogger = so.GetIterator();
while(true) {
Debug.Log("name = " + propertyLogger.name + " type = " + propertyLogger.type);
if(!propertyLogger.Next(includeChildren)) break;
}
}
// variablePath may have a structure like this:
// "meshData.Array.data[0].vertexColors"
// So it uses FindProperty to get data from a specific field in an object array
public static void SetSerializedProperty(UnityEngine.Object obj, string variablePath, object variableValue) {
SerializedObject so = new SerializedObject(obj);
SerializedProperty sp = so.FindProperty(variablePath);
if(sp == null) {
Debug.Log("Error setting serialized property! Variable path: \"" + variablePath + "\" not found in object!");
return;
}
so.Update(); // refresh the data
//SerializedPropertyType type = sp.propertyType; // get the property type
System.Type valueType = variableValue.GetType(); // get the type of the incoming value
if(sp.isArray && valueType != typeof(string)) { // serialized property is an array, except string which is also an array
// assume the incoming value is also an array
if(!WriteSerializedArray(sp, variableValue)) return; // write the array
} else { // not an array
if(!WriteSerialzedProperty(sp, variableValue)) return; // write the value to the property
}
so.ApplyModifiedProperties(); // apply the changes
}
private static bool WriteSerialzedProperty(SerializedProperty sp, object variableValue) {
// Type the property and fill with new value
SerializedPropertyType type = sp.propertyType; // get the property type
if(type == SerializedPropertyType.Integer) {
int it = (int)variableValue;
if(sp.intValue != it) {
sp.intValue = it;
}
} else if(type == SerializedPropertyType.Boolean) {
bool b = (bool)variableValue;
if(sp.boolValue != b) {
sp.boolValue = b;
}
} else if(type == SerializedPropertyType.Float) {
float f = (float)variableValue;
if(sp.floatValue != f) {
sp.floatValue = f;
}
} else if(type == SerializedPropertyType.String) {
string s = (string)variableValue;
if(sp.stringValue != s) {
sp.stringValue = s;
}
} else if(type == SerializedPropertyType.Color) {
Color c = (Color)variableValue;
if(sp.colorValue != c) {
sp.colorValue = c;
}
} else if(type == SerializedPropertyType.ObjectReference) {
Object o = (Object)variableValue;
if(sp.objectReferenceValue != o) {
sp.objectReferenceValue = o;
}
} else if(type == SerializedPropertyType.LayerMask) {
int lm = (int)variableValue;
if(sp.intValue != lm) {
sp.intValue = lm;
}
} else if(type == SerializedPropertyType.Enum) {
int en = (int)variableValue;
if(sp.enumValueIndex != en) {
sp.enumValueIndex = en;
}
} else if(type == SerializedPropertyType.Vector2) {
Vector2 v2 = (Vector2)variableValue;
if(sp.vector2Value != v2) {
sp.vector2Value = v2;
}
} else if(type == SerializedPropertyType.Vector3) {
Vector3 v3 = (Vector3)variableValue;
if(sp.vector3Value != v3) {
sp.vector3Value = v3;
}
} else if(type == SerializedPropertyType.Rect) {
Rect r = (Rect)variableValue;
if(sp.rectValue != r) {
sp.rectValue = r;
}
} else if(type == SerializedPropertyType.ArraySize) {
int aSize = (int)variableValue;
if(sp.intValue != aSize) {
sp.intValue = aSize;
}
} else if(type == SerializedPropertyType.Character) {
int ch = (int)variableValue;
if(sp.intValue != ch) {
sp.intValue = ch;
}
} else if(type == SerializedPropertyType.AnimationCurve) {
AnimationCurve ac = (AnimationCurve)variableValue;
if(sp.animationCurveValue != ac) {
sp.animationCurveValue = ac;
}
} else if(type == SerializedPropertyType.Bounds) {
Bounds bounds = (Bounds)variableValue;
if(sp.boundsValue != bounds) {
sp.boundsValue = bounds;
}
} else {
Debug.Log("Unsupported SerializedPropertyType \"" + type.ToString() + " encoutered!");
return false;
}
return true;
}
private static bool WriteSerializedArray(SerializedProperty sp, object arrayObject) {
System.Array[] array = (System.Array[])arrayObject; // cast to array
sp.Next(true); // skip generic field
sp.Next(true); // advance to array size field
// Set the array size
if(!WriteSerialzedProperty(sp, array.Length)) return false;
sp.Next(true); // advance to first array index
// Write values to array
int lastIndex = array.Length - 1;
for(int i = 0; i < array.Length; i++) {
if(!WriteSerialzedProperty(sp, array[i])) return false; // write the value to the property
if(i < lastIndex) sp.Next(false); // advance without drilling into children }
return true;
}
// A way to see everything a SerializedProperty object contains in case you don't
// know what type is stored.
public static void LogAllValues(SerializedProperty serializedProperty) {
Debug.Log("PROPERTY: name = " + serializedProperty.name + " type = " + serializedProperty.type);
Debug.Log("animationCurveValue = " + serializedProperty.animationCurveValue);
Debug.Log("arraySize = " + serializedProperty.arraySize);
Debug.Log("boolValue = " + serializedProperty.boolValue);
Debug.Log("boundsValue = " + serializedProperty.boundsValue);
Debug.Log("colorValue = " + serializedProperty.colorValue);
Debug.Log("depth = " + serializedProperty.depth);
Debug.Log("editable = " + serializedProperty.editable);
Debug.Log("enumNames = " + serializedProperty.enumNames);
Debug.Log("enumValueIndex = " + serializedProperty.enumValueIndex);
Debug.Log("floatValue = " + serializedProperty.floatValue);
Debug.Log("hasChildren = " + serializedProperty.hasChildren);
Debug.Log("hasMultipleDifferentValues = " + serializedProperty.hasMultipleDifferentValues);
Debug.Log("hasVisibleChildren = " + serializedProperty.hasVisibleChildren);
Debug.Log("intValue = " + serializedProperty.intValue);
Debug.Log("isAnimated = " + serializedProperty.isAnimated);
Debug.Log("isArray = " + serializedProperty.isArray);
Debug.Log("isExpanded = " + serializedProperty.isExpanded);
Debug.Log("isInstantiatedPrefab = " + serializedProperty.isInstantiatedPrefab);
Debug.Log("name = " + serializedProperty.name);
Debug.Log("objectReferenceInstanceIDValue = " + serializedProperty.objectReferenceInstanceIDValue);
Debug.Log("objectReferenceValue = " + serializedProperty.objectReferenceValue);
Debug.Log("prefabOverride = " + serializedProperty.prefabOverride);
Debug.Log("propertyPath = " + serializedProperty.propertyPath);
Debug.Log("propertyType = " + serializedProperty.propertyType);
Debug.Log("quaternionValue = " + serializedProperty.quaternionValue);
Debug.Log("rectValue = " + serializedProperty.rectValue);
Debug.Log("serializedObject = " + serializedProperty.serializedObject);
Debug.Log("stringValue = " + serializedProperty.stringValue);
Debug.Log("tooltip = " + serializedProperty.tooltip);
Debug.Log("type = " + serializedProperty.type);
Debug.Log("vector2Value = " + serializedProperty.vector2Value);
Debug.Log("vector3Value = " + serializedProperty.vector3Value);
}
This information is extremely valuable. Could I request one addition? It would be very helpful to see a function that takes a SerializedProperty (representing a List, so an Array) that returns a List. Or maybe it would be easier to populate a List referenced so the signature includes a List and just does a Clear() and AddRange() perhaps? WDYT?
I'll edit my question to be more explicit too. When I posted it I wasn't even sure what I was asking. After refitting most of our inspectors I have a better grasp of things, but I still couldn't figure out the Array internal workings.
Answer by numberkruncher · Apr 12, 2014 at 10:27 PM
I do not fully understand your question, but I have put together a few examples which I thought might be helpful for you:
The default list editor with multi-editing and undo support.
Modifying the data directly using
Undo.RecordObjects
.The default text field with multi-editing and undo support.
Converting existing editor GUI controls into multi-editing versions.
Also, if you really need to you can enumerate arrays using SerializedProperty
:
yourProp.arraySize
yourProp.GetArrayElementAtIndex
yourProp.InsertArrayElementAtIndex
yourProp.DeleteArrayElementAtIndex
yourProp.MoveArrayElement
Example Behaviour:
using UnityEngine;
using System.Collections.Generic;
public class Example : MonoBehaviour {
public List<string> things = new List<string>(); public string label1 = ""; public string label2 = ""; }
Example Editor:
using UnityEngine; using UnityEditor;
[CustomEditor(typeof(Example)), CanEditMultipleObjects] public class ExampleEditor : Editor {
private SerializedProperty thingsProp; private SerializedProperty label1Prop; private SerializedProperty label2Prop; private void OnEnable() { thingsProp = serializedObject.FindProperty("things"); label1Prop = serializedObject.FindProperty("label1"); label2Prop = serializedObject.FindProperty("label2"); } public override void OnInspectorGUI() { serializedObject.Update(); // Editable list of things. EditorGUILayout.PropertyField(thingsProp, new GUIContent("Things"), true); if (GUILayout.Button("Append Item")) { // Undo/redo support for changes made to behaviour fields. Undo.RecordObjects(targets, "Append Item"); // You can loop through list as normal here! foreach (Example target in targets) { target.things.Add("Appended!"); // You can use either `foreach` or `for`. foreach (var thing in target.things) Debug.Log(thing); // Any changes made to the array will be undoable. } } // Multi-object editable label (the easy way): EditorGUILayout.PropertyField(label1Prop, new GUIContent("Label 1")); // Multi-object editable label (the manual way): EditorGUI.showMixedValue = label2Prop.hasMultipleDifferentValues; EditorGUI.BeginChangeCheck(); string newValue = EditorGUILayout.TextField("Label 2", label2Prop.stringValue); if (EditorGUI.EndChangeCheck()) label2Prop.stringValue = newValue; EditorGUI.showMixedValue = false; serializedObject.ApplyModifiedProperties(); } }
Aside:
You might be interested in my reorderable list control. Whilst I haven't tested it for multi-object editing, I suspect that it could be used (perhaps with some minor alterations):
https://bitbucket.org/rotorz/reorderable-list-editor-field-for-unity
I hope that this helps a little anyhow. Feel free to follow up with comments :)
Thanks for the effort but the question is actually about multi-object editing of custom List fields. Which after learning a lot more really means getting in and out of the SerializedProperty to handle changes. In other words, using a serialized property for display and then converting data back to a generic List for handling (reading the changes) and perhaps even setting further changes back to the SerializedProperty (writing the changes).
Answer by Fornoreason1000 · Jan 17, 2015 at 11:08 AM
For those who are interested there is an array handler class in UnityEditorInternal namespace. However there is ZERO docs on it , mostly because UnityDevs don't want us to use it or something.
Inside the namespace there is a class called ReOrderableList which handles your array for example
using System.Collections.Generic;
using UnityEngine
[System.Serializable]
class ClassThatHasAList {
[SerializeField]
List<int> myIntList = new List<int>();
}
We've already seen how hard using editor script when dealing with Generic Types, its made worse with the way their handled in Serialized Property.
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System.Collections;
using System.Collection.Generic;
[CustomEditor("ClassThatHasAList")]
class ListIntEditor : Editor {
ReorderableList list;
public void OnEnable() {
list = new ReorderableList(serializedObject,
serializedObject.FindProperty("myIntList"),
true,
true,
true,
true);
}
public override void OnInspectorGUI () {
list.DoLayoutList();
}
}
This should Give You something Similar you see in the Event Trigger Component, It also has several call backs you can use Lambda expressions to customize how it is drawn and behaves.
I found some more info on them here: http://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
Hope it Helps