前面我们实现如果通过界面,把我们自定义得节点管理起来。上次提到,我们如何把我们创建得节点序列化,这样再下次打开的时候可以还原进来。通过也可以给节点的执行当作资源赋值给组件中,这样就能在运行时动态执行我们的节点程序了。
这里我们不走系统提供的序列化函数,我们完全靠自己去实现一个。首先,我们定义一个序列化的接口类:
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的序列化和反序列化过程,里面存在一些偷懒的地方,读者可以根据情况自行扩展。
本系列简单说明教程到此结束,希望对各位有所帮助。
38 引用通告