I actually don’t get a lot of feedback on my posts, but one post stood out: Creating a Node Based Editor in Unity. Some people asked for a more advanced version and some asked how they can save and load data to the editor. I can make an advanced version, but it probably wouldn’t cover all usage cases. On the other hand, serializing (and deserializing) a node list is a more common problem. Although I addressed some issues in XML Serialization in Unity post, I would like to complete the node based editor by adding save and load features in this post.

So, in this post we’ll have a look at those two posts, merge them together and fix some issues. We might even end up revisiting Creating a Clone of Unity’s Console Window. After this tutorial, you’ll have a node based editor which can restore its last state after it is closed.

Revisiting the Node Based Editor

Well, it’s kind of obvious, but we need a node based editor in order to serialize it. We’ll use the code from this post, so if you haven’t already, go ahead and check it out now. It’s pretty straightforward, but we’ll have to modify it in order to properly serialize the nodes.

Node based editor The node based editor we created previously

Revisiting XML Serialization

There are a number of serialization options, such as JSON or Unity’s own serialization system. However, since we already covered XML serialization previously, why not use it? We’ll use the XMLOp class and some of the XML attributes from this post.

Revisiting Console Window Clone

This post is not that relevant to our matter at hand, but the node editor window lacks buttons for saving and loading. It would be nice to stick with Unity’s standards, so I’ll copy the menu bar from this window clone. It is easy to implement and nice to look at.

Unity's console window We'll copy the part that you see in area 1

Adding a Menu Bar

We’ll start with adding a menu bar, and we’ll copy it from the console window clone:

    private float menuBarHeight = 20f;
    private Rect menuBar;
    private void OnGUI()
    {
        DrawGrid(20, 0.2f, Color.gray);
        DrawGrid(100, 0.4f, Color.gray);
        DrawMenuBar();

        DrawNodes();
        DrawConnections();

        DrawConnectionLine(Event.current);

        ProcessNodeEvents(Event.current);
        ProcessEvents(Event.current);

        if (GUI.changed) Repaint();
    }

    private void DrawMenuBar()
    {
        menuBar = new Rect(0, 0, position.width, menuBarHeight);

        GUILayout.BeginArea(menuBar, EditorStyles.toolbar);
        GUILayout.BeginHorizontal();

        GUILayout.Button(new GUIContent("Save"), EditorStyles.toolbarButton, GUILayout.Width(35));
        GUILayout.Space(5);
        GUILayout.Button(new GUIContent("Load"), EditorStyles.toolbarButton, GUILayout.Width(35));

        GUILayout.EndHorizontal();
        GUILayout.EndArea();
    }

On line 56, we call DrawMenuBar() method and between lines 69-82 we create the menu bar. The console window clone had a button and 6 toggles, but since we are only serializing and deserializing, we need no more than two buttons. Keep in mind that the editor GUI system has a draw order and it draws elements from back to front in the order you call them. That’s why we are drawing the menu bar after drawing the grid. Otherwise the grid would have been drawn over the menu bar.

Grid over menu bar DrawMenuBar() is called before DrawGrid(), hence the ugly grid over the menu bar.

Currently, save and load buttons do nothing, but we’ll get to that.

Serialization

Next up, we need to prepare our classes (Node and Connection) for serialization. Let’s remember the two important key points about XML serializing:

  1. XML serializer can only serialize public fields.
  2. Class to be serialized should have a parameterless constructor.

