编辑器数据类

要做的节点如下图所示
NodePreview
根据图片可以很清晰的知道,数据分为4部分。
1.要有一个类存储着所有的节点数据。
2.每个节点需要保存自己的类型名字(用于反射生成)、节点唯一ID(用于标识节点)、标题、节点位置、输出/输出端口的数据、以及一个Object类型的每个子类自己的数据。
3.端口数据记录端口所在节点ID、端口唯一ID以及连接的数据。
4.连接数据记录着连接的节点ID和端口。
我的实现使用了Unity的ScriptableObject。数据结构的图如下。
GraphNodeData

GraphSoData 黑板数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Serializable]
public class GraphSoData : ScriptableObject
{
[SerializeField] public string graphName;

[SerializeField] public List<NodeSoData> nodeSoDataList = new List<NodeSoData>();

public void Dispose()
{
if (nodeSoDataList != null)
{
nodeSoDataList.Clear();
nodeSoDataList = null;
}
}
}

Node 节点数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Serializable]
public class NodeSoData : ScriptableObject
{
[SerializeField] public string nodeFullTypeName;

[SerializeField] public long nodeID;

[SerializeField] public Vector2 nodePos;

[SerializeField] public string nodeTitle;

[SerializeField] public List<PortSerializationData> intputPortDataList;

[SerializeField] public List<PortSerializationData> outputPortDataList;

[SerializeReference] public System.Object userData;
}

port 端口数据

其中端口数据需要记录该端口自己的端口唯一ID、所属的节点ID、以及连接的信息。其中连接的信息保存的就是连接的节点与连接的端口。

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
[Serializable]
public class LinkData
{
[SerializeField] public long linkPortID;
[SerializeField] public long linkNodeID;

public LinkData(long linkPortID, long linkNodeID)
{
this.linkPortID = linkPortID;
this.linkNodeID = linkNodeID;
}
}

[Serializable]
public class PortSerializationData
{
[SerializeField]
//端口ID
public long portID;

[SerializeField]
//端口所属节点ID
public long portNodeID;

[SerializeField]
//连接的端口ID
public List<LinkData> linkData;
}

NodeBase 编辑器节点基类

该Node类做的是显示工作,该类继承于UnityEditor.Experimental.GraphView.Node。类中包含了NodeGraphView黑板实例、NodeSoData此Node的节点数据,以及节点大小等数据。下面是一些流程函数的说明。

初始化

拿到节点数据和黑板实例后,流程先初始化数据,后初始化组件,最后刷新节点。比较简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public virtual void Init(NodeSoData nodeData, NodeGraphView nodeGraphView)
{
this.nodeGraphView = nodeGraphView;
this.nodeData = nodeData;
InitData();
InitComponent();
Refresh();
}

public virtual void Init(NodeGraphView nodeGraphView, bool isStartNode)
{
this.nodeGraphView = nodeGraphView;
nodeData = GraphEditorUnility.CreateDefaultNodeData(isStartNode);
InitData();
InitComponent();
Refresh();
}

初始化组件

这个函数比较关键,所有节点的操作都在这个函数中初始化了。拿节点数据设置位置、标题,遍历。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
protected virtual void InitComponent()
{
AddStyle();
SetPosition(new Rect(nodeData.nodePos, Vector2.zero));

InitTitleLabel();

InitPort();

AddResizeHandle();
}

private void InitTitleLabel()
{
TextField textField = new TextField()
{
value = nodeData.nodeTitle
};

titleContainer.Insert(0, textField);
}

public virtual void InitPort()
{
//初始化输入端口
if (nodeData.intputPortDataList != null && nodeData.intputPortDataList.Count > 0)
{
foreach (var port in nodeData.intputPortDataList)
{
InstantiatePort(port,nodeData.intputPortDataList,Direction.Input,inputContainer);
}
}

//初始化输出端口
if (nodeData.outputPortDataList != null && nodeData.outputPortDataList.Count > 0)
{
foreach (var port in nodeData.outputPortDataList)
{
InstantiatePort(port,nodeData.outputPortDataList,Direction.Output,outputContainer);
}
}
}

