Search Unity

Propertydrawer for a DataStructure that is not-serializable ("tree" data structure- in this case)

Discussion in 'Immediate Mode GUI (IMGUI)' started by Glurth, Mar 18, 2015.

  1. Glurth

    Glurth

    Joined:
    Dec 29, 2014
    Posts:
    109
    I started with this (http://docs.unity3d.com/Manual/script-Serialization.html) sample from the docs, which allows one to add a normally non-serializable tree data structure to a component's inspector window, and have it display and serialize properly.
    Unfortunately the sample is based on a monobehavior class, but I wanted to be able add a "Tree" to ANY monobehavor component, just like say.. a List..and have it display and serialize properly.

    I couldn't find anything (possibly due to my awful searching skills) on the forums, docs or answers site for how to do this for a normally non-serializable data structure, like a tree, so I'm positing what I came up with. Hopefully it will help others.

    After struggling with a property drawer for this object, I eventually came up with the following.
    It certainly doesn't work perfectly in the inspector, but it works. I was hoping to get some feedback, because I have the feeling I did many things incorrectly.
    NOTE: though I did create a generic SerializableTree < T > class, it appears, one must declare and use a particular version of it, like say class StringTree: SerializableTree<string>, in order to actually use it in a monobehavior or propertydrawer. In the code I'm using a custom MenuInfo class for this: MenuTree : SerializableTree<MenuInfo>.
    Three files are shown below, the generic tree, the menu tree/sample component, and the property drawer file.

    GenericTree class

    Code (CSharp):
    1. using UnityEngine;
    2. #if UNITY_EDITOR
    3. using UnityEditor;
    4. #endif
    5. using System.Collections;
    6. using System.Collections.Generic;
    7. using System;
    8.  
    9. //node class that we will use for serialization
    10. [Serializable]
    11. public class SerializableIndexNode
    12. {
    13.     public int dataIndex;  //index into SerializableTree's dataList
    14.     public int childCount; //number of child nodes
    15.     public int indexOfFirstChild;  //index into SerializableTree's serializedNodes list of first child node (other children are consecutive)
    16.     public bool openForInspection; //used by the inspector to determine if node is expanded or collapsed
    17. }
    18.  
    19. [Serializable]
    20. public class SerializableTree<T> : ISerializationCallbackReceiver
    21. {
    22.     public class Node  //this class is declared INSIDE the Serializable tree because it references T
    23.     {
    24.         public T dataValue;
    25.         public int serializableDataIndex;
    26.         public Node parent;
    27.         public List<Node> children;
    28.         public bool openForInspection;
    29.         public Node(Node _parent, T data)
    30.         {
    31.             parent = _parent;
    32.             children = new List<Node>();
    33.             dataValue = data;
    34.             openForInspection = false;
    35.         }
    36.     }
    37.  
    38.     [System.NonSerialized]
    39.     //the root of what we use at runtime. not serialized.
    40.     public Node root;
    41.  
    42.     //the fields we give unity to serialize.
    43.     public List<SerializableIndexNode> serializedNodes;
    44.     public List<T> dataList;
    45.  
    46.  
    47.     public bool AddChild(Node parent, T data)//called by both he inspector & runtime users of the tree
    48.     {
    49.         if (parent == null) return false;
    50.    
    51.         parent.children.Add(new Node(parent,data));
    52.         ReSerialize();
    53.         return true;
    54.     }
    55.  
    56.     public void OnBeforeSerialize()
    57.     {
    58.         ReSerialize();
    59.         return;
    60.     }
    61.     public void ReSerialize()
    62.     {
    63.         //unity is about to read the serializedNodes field's contents. lets make sure
    64.         //we write out the correct data into that field from our runtime data
    65.  
    66.         //clear or create new serialized data lists
    67.         if (serializedNodes == null) serializedNodes = new List<SerializableIndexNode>();
    68.         serializedNodes.Clear();
    69.         if (dataList == null) dataList = new List<T>();
    70.         dataList.Clear();
    71.  
    72.  
    73.         int childIndex = 1; //counter to keep track of index to put new children at
    74.         AddNodeToSerializedNodes(root, ref childIndex);//store root node
    75.         AddNodeChildrenToSerializedNodeList(root, ref childIndex);//store root node children & further decendants, recursively
    76.     }
    77.  
    78.     void AddNodeToSerializedNodes(Node n, ref int newChildrenIndex)
    79.     {
    80.         if (n == null) return;
    81.         if (dataList == null) return;
    82.         if (serializedNodes == null) return;
    83.         dataList.Add(n.dataValue);
    84.         SerializableIndexNode serializedNode = new SerializableIndexNode();
    85.  
    86.         serializedNode.dataIndex = dataList.Count - 1;
    87.         //since we are changing the serialized data index for this node, we need to record it in the runtime node also, so it will reflect changes in inspector
    88.         n.serializableDataIndex = serializedNode.dataIndex;
    89.    
    90.         serializedNode.childCount = n.children.Count;
    91.         serializedNode.openForInspection=n.openForInspection;
    92.         if (serializedNode.childCount > 0)
    93.         {
    94.  
    95.             serializedNode.indexOfFirstChild = newChildrenIndex;
    96.             newChildrenIndex += n.children.Count;
    97.         }
    98.         else
    99.             serializedNode.indexOfFirstChild = 0;
    100.  
    101.         serializedNodes.Add(serializedNode);
    102.     }
    103.  
    104.     void AddNodeChildrenToSerializedNodeList(Node n, ref int newChildrenIndex)//store node children & further decendants, recursively
    105.     {
    106.         //first add the children
    107.         foreach (var child in n.children)
    108.         {
    109.             AddNodeToSerializedNodes(child, ref newChildrenIndex);
    110.         }
    111.         //then add the children's children - recursively
    112.         foreach (var child in n.children)
    113.         {
    114.             AddNodeChildrenToSerializedNodeList(child, ref newChildrenIndex);
    115.         }
    116.     }
    117.  
    118.     public void OnAfterDeserialize()
    119.     {
    120.         //Unity has just written new data into the serializedNodes field.
    121.         //let's populate our actual runtime data with those new values.
    122.    
    123.         if (serializedNodes.Count > 0)
    124.             root = ReadNodeFromSerializedNodes(0, null);
    125.         else
    126.             root = null;
    127.     }
    128.  
    129.     Node ReadNodeFromSerializedNodes(int index,Node _parent)
    130.     {
    131.         var serializedNode = serializedNodes[index];
    132.         Node ret_value = new Node(_parent, dataList[serializedNode.dataIndex]);
    133.         //ret_value.dataValue = dataList[serializedNode.dataIndex];
    134.         ret_value.serializableDataIndex = serializedNode.dataIndex;
    135.         ret_value.openForInspection = serializedNode.openForInspection;
    136.    
    137.         for (int i = 0; i != serializedNode.childCount; i++)
    138.             ret_value.children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i, ret_value));
    139.  
    140.         return ret_value;
    141.  
    142.     }
    143.  
    144.  
    145.     override public string ToString()//useful for debugging
    146.     {
    147.         return ToString(root);
    148.     }
    149.     string ToString(Node node, string indent = "")//demonstration of how to recursively iterate through the tree.
    150.     {
    151.         if (node == null) return "";
    152.         string ret_val = "\n" + indent + "Node data:" + node.dataValue.ToString();
    153.         ret_val += "\n" + indent + "Children nodes--";
    154.         foreach (Node n in node.children)
    155.         {
    156.             ret_val += "\n" + indent + "   child: ";
    157.             ret_val += ToString(n, indent + "        ");
    158.         }
    159.         return ret_val;
    160.     }
    161.  
    162. }
    163.  
    164.  
    MenuTree component

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System;
    5. using UnityEngine.EventSystems;
    6. using UnityEngine.Events;
    7. using UnityEditor;
    8.  
    9.  
    10.  
    11. [System.Serializable]
    12. public class MenuInfo   //this is a DATA class we want to store in a tree.  All members to be stored, must be serializable.
    13. {
    14.     public EventTrigger.TriggerEvent SubmitOverride;
    15.     public GameObject levelObject;
    16.     public string text;
    17.  
    18.     public override string ToString()  //useful for debugging
    19.     {
    20.         string submitstr= "null";
    21.         if (SubmitOverride != null)
    22.             submitstr=SubmitOverride.ToString();
    23.         string levelstr = "null";
    24.         if (levelObject != null)
    25.             levelstr = levelObject.name;
    26.         return "\n  submit:" + submitstr + "\n  obj:" + levelstr + "\n  txt:" + text;
    27.     }
    28. }
    29.  
    30. [System.Serializable]
    31. public class MenuTree : SerializableTree<MenuInfo>
    32.     // the main reason for this class to exist is so that we force the pre-compiler to generate a particular version of the SerializableTree, <MenuInfo> in this case.
    33.     //it also makes it easier to refrence in the component that uses it
    34. {  }
    35.  
    36.  
    37. public class MenuTreeComponent : MonoBehaviour
    38. {
    39.     //we want these two variables to show show up in the inspector for this compoenent
    40.     public int anInt;
    41.     public MenuTree menu;
    42.  
    43.     void Start()
    44.     {
    45.         if (menu.root == null)
    46.             Debug.Log("Update local root not set");
    47.         else
    48.             Debug.Log("tree found:" + menu.ToString());
    49.     }
    50.  
    51.     void Update()
    52.     {
    53.         //stuff
    54.     }
    55.  
    56.  
    57.  
    58. }
    59.  
    MenuTree's Property Drawer (this files goes in the "editor" folder. eventually would like to make this more generalized to work on ANY type of SeriliazedTree < T > )

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections;
    4. using UnityEngine.EventSystems;
    5. using UnityEngine.Events;
    6.  
    7. [CustomPropertyDrawer(typeof(MenuTree))]
    8. public class MenuTreePropertyDrawer : PropertyDrawer
    9. {
    10.  
    11.     SerializedProperty datalist;
    12.  
    13.     float runningHight;
    14.     MenuTree runtimeMenu;
    15.     float defaultLineHeight = 25;
    16.  
    17.  
    18.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    19.     {
    20.         float startY = position.yMin;
    21.  
    22.         Object o = property.serializedObject.targetObject;
    23.         System.Reflection.FieldInfo fields= fieldInfo;
    24.  
    25.         runtimeMenu = (MenuTree) fieldInfo.GetValue(o);
    26.    
    27.         datalist = property.FindPropertyRelative("dataList");
    28.  
    29.         GUIContent nodeText = new GUIContent("Menu root");
    30.  
    31.         SerializedProperty rootNodeData = datalist.GetArrayElementAtIndex(runtimeMenu.root.serializableDataIndex);
    32.         EditorGUI.PropertyField(position, rootNodeData, nodeText, true);
    33.         if (rootNodeData.isExpanded)
    34.         {
    35.             position.y += 150;
    36.             EditorGUI.indentLevel = EditorGUI.indentLevel + 1;
    37.             DisplayNodeChildren(ref position,// rootNode, 0,
    38.                 label, runtimeMenu.root);
    39.             EditorGUI.indentLevel = EditorGUI.indentLevel - 1;
    40.         }
    41.         else
    42.         {
    43.             position.y += defaultLineHeight;
    44.         }
    45.  
    46.         runningHight = position.yMin - startY;
    47.  
    48.     }
    49.  
    50.     void DisplayNodeChildren(ref Rect position, GUIContent label, MenuTree.Node runtimeNode)
    51.     {
    52.         position.xMin += 20; //shift right
    53.         int childCount = runtimeNode.children.Count;
    54.         int indexOfFirstChild = 0;
    55.         position.height = defaultLineHeight;
    56.  
    57.         if (runtimeNode.openForInspection = EditorGUI.Foldout(position, runtimeNode.openForInspection, "children: " + runtimeNode.children.Count.ToString() + " Nodes", true))
    58.         {
    59.             position.y += defaultLineHeight; // foldout
    60.        
    61.             position.xMin += 20;
    62.  
    63.             int endIndex = childCount + indexOfFirstChild;
    64.             int childCounter = 0;
    65.             for (int i = indexOfFirstChild; i < endIndex; i++, childCounter++)
    66.             {
    67.                 SerializedProperty childData = datalist.GetArrayElementAtIndex(runtimeNode.children[childCounter].serializableDataIndex);
    68.                 EditorGUI.PropertyField(position, childData, new GUIContent("Child " + (childCounter+1).ToString() + " : " + runtimeNode.children[childCounter].dataValue.text), true);
    69.                 if (childData.isExpanded)
    70.                 {
    71.                     position.y += 150;
    72.                     DisplayNodeChildren(ref position, label, runtimeNode.children[childCounter]);
    73.                 }
    74.                 else
    75.                     position.y += defaultLineHeight; //unexpanded child
    76.             }
    77.        
    78.  
    79.             Rect buttonPos = position;
    80.             buttonPos.width *= 0.8f;
    81.        
    82.             if (GUI.Button(buttonPos, "Add Child"))
    83.             {
    84.                 Debug.Log("click");
    85.                 MenuInfo newItem = new MenuInfo();
    86.                 newItem.text = "new item " + (runtimeNode.children.Count+1).ToString();
    87.                 runtimeMenu.AddChild(runtimeNode, newItem);
    88.                 return; // we will need to just redraw everything
    89.             }
    90.             position.y += defaultLineHeight+5;//button
    91.  
    92.        
    93.             position.xMin -= 20;
    94.         }//end if expanded
    95.         else
    96.         {
    97.             position.y += defaultLineHeight; // foldout
    98.         }
    99.  
    100.         position.xMin -= 20;
    101.     }
    102.  
    103.  
    104.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    105.     {
    106.         return runningHight;
    107.     }
    108.  
    109. }
    110.  
     
    Last edited: Mar 18, 2015
  2. BMayne

    BMayne

    Joined:
    Aug 4, 2014
    Posts:
    186
    Hey there,

    One issue I know you are going to run into is the fact that you can't write an editor for a generic class. The issue is the editor will only show if the <T> equals the instance you are looking at. So unless you write a custom editor class for every class that could ever exist you are going to run into issues. What I would suggest is making a base class that is not generic for your generic class. That way you could write a custom editor for that.

    For example
    Code (CSharp):
    1. public class SerializableTree<T> :  BaseSerializableTree, ISerializationCallbackReceiver
    2. {
    3.    ... code here
    4. }
     
  3. Glurth

    Glurth

    Joined:
    Dec 29, 2014
    Posts:
    109
    Hi BMayne,

    Thanks for the feedback!

    Perhaps I'm making it more complex than it needs to be: but I'm not sure how to implement your suggestion. The difficulty I see arises from the fact that, I'm using the root node to iterate though the tree in the property drawer, but no such variable would exist in the BaseSerializableTree. And even if I can just cast it, what class do I cast it into? A generic SerializableTree< T >? (would that even compile?) If I need to cast into a specified T version of SerializableTee< T> like MenuTree, then I'd just be back to square one.

    Hmm, I supposed the entire tree structure- WITHOUT data references, could be implemented in the BaseSerializableTree class, and then override the appropriate functions in the generic/data-having SerializableTree<T> class, to add in the data where appropriate. (this sounds pretty tricky.)