打开SO数据

使用Unity内置函数添加双击打开事件

使用Unity提供的OnOpenAsset(0),然后重新设置graphView的数据并重绘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[OnOpenAsset(0)]
public static bool OnOpen(int instanceID, int line)
{
GraphSoData nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as GraphSoData;
if (nodeGraph != null)
{
Open(nodeGraph);
return true;
}
return false;
}

public static void Open(GraphSoData nodeGraph)
{
ShowExample();
m_graphView.SetGraphData(nodeGraph);
m_graphView.RebuildGraph();
}

获取So数据后进行节点的创建

RebuildGraph函数先清空原有的所有节点和节点连接,再重新创建。

1
2
3
4
5
public void RebuildGraph()
{
ClearNodeAndEdge();
CreateNode(m_graphData.nodeSoDataList);
}

清空节点

contentContainer是Unity节点内的所有元素的容器,遍历这个容器,把NodeBase和Edge删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public virtual void ClearNodeAndEdge()
{
if (contentContainer == null || contentContainer.childCount <= 0)
{
return;
}

for (int i = contentContainer.childCount - 1; i >= 0; i--)
{
var elementItem = contentContainer.ElementAt(i);

//???Node
if (elementItem is NodeBase node)
{
contentContainer.RemoveAt(i);
}

//???Edge
if (elementItem is Edge edge)
{
contentContainer.RemoveAt(i);
}
}
}

创建节点

拿到GraphSoData中的NodeSoData列表信息进行遍历。对每个Node数据信息,拿到TypeName后使用反射实例化出具体的Node子类,随后通过AddElement函数把Node添加到图中管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public virtual List<NodeBase> CreateNode(List<NodeSoData> nodeDataList)
{

if (nodeDataList == null || nodeDataList.Count <= 0)
{
return null;
}

List<NodeBase> nodeViewList = new List<NodeBase>(nodeDataList.Count);

foreach (NodeSoData node in nodeDataList)
{
Type nodeType = Type.GetType(node.nodeFullTypeName);
NodeBase newNode = (NodeBase)Activator.CreateInstance(nodeType);
newNode.Init(node, this);
nodeViewList.Add(newNode);
AddElement(newNode);
}

LinkNode();

return nodeViewList;
}

创建连接

为了避免找不到未生成节点的先后顺序问题,所以在所有的节点创建完成后,再进行节点连接的创建。节点之间的连接通过之前记录在端口中的LinkData数据,通过节点ID和端口ID找到唯一的端口,并通过ConnectTo函数连接两个节点,最后把Edge连接通过AddElement函数添加到Graph中管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected virtual void LinkNode()
{
foreach (var node in nodes)
{
NodeBase nodeBase = node as NodeBase;

if (nodeBase == null)
{
continue;
}
foreach (var portData in nodeBase.nodeData.outputPortDataList)
{
long curPortID = portData.portID;
Port curPort = GetPortByID(curPortID);
foreach (var linkData in portData.linkData)
{
long linkPortID = linkData.linkPortID;
Port linkPort = GetPortByID(linkPortID);
if (linkPort == null)
{
Debug.LogError($"nodeID={nodeBase.nodeData.nodeID} linkPortID={linkPortID},port is null");
}
var edge = curPort.ConnectTo(linkPort);
AddElement(edge);
}
}
}
}

保存运行时序列化数据

前面的GraphSoData是仅作为编辑器使,而且运行时的时候比如一些节点的大小、位置这些信息是用不到的。为了数据的精简和高效,所以在原有的NodeSoData创建了一个NodeSerializationData用于保存运行时序列化信息。在运行时一张图就转变为一个List数据。
这个NodeSerializationData数据由NodeBase提供一个虚方法,并由各个具体的子类继承实现。

1
2
3
4
public virtual NodeSerializationData GetSerializationData()
{
return new NodeSerializationData(nodeData);
}

窗口添加保存按钮

在对应的windows窗口中添加一个ToolBar并在其中添加一个按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void AddToolBar()
{
Toolbar toolbar = new Toolbar();
//// 设置工具栏样式(确保可见)
//toolbar.style.height = 30;
//// 关键修复:设置工具栏宽度为100%
//toolbar.style.width = Length.Percent(100);

//// 设置背景色使其可见
//toolbar.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f);
Button saveBtn = new Button();
saveBtn.text = "保存";
saveBtn.clicked += SaveBtnClick;
toolbar.Add(saveBtn);

rootVisualElement.Add(toolbar);
}

private void SaveBtnClick()
{
if (m_graphView == null)
{
return;
}

m_graphView.SaveData();
}

核心逻辑为调用GraphView中的SaveData接口,在里面遍历所有的Nodebase的获取序列化数据的接口,并且获得数据都存入list容器中最后写入文件。
我这里的处理办法比较简单粗暴,用了NewtonJson去序列化每个Node信息后直接使用|去分割每个Node的信息后序列化成了string保存起来。可以使用protobuf等二进制数据去序列化,进一步地优化文件的体积大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public virtual void SaveData()
{
List<NodeSerializationData> nodeSaveData = new List<NodeSerializationData>();
foreach (var node in nodes)
{
if (node == null)
{
continue;
}

if (node is NodeBase nodeBase)
{
nodeSaveData.Add(nodeBase.GetSerializationData());
}
}

if (nodeSaveData.Count <= 0)
{
Debug.Log("Graph的Node数量为0!");
return;
}

// string data = MergeUnsafe(nodeSaveData);
var data = MergeNodeData(nodeSaveData);
GraphEditorUnility.SaveDataToBytes(data, m_graphData.graphName);
}

private string MergeNodeData(List<NodeSerializationData> nodeData)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < nodeData.Count; i++)
{
string data = nodeData[i].Serialize();
//Debug.Log(data);
sb.Append(data);
if (i < nodeData.Count - 1)
{
sb.Append("|");
}
}

return sb.ToString();
}

结果

因为是继承Unity的GraphView和Node,所以现在已经能自由的添加/删除连接、添加/删除节点了。节点编辑器的基本功能已经实现完成了。