Rule number 1 doesn’t cause that many problems (it still causes some but we’ll get to that), but rule number 2 is problematic. Both of our classes have constructors with parameters. Let’s fix that first:

    public Node() { }
    
    public Node(Vector2 position, float width, float height, GUIStyle nodeStyle, GUIStyle selectedStyle, GUIStyle inPointStyle, GUIStyle outPointStyle, Action<ConnectionPoint> OnClickInPoint, Action<ConnectionPoint> OnClickOutPoint, Action<Node> OnClickRemoveNode)
    {
        rect = new Rect(position.x, position.y, width, height);
        style = nodeStyle;
        inPoint = new ConnectionPoint(this, ConnectionPointType.In, inPointStyle, OnClickInPoint);
        outPoint = new ConnectionPoint(this, ConnectionPointType.Out, outPointStyle, OnClickOutPoint);
        defaultNodeStyle = nodeStyle;
        selectedNodeStyle = selectedStyle;
        OnRemoveNode = OnClickRemoveNode;
    }
    public Connection() { }
    
    public Connection(ConnectionPoint inPoint, ConnectionPoint outPoint, Action<Connection> OnClickRemoveConnection)
    {
        this.inPoint = inPoint;
        this.outPoint = outPoint;
        this.OnClickRemoveConnection = OnClickRemoveConnection;
    }

Next, we are going to ignore the properties that can’t be serialized or don’t need to be serialized. For example, in Node class, GUIStyles can be left out of serialization, because they are already provided by the editor itself. We don’t need isDragged or isSelected either. Actually, Node class has only one property that needs to be serialized: the rect. Let’s see how Node class looks like after properly ignoring unnecessary and unserializable properties:

public class Node
{
	public Rect rect;

	[XmlIgnore] public string title;
	[XmlIgnore] public bool isDragged;
	[XmlIgnore] public bool isSelected;

	[XmlIgnore] public ConnectionPoint inPoint;
	[XmlIgnore] public ConnectionPoint outPoint;

	[XmlIgnore] public GUIStyle style;
	[XmlIgnore] public GUIStyle defaultNodeStyle;
	[XmlIgnore] public GUIStyle selectedNodeStyle;

	[XmlIgnore] public Action<Node> OnRemoveNode;

	public Node() { }

Node class is ready to be serialized at this point. So, let’s serialize nodes!

Saving Nodes

Remember the save button which did nothing at all? Well, it should at least save nodes, since they are now serializable. The method for saving nodes is really simple:

    private void Save()
    {
        XMLOp.Serialize(nodes, "Assets/Resources/nodes.xml");
    }

And we are going to call that method when the user clicks Save button:

    private void DrawMenuBar()
    {
        menuBar = new Rect(0, 0, position.width, menuBarHeight);

        GUILayout.BeginArea(menuBar, EditorStyles.toolbar);
        GUILayout.BeginHorizontal();

        if (GUILayout.Button(new GUIContent("Save"), EditorStyles.toolbarButton, GUILayout.Width(35)))
        {
            Save();
        }
        
        GUILayout.Space(5);
        GUILayout.Button(new GUIContent("Load"), EditorStyles.toolbarButton, GUILayout.Width(35));

        GUILayout.EndHorizontal();
        GUILayout.EndArea();
    }

Now, open up your Node Based Editor, place a couple of nodes, and then hit Save (you must have a Resources folder under Assets before doing that). Unity will create a file called nodes.xml in Resources, if you can’t see it, simply right-click on Resources and then click Reimport. Contents of the nodes.xml file should be something like this:

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfNode xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Node>
    <rect>
      <x>166</x>
      <y>183</y>
      <position>
        <x>166</x>
        <y>183</y>
      </position>
      <center>
        <x>266</x>
        <y>208</y>
      </center>
      <min>
        <x>166</x>
        <y>183</y>
      </min>
      <max>
        <x>366</x>
        <y>233</y>
      </max>
      <width>200</width>
      <height>50</height>
      <size>
        <x>200</x>
        <y>50</y>
      </size>
      <xMin>166</xMin>
      <yMin>183</yMin>
      <xMax>366</xMax>
      <yMax>233</yMax>
    </rect>
  </Node>
  <Node>
    <rect>
      <x>345</x>
      <y>345</y>
      <position>
        <x>345</x>
        <y>345</y>
      </position>
      <center>
        <x>445</x>
        <y>370</y>
      </center>
      <min>
        <x>345</x>
        <y>345</y>
      </min>
      <max>
        <x>545</x>
        <y>395</y>
      </max>
      <width>200</width>
      <height>50</height>
      <size>
        <x>200</x>
        <y>50</y>
      </size>
      <xMin>345</xMin>
      <yMin>345</yMin>
      <xMax>545</xMax>
      <yMax>395</yMax>
    </rect>
  </Node>
</ArrayOfNode>

So, our nodes can be serialized now and if we can serialize Connections, our node editor will be completely serializable. Let’s get on with it then.

Serializing Connections

Connection class has only 2 properties that can be serialized: inPoint (ConnectionPoint) and outPoint (ConnectionPoint). However, serializing these two properties would be meaningless, because objects do not keep references to other objects after deserialization. Which means, if we deserialize a connection, it would create two connection points and connect them, but those connection points wouldn’t belong to the nodes it used to connect (see the figure below).

Broken connections Serialization breaks connections and deserializing doesn't fix that.

In order to solve this issue, we need some kind of an identifier for a connection point, i.e a unique ID, so that after deserializing, we can look for those connection points by their IDs and give reference to actual objects to restore connections.

public class ConnectionPoint
{
    public string id;
    