//创建节点的端口实例
private Port InstantiatePort(PortSerializationData portData, List<PortSerializationData> portDataList,Direction direction, VisualElement portContainer)
{
// 创建一个水平布局的容器来放置删除按钮和端口
var horizontalContainer = new VisualElement();
horizontalContainer.style.flexDirection = FlexDirection.RowReverse; // 设置为水平排列
horizontalContainer.style.alignItems = Align.Center; // 垂直居中对齐

Port newPort = InstantiatePort(Orientation.Horizontal, direction, Port.Capacity.Multi, typeof(bool));
newPort.name = string.Empty;
newPort.userData = portData;

// Button deleteButton = new Button();
// deleteButton.text = "X";
// deleteButton.clicked += Click;
//
// horizontalContainer.Add(deleteButton);
horizontalContainer.Add(newPort);

portContainer.Add(horizontalContainer);

// void Click()
// {
// portDataList.Remove(portData);
// nodeGraphView.DisconnectPort(newPort);
// deleteButton.clicked -= Click;
//
// portContainer.Remove(horizontalContainer);
// }

return newPort;
}

鼠标拖动节点大小

其中AddResizeHandle函数是添加了节点宽度水平拉伸。为了做到鼠标移动到右边界改变鼠标样式,所以在节点的右侧,添加一个透明的VisualElement,并且高度占满节点,宽度为5px(可以根据喜好自行修改),其中鼠标样式需要添加到uss文件中。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//uss代码,设置一个水平的鼠标样式
.resize-handle
{
cursor: resize-horizontal;

}

protected virtual void AddResizeHandle()
{
resizeHandle = new VisualElement();
resizeHandle.style.position = Position.Absolute;
resizeHandle.style.right = 0;
resizeHandle.style.top = 0;
resizeHandle.style.bottom = 0;
resizeHandle.style.width = 10;
resizeHandle.style.backgroundColor = Color.clear;
resizeHandle.AddToClassList("resize-handle");

Add(resizeHandle);

resizeHandle.RegisterCallback<MouseDownEvent>(OnResizeHandleMouseDown);
resizeHandle.RegisterCallback<MouseMoveEvent>(OnResizeHandleMouseMove);
resizeHandle.RegisterCallback<MouseUpEvent>(OnResizeHandleMouseUp);
}

//鼠标按下回调,添加一个绿框样式
private void OnResizeHandleMouseDown(MouseDownEvent evt)
{
if (evt.button == 0) //
{
isResizing = true;
resizeStartPosition = evt.mousePosition;
resizeStartSize = new Vector2(resolvedStyle.width, resolvedStyle.height);

style.borderTopWidth = 2;
style.borderBottomWidth = 2;
style.borderLeftWidth = 2;
style.borderRightWidth = 2;
style.borderTopColor = Color.green;
style.borderBottomColor = Color.green;
style.borderLeftColor = Color.green;
style.borderRightColor = Color.green;


resizeHandle.CaptureMouse();
evt.StopPropagation();
}
}

//鼠标按下移动,一直改变节点大小
private void OnResizeHandleMouseMove(MouseMoveEvent evt)
{
if (isResizing && resizeHandle.HasMouseCapture())
{
Vector2 delta = evt.mousePosition - resizeStartPosition;
Vector2 newSize = resizeStartSize + new Vector2(delta.x, delta.y);


newSize.x = Mathf.Clamp(newSize.x, minSize.x, maxSize.x);
newSize.y = Mathf.Clamp(newSize.y, minSize.y, maxSize.y);


style.width = newSize.x;

RefreshPorts();
evt.StopPropagation();
}
}

//鼠标抬起设置大小
private void OnResizeHandleMouseUp(MouseUpEvent evt)
{
if (isResizing && evt.button == 0)
{
isResizing = false;
resizeHandle.ReleaseMouse();
evt.StopPropagation();

style.borderTopWidth = StyleKeyword.Null;
style.borderBottomWidth = StyleKeyword.Null;
style.borderLeftWidth = StyleKeyword.Null;
style.borderRightWidth = StyleKeyword.Null;
style.borderTopColor = StyleKeyword.Null;
style.borderBottomColor = StyleKeyword.Null;
style.borderLeftColor = StyleKeyword.Null;
style.borderRightColor = StyleKeyword.Null;

OnSizeChanged();
}
}

