编辑器数据类
要做的节点如下图所示

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

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] public long portID;
[SerializeField] public long portNodeID;
[SerializeField] 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; horizontalContainer.Add(newPort); portContainer.Add(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
| .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); 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; }
|
结果
可以在菜单项创建新的节点数据,并且双击打开能看到一个开始节点。
