前面我们实现如果通过界面,把我们自定义得节点管理起来。上次提到,我们如何把我们创建得节点序列化,这样再下次打开的时候可以还原进来。通过也可以给节点的执行当作资源赋值给组件中,这样就能在运行时动态执行我们的节点程序了。

这里我们不走系统提供的序列化函数,我们完全靠自己去实现一个。首先,我们定义一个序列化的接口类:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface INodeSerialize
{
    NodeData ToSerialize();
    bool ToUnSerialize(NodeData data);  
}

里面很简单,就把节点序列化和反序列化的过程。其中,我们没有直接序列化成string,而是一个可以又系统自动识别序列化的结构体对象,结构体的定义如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using System;

[Serializable]
public struct ParamBody
{
    public string ParamName;
    public Type ParamType;
    public string ParamValue;
} 

[Serializable]
public struct NodeData
{
    public string NodeName;
    public Type NodeType;
    public int NodeId;
    public ParamBody[] Params;
}

[Serializable]
public struct PortLine
{
    public int OutNodeId;
    public int InNodeId;
    public string OutPortName;
    public string InPortName;
}

[Serializable]
public struct PortConnect
{
    public PortLine[] PortLines;
}

接下来,我们为我们自己定义的节点添加上这个接口的实现:

//MyNodeBase    
public virtual NodeData ToSerialize()
    {
        NodeData data = new NodeData();
        data.NodeId = id;
        data.NodeName = "MyBaseNode";
        data.NodeType = typeof(MyBaseNode);
        return data;
    }

    public virtual bool ToUnSerialize(NodeData data)
    {
        this.id = data.NodeId;
        return true;
    }

根据基类,我们具体的IntAddNode 和 LogNode 覆写几个方法:

//IntAddNode
public override NodeData ToSerialize()
    {
        NodeData data =  base.ToSerialize();
        data.NodeName = "AddIntNode";
        data.NodeType = typeof(AddIntNode);

        List< ParamBody > paramBodys = new List<ParamBody>();
        ParamBody body1 = new ParamBody();
        body1.ParamName = "Param1";
        body1.ParamType = typeof(string);
        body1.ParamValue = Param1.value;
        paramBodys.Add(body1);

        ParamBody body2 = new ParamBody();
        body2.ParamName = "Param2";
        body2.ParamType = typeof(string);
        body2.ParamValue = Param2.value;
        paramBodys.Add(body2);

        data.Params = paramBodys.ToArray();
        return data;
    }

    public override bool ToUnSerialize(NodeData data)
    {
        base.ToUnSerialize(data);

        foreach(var it in data.Params)
        {
            if(it.ParamName == "Param1")
            {
                Param1.value = it.ParamValue;
            }else if(it.ParamName == "Param2")
            {
                Param2.value = it.ParamValue;
            }
        }

        return true;
    }
//LogNode
public override NodeData ToSerialize()
    {
        NodeData data = base.ToSerialize();
        data.NodeName = "LogNode";
        data.NodeType = typeof(LogNode);


        return data;
    }

我们只需要带有节点设置Field的地方覆写一下,Port的参数不需要序列化出来,因为Port的对象值时实时计算的,只需要序列化连接线即可。

然后我们在MyGraphView类中添加一个方法:

public void SaveToScriptableObject()
    {
        NodeScriptable obj = ScriptableObject.CreateInstance<NodeScriptable>();
        MyBaseNode[] nodes = this.GetAllNode();
        PortConnect connectLine = new PortConnect();
        List<NodeData> datas = new List<NodeData>();
        List<PortLine> lines = new List<PortLine>();
        for(int i = 0; i < nodes.Length; ++i)
        {
             //Port连接边查找
            int portCount = nodes[i].outputContainer.childCount;
            for(int j = 0; j < portCount; j++)
            {
                Port port = nodes[i].outputContainer.ElementAt(j) as Port;
                if(port != null)
                {
                    foreach(var pp in port.connections)
                    {
                        PortLine line = new PortLine();
                        line.OutNodeId = (pp.output.node as MyBaseNode).id;
                        line.InNodeId = (pp.input.node as MyBaseNode).id;
                        line.OutPortName = pp.output.portName;
                        line.InPortName = pp.input.portName;
                        lines.Add(line);
                    }
                }
            }
            NodeData d = nodes[i].ToSerialize();
            datas.Add(d);
        }
        connectLine.PortLines = lines.ToArray();
        obj.Nodes = datas.ToArray();
        obj.LineConnects = connectLine;

        UnityEditor.AssetDatabase.CreateAsset(obj, "Assets/NodeGraph-" + obj.GetInstanceID() + ".asset");
        EditorUtility.SetDirty(obj);
        UnityEditor.AssetDatabase.Refresh();

    }