    [XmlIgnore] public Rect rect;

    [XmlIgnore] public ConnectionPointType type;

    [XmlIgnore] public Node node;

    [XmlIgnore] public GUIStyle style;

    [XmlIgnore] public Action<ConnectionPoint> OnClickConnectionPoint;

    public ConnectionPoint() { }
    
    public ConnectionPoint(Node node, ConnectionPointType type, GUIStyle style, Action<ConnectionPoint> OnClickConnectionPoint, string id = null)
    {
        this.node = node;
        this.type = type;
        this.style = style;
        this.OnClickConnectionPoint = OnClickConnectionPoint;
        rect = new Rect(0, 0, 10f, 20f);

        this.id = id ?? Guid.NewGuid().ToString();
    }
public class Connection
{
    public ConnectionPoint inPoint;
    public ConnectionPoint outPoint;
    [XmlIgnore] public Action<Connection> OnClickRemoveConnection;
public class Node
{
	public Rect rect;

	[XmlIgnore] public string title;
	[XmlIgnore] public bool isDragged;
	[XmlIgnore] public bool isSelected;

	public ConnectionPoint inPoint;
	public ConnectionPoint outPoint;

	[XmlIgnore] public GUIStyle style;
	[XmlIgnore] public GUIStyle defaultNodeStyle;
	[XmlIgnore] public GUIStyle selectedNodeStyle;

	[XmlIgnore] public Action<Node> OnRemoveNode;

Of course, we need to update our Save method to include connections as well:

    private void Save()
    {
        XMLOp.Serialize(nodes, "Assets/Resources/nodes.xml");
        XMLOp.Serialize(connections, "Assets/Resources/connections.xml");
    }

This concludes the serialization (and frankly, the hard) part. Now we have an XML representation of the current state of our node based editor. All we have to do is convert it back.

Deserializing

First things first: the Load button should be functional.

    private void DrawMenuBar()
    {
        menuBar = new Rect(0, 0, position.width, menuBarHeight);

        GUILayout.BeginArea(menuBar, EditorStyles.toolbar);
        GUILayout.BeginHorizontal();

        if (GUILayout.Button(new GUIContent("Save"), EditorStyles.toolbarButton, GUILayout.Width(35)))
        {
            Save();
        }
        
        GUILayout.Space(5);

        if (GUILayout.Button(new GUIContent("Load"), EditorStyles.toolbarButton, GUILayout.Width(35)))
        {
            Load();
        }

        GUILayout.EndHorizontal();
        GUILayout.EndArea();
    }

We are going to deserialize the contents of the XML files in Load() method, create nodes and connections and assign them to their respective properties. Deserializing the XML files is a pretty straightforward process; all we have to do is call XMLOp.Deserialize<T>(string):

