If you are writing editor code, it is common to use the SerializedObject and SerializedProperty interface to view the serialized form of data. How this data looks and acts is somewhat documented but it has a couple quirks. For this reason I’ve created a small utility class for viewing the representation of a SerializedObject in unity. I will talk a bit about SerializedProperties. If you are interested in the viewer code, the Gist is at the bottom of the post.
And here are some of my interesting findings using my viewer.
Strings…are Arrays!
You may want to iterate over all arrays in an object. Turns out that strings….are also serialized as arrays of chars! This may not be a surprise to those with a C background but what is also interesting is that *both* representations are present when iterating:
This is quite important to understand when using SerializedProperty.IsArray:
Strings will return true for being arrays.
Next-ness
When you are iterating over SerializedProperties, knowing the structure of the data is not intuitive and highly important. SerializedProperty.Next() will take you to the ‘next’ property…but what does that mean? What defines ‘next-ness?’ Unfortunately the docs could be improved a bit on this.
You may think that .Next(false ) would return false once it stopped iterating over the current depth of items because you are saying you don’t want to enter children. This is not the case. The serializedProperty will continue on to the ‘next’ property in the list. The correct way to determine the ‘end’ of a series of SerializedProperties is to use SerializedProperty.GetEndProperty() and SerializedProperty.EqualContents() to compare these values.
Materials
I initially wrote this utility to view Materials because they are more verbose than most UnityEngine.Objects, and the serialized format is pretty much NOTHING like what you see in the editor. I also added the ability to highlight search strings and a button that would select objects so I could easily go to references:
Custom Data
I have also used this information to understand how SerializedProperties work with custom data types. Let’s say you have a class such as this:
[System.Serializable] public class StringData { public string foo; public string bar; }
And you declare a member variable like so:
public StringData[] data;
What does the representation look like? Does Unity declare ‘this is an array of StringData’ in some form? The answer is no. There is no concept of the final data type within the serialization. The data is instead stored as generic data types such as lists, which you can see in the SerializedPropertyViewer output:
You can see that the type of data is called ‘Generic Mono’ and given the name of the member property that it is a part of. What is interesting is that you can use the SerializedProperty.propertyPath to update and change values of items in custom data types. For example you can update the value for the 2nd StringData’s Foo value with the following:
SerializedProperty secondFoo = serializedObject.FindProperty("data.Array.data[1].foo"); secondFoo.stringValue = "new value";
Once you know the path, which isn’t necessarily obvious, it becomes simpler to manipulate these values in the editor.
Finding Leaks
Another useful aspect of viewing the serialized properties is finding weird references that are causing memory usage. To give you an idea: I had an issue where a superclass of an object was always having a texture be serialized by an editor script. BUT this variable had the format of:
[SerializeField][HideInInspector] private Texture2D mTex;
So guess what? These textures were being loaded at runtime but I had no idea how/why! I couldn’t see them in the editor. But they were loading! I finally found the references being serialized and manually removed them from the YAML.
Show Me The Code!
Here’s code, and a quick screengrab of usage. Enjoy!
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
using UnityEngine; | |
using UnityEditor; | |
using System.Text; | |
using System.Collections; | |
using System.Collections.Generic; | |
public class SerializedPropertyViewer : EditorWindow | |
{ | |
public class SPData | |
{ | |
public int depth; | |
public string info; | |
public string val; | |
public int oid; | |
public SPData(int d, string i, string v, int o) | |
{ | |
if(d < 0) | |
{ | |
d = 0; | |
} | |
depth = d; | |
info = i; | |
val = v; | |
oid = o; | |
} | |
} | |
[MenuItem ("Window/SP Viewer")] | |
static void Init () | |
{ | |
// Get existing open window or if none, make a new one: | |
SerializedPropertyViewer window = (SerializedPropertyViewer)EditorWindow.GetWindow (typeof (SerializedPropertyViewer)); | |
window.titleContent = new GUIContent("SP Viewer"); | |
window.Show(); | |
} | |
UnityEngine.Object obj; | |
Vector2 scrollPos; | |
List<SPData> data; | |
bool dirty = true; | |
string searchStr; | |
string searchStrRep; | |
public static GUIStyle richTextStyle; | |
void OnGUI () | |
{ | |
if(richTextStyle == null) | |
{ | |
//EditorStyles does not exist in Constructor?? | |
richTextStyle = new GUIStyle(EditorStyles.label); | |
richTextStyle.richText = true; | |
} | |
UnityEngine.Object newObj = EditorGUILayout.ObjectField("Object:", obj, typeof(UnityEngine.Object), false); | |
string newSearchStr = EditorGUILayout.TextField("Search:", searchStr); | |
if(newSearchStr != searchStr) | |
{ | |
searchStr = newSearchStr; | |
searchStrRep = "<color=green>"+searchStr+"</color>"; | |
dirty = true; | |
} | |
if(obj != newObj) | |
{ | |
obj = newObj; | |
dirty = true; | |
} | |
if(data == null) | |
{ | |
dirty = true; | |
} | |
if(dirty == true) | |
{ | |
dirty = false; | |
searchObject(obj); | |
} | |
scrollPos = EditorGUILayout.BeginScrollView(scrollPos); | |
foreach(SPData line in data) | |
{ | |
EditorGUI.indentLevel = line.depth; | |
if(line.oid > 0){ | |
GUILayout.BeginHorizontal(); | |
} | |
EditorGUILayout.LabelField(line.info, richTextStyle); | |
if(line.oid > 0) | |
{ | |
if(GUILayout.Button(">>", GUILayout.Width(50))) | |
{ | |
Selection.activeInstanceID = line.oid; | |
} | |
GUILayout.EndHorizontal(); | |
} | |
} | |
EditorGUILayout.EndScrollView(); | |
} | |
void searchObject(UnityEngine.Object obj) | |
{ | |
data = new List<SPData>(); | |
if(obj == null) | |
{ | |
return; | |
} | |
SerializedObject so = new SerializedObject(obj); | |
SerializedProperty iterator = so.GetIterator(); | |
search(iterator, 0); | |
} | |
void search(SerializedProperty prop, int depth) | |
{ | |
logProperty(prop); | |
while(prop.Next(true)) | |
{ | |
logProperty(prop); | |
} | |
} | |
void logProperty(SerializedProperty prop) | |
{ | |
string strVal = getStringValue(prop); | |
string propDesc = prop.propertyPath+" type:"+prop.type + " name:"+prop.name + " val:"+ strVal; | |
if(searchStr.Length > 0) | |
{ | |
propDesc = propDesc.Replace(searchStr, searchStrRep); | |
} | |
data.Add(new SPData(prop.depth, propDesc, strVal, getObjectID(prop))); | |
} | |
int getObjectID(SerializedProperty prop) | |
{ | |
if(prop.propertyType == SerializedPropertyType.ObjectReference && prop.objectReferenceValue != null) | |
{ | |
return prop.objectReferenceValue.GetInstanceID(); | |
} | |
return 0; | |
} | |
string getStringValue(SerializedProperty prop) | |
{ | |
switch(prop.propertyType) | |
{ | |
case SerializedPropertyType.String: | |
return prop.stringValue; | |
case SerializedPropertyType.Character: //this isn't really a thing, chars are ints! | |
case SerializedPropertyType.Integer: | |
if(prop.type == "char") | |
{ | |
return System.Convert.ToChar(prop.intValue).ToString(); | |
} | |
return prop.intValue.ToString(); | |
case SerializedPropertyType.ObjectReference: | |
if(prop.objectReferenceValue != null) | |
{ | |
return prop.objectReferenceValue.ToString(); | |
}else{ | |
return "(null)"; | |
} | |
default: | |
return ""; | |
} | |
} | |
} |