连接边的查找,我们只需要根据输出Port遍历就可以了,因为输出有边,一定时连接了一个输入。其中NodeScriptable对象定义如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

[Serializable]
public class NodeScriptable : ScriptableObject
{
    public NodeData[] Nodes;
    public PortConnect LineConnects;
}

反序列化就是根据我们的 NodeScriptable 首先创建出节点,然后根据节点ID查找节点上的Port 进行连接即可。

 public void LoadFromScriptable(NodeScriptable obj)
    {
        foreach(var it in obj.Nodes)
        {
            MyBaseNode node = NodeFactory.CreateNode(it.NodeName);
            
            node.ToUnSerialize(it);
            AddElement(node);
        }

        foreach(var it in obj.LineConnects.PortLines)
        {
            MyBaseNode outNode = GetNodeById(it.OutNodeId);
            MyBaseNode inNode = GetNodeById(it.InNodeId);
            if(outNode != null && inNode != null)
            {
                Port pout = outNode.GetPortByName(it.OutPortName);
                Port pin = inNode.GetPortByName(it.InPortName);
                Edge e = pout.ConnectTo(pin);

                Add(e);
            }
        }
    }

public MyBaseNode GetNodeById(int id)
    {
        for (int i = 0; i < this.nodes.ToList().Count; ++i)
        {
            MyBaseNode node = this.nodes.AtIndex(i) as MyBaseNode;
            if (node.id == id)
            {
                return node;
            }
        }

        return null;
    }

这其中,要把MyBaseNode添加一个虚方法,可以根据Port名字查找Port对象

//MyNodeBase
public virtual Port GetPortByName(string name)
    {
        return null;
    }
//ProcessBaseNode
public override Port GetPortByName(string name)
    {
        if (name == "ParentNode" || name == ParentNode.portName)
        {
            return ParentNode;
        }else if(name == "ChildNode" || name == ChildNode.portName)
        {
            return ChildNode;
        }

        return null;
    }
//IntAddNode
public override Port GetPortByName(string name)
    {
        if(name == "Result" || name == Result.portName)
        {
            return Result;
        }

        return base.GetPortByName(name); ;
    }

LogNode同理,这里就不粘贴了,就是先从自己中寻找,没有就从父类中查找。

我这里没有使用反射创建Node,添加一个NodeFactory类进行创建。

using System.Collections.Generic;
using UnityEngine;

public class NodeFactory 
{
    public static MyBaseNode CreateNode(string name) 
    {
        if(name == "AddIntNode")
        {
            return new AddIntNode();
        }else if(name == "LogNode")
        {
            return new LogNode();
        }

        return null;
    }
}

好了,现在就需要添加2个按钮,一个保存,一个打开,用于保存序列化对象和打开序列化进行重建。修改我们的uxml文件

  <!--自定义内容开始-->
  <engine:VisualElement class="root" name="XXXXXX">
    
    <engine:VisualElement class="split left">
      <engine:VisualElement class="" >
        <engine:Button text="保存" name="button-save"></engine:Button>
        <engine:Button text="打开" name="button-open"></engine:Button>
      </engine:VisualElement>
      <engine:VisualElement class="split left" name="listView">
      </engine:VisualElement>
    </engine:VisualElement>

    <engine:VisualElement class="split right" name="graphView">
      
    </engine:VisualElement>
  </engine:VisualElement>
  <!--自定义内容结束-->

在Window中,实现2个按钮的点击:

Button saveBtn = rootUI.Query<Button>("button-save");
        saveBtn.clicked += () => {
            graphView.SaveToScriptableObject();
        };

        Button openBtn = rootUI.Query<Button>("button-open");
        openBtn.clicked += () => {
            string path = EditorUtility.OpenFilePanel("打开", "Assets/", "asset");
            path = path.Substring(path.IndexOf("Assets/"));
            NodeScriptable obj = AssetDatabase.LoadAssetAtPath<NodeScriptable>(path);
            if(obj != null)
            {
                graphView.LoadFromScriptable(obj);
            }
        };

这里就基本实现了对Node的序列化和反序列化过程,里面存在一些偷懒的地方,读者可以根据情况自行扩展。

本系列简单说明教程到此结束,希望对各位有所帮助。