    private void Load()
    {
        var nodesDeserialized = XMLOp.Deserialize<List<Node>>("Assets/Resources/nodes.xml");
        var connectionsDeserialized = XMLOp.Deserialize<List<Connection>>("Assets/Resources/connections.xml");

        nodes = new List<Node>();
        connections = new List<Connection>();
    }

However, deserializing the XML files alone is not enough to restore our editor to its last state, because as you can see in the figure above, we broke the relation between the nodes and connections while serializing and we need to reconnect them. This reconnection process requires finding nodes by IDs and creating a connection between them. This is why we added unique IDs to our ConnectionPoint class. We need to recreate the ConnectionPoints with those IDs, so we are going to add another constructor to Node class:

    public Node(Vector2 position, float width, float height, GUIStyle nodeStyle, GUIStyle selectedStyle, GUIStyle inPointStyle, GUIStyle outPointStyle, Action<ConnectionPoint> OnClickInPoint,
		Action<ConnectionPoint> OnClickOutPoint, Action<Node> OnClickRemoveNode, string inPointID, string outPointID)
	{
		rect = new Rect(position.x, position.y, width, height);
		style = nodeStyle;
		inPoint = new ConnectionPoint(this, ConnectionPointType.In, inPointStyle, OnClickInPoint, inPointID);
		outPoint = new ConnectionPoint(this, ConnectionPointType.Out, outPointStyle, OnClickOutPoint, outPointID);
		defaultNodeStyle = nodeStyle;
		selectedNodeStyle = selectedStyle;
		OnRemoveNode = OnClickRemoveNode;
	}

This is a new constructor, and it will create a Node with two ConnectionPoints with given (instead of generated) IDs. Now we are going to create new nodes based on deserialized nodes:

    private void Load()
    {
        var nodesDeserialized = XMLOp.Deserialize<List<Node>>("Assets/Resources/nodes.xml");
        var connectionsDeserialized = XMLOp.Deserialize<List<Connection>>("Assets/Resources/connections.xml");

        nodes = new List<Node>();
        connections = new List<Connection>();

        foreach (var nodeDeserialized in nodesDeserialized)
        {
            nodes.Add(new Node(
                nodeDeserialized.rect.position, 
                nodeDeserialized.rect.width, 
                nodeDeserialized.rect.height, 
                nodeStyle, 
                selectedNodeStyle, 
                inPointStyle, 
                outPointStyle, 
                OnClickInPoint, 
                OnClickOutPoint, 
                OnClickRemoveNode,
                nodeDeserialized.inPoint.id,
                nodeDeserialized.outPoint.id
                )
            );
        }
    }

Go ahead and try it. Create a couple of nodes, save it, close the editor, open it again and hit the Load button. You’ll see that your nodes return back to their positions. Let’s deserialize connections and finalize our node based editor:

    private void Load()
    {
        var nodesDeserialized = XMLOp.Deserialize<List<Node>>("Assets/Resources/nodes.xml");
        var connectionsDeserialized = XMLOp.Deserialize<List<Connection>>("Assets/Resources/connections.xml");

        nodes = new List<Node>();
        connections = new List<Connection>();

        foreach (var nodeDeserialized in nodesDeserialized)
        {
            nodes.Add(new Node(
                nodeDeserialized.rect.position, 
                nodeDeserialized.rect.width, 
                nodeDeserialized.rect.height, 
                nodeStyle, 
                selectedNodeStyle, 
                inPointStyle, 
                outPointStyle, 
                OnClickInPoint, 
                OnClickOutPoint, 
                OnClickRemoveNode,
                nodeDeserialized.inPoint.id,
                nodeDeserialized.outPoint.id
                )
            );
        }

        foreach (var connectionDeserialized in connectionsDeserialized)
        {
            var inPoint = nodes.First(n => n.inPoint.id == connectionDeserialized.inPoint.id).inPoint;
            var outPoint = nodes.First(n => n.outPoint.id == connectionDeserialized.outPoint.id).outPoint;
            connections.Add(new Connection(inPoint, outPoint, OnClickRemoveConnection));
        }
    }

Last Words

This concludes our tutorial on serializing a node based editor. You now have a fully functioning node based editor with save and load features. If you have any questions or feedback, leave a comment.

As always, here are the scripts in full. Until next time.