StartNode开始节点

此节点继承于NodeBase,开始节点是每个GraphView中必备的,表示从这个节点开始往下执行。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//节点数据
[Serializable]
public class StartNodeData : NodeSerializationData
{
[SerializeField]
public bool isStartNode;

}

//开始节点
public class StartNode : NodeBase
{
public StartNodeData startNodeData;


private Toggle checkBox;

protected override void InitData()
{

if (nodeData.userData != null)
{
startNodeData = nodeData.userData as StartNodeData;
}
else
{
startNodeData = new StartNodeData();
startNodeData.isStartNode = true;
}

}

protected override void InitComponent()
{
base.InitComponent();

var horizontalContainer = new VisualElement();
horizontalContainer.style.flexDirection = FlexDirection.Row; // 设置为水平排列
horizontalContainer.style.alignItems = Align.Center; // 垂直居中对齐

checkBox = new Toggle();
checkBox.SetEnabled(false);
checkBox.value = startNodeData.isStartNode;

var label = new Label("初始节点");

horizontalContainer.Add(checkBox);
horizontalContainer.Add(label);

extensionContainer.Add(horizontalContainer);
}

public override NodeSerializationData GetSerializationData()
{
StartNodeData serializationData = new StartNodeData();
serializationData.isStartNode = true;
serializationData.nodeID = nodeData.nodeID;
serializationData.outputPortDataList = nodeData.outputPortDataList;
serializationData.intputPortDataList = nodeData.intputPortDataList;
return serializationData;
}
}

创建GraphView的So数据

这里用的是Unity编辑器的菜单项,创建了一个So数据,并往里塞一个开始节点的默认数据。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
[MenuItem("★Tools★/CraeteGraph")]
public static void CreateGraph()
{
GraphEditorUnility.CreateDefaultGraph();
}

public static void CreateDefaultGraph()
{

GraphSoData graph = ScriptableObject.CreateInstance<GraphSoData>();

int soFileCount = CountFilesExcludingMeta(DEFAULT_FILE_PATH);
string defaultFileName = string.Format(DEFAULT_FILE_NAME, soFileCount);
string defaultFilePath = System.IO.Path.Combine(DEFAULT_FILE_PATH, defaultFileName);

graph.graphName = string.Format(DEFAULT_FILE_SAVE_NAME, soFileCount);


AssetDatabase.CreateAsset(graph, defaultFilePath);


NodeSoData defaultNode = CreateDefaultNodeData(true);


graph.nodeSoDataList.Add(defaultNode);
AssetDatabase.AddObjectToAsset(defaultNode, graph);

EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(defaultNode);
AssetDatabase.SaveAssets();
}

public static NodeSoData CreateDefaultNodeData(bool isStartNode)
{
NodeSoData defaultSoData = ScriptableObject.CreateInstance<NodeSoData>();

defaultSoData.name = "node";
defaultSoData.nodeID = GraphUtility.GenerateID();
defaultSoData.nodeTitle = "New Node";
defaultSoData.nodePos = new Vector2(200, 200);
//起始节点加入输出node
if (isStartNode)
{
defaultSoData.nodeFullTypeName = typeof(StartNode).FullName;
StartNodeData startNodeData = new StartNodeData();
startNodeData.isStartNode = true;
defaultSoData.userData = startNodeData;
defaultSoData.AddOutputPortData();
}
else
{
defaultSoData.nodeFullTypeName = typeof(NodeBase).FullName;
}
defaultSoData.outputPortDataList = new List<PortSerializationData>();
defaultSoData.intputPortDataList = new List<PortSerializationData>();

return defaultSoData;
}

结果

可以在菜单项创建新的节点数据,并且双击打开能看到一个开始节点。
NodeResult