抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

通过 Attribute 标签简化节点编辑器开发。

支持选中描边,自定义节点大小,未手动修改大小的时候自适应节点大小。

支持打开、保存、关闭节点图。支持 Ctrl+S 保存当前节点图。

本文只阐述原理,具体的使用方法请见说明文档 GraphViewExtension 文档

演示视频

节点操作

文件操作

原理

节点

UI

节点的 UI 初始化是通过给字段打 Attribute 标签,然后在创建节点的时候进行反射初始化。

因为 GraphView 用的是 UI ToolKit 的体系,所以所有的 UI 都是 VisualElement 节点。该体系和 Editor 的 UI 不同,因此此处不套用 EditorUIExtension 的初始化方式。

为了保持 UI 初始化的一致性,本项目采用 VisualElement 作为一个 UI 的父容器,里面再添加具体的 UI。这样在 UI 构造为 标签 + 输入框 的类型的时候,就可以在这个父容器里面进行横向排布而不影响到最上层的容器。

注意:父容器方向默认是竖向排列,需要自己设置样式为横向

graph LR;
    table_1["父容器
标签输入框
"]

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case NodeTypeEnum.Input:
ele.style.flexDirection = FlexDirection.Row;

//foreLabel 为在 switch 之外定义的辅助标签,在此处代码省略定义过程
foreLabel = new Label();
foreLabel.style.fontSize = _fontSize;
foreLabel.text = subName;
ele.Add(foreLabel);

TextField text = new TextField();
text.style.flexGrow = 1;
text.style.height = _fontSize * 1.2f;

var fontChild = text.Children().FirstOrDefault().Children().FirstOrDefault();
fontChild.style.fontSize = _fontSize;

text.RegisterValueChangedCallback(evt => { setValue(this, evt.newValue); });
ele.Add(text);
break;

同时为了能够把自定义数量的 UI 框在同一个矩形里,额外定义一种 Box 类型的 UI,在该 UI 初始化的时候,会创建一个新的顶层容器来容纳 UI。容纳一定数量的 UI 之后(Box 设置的数量),下一次创建 UI 会再新建一个顶层容器,后面的 UI 都在这个新的容器里容纳。

graph TD;
    table_1["默认顶层容器
UI1
UI2
UI3
"] table_2["Box 创建的顶层容器
UI4
UI5
"] table_3["Box 之后的新顶层容器
UI6
UI7
UI8
"] table_1 --> table_2 table_2 --> table_3

具体的实现如下:

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
//ui box 分割。没有 box 标签的情况下都在默认的第一个 box 中添加 ui
int boxCount = -1;
var box = new VisualElement();
//extensionContainer 为所有顶层容器的父容器
extensionContainer.Add(box);
foreach (var field in fields)
{
GraphNode attr = field.GetCustomAttribute<GraphNode>();

if (attr != null)
{
// 判断是否需要增加 ui box
if (boxCount == 0)
{
box = new VisualElement();
extensionContainer.Add(box);
}

if (boxCount > -1)
{
boxCount--;
}
switch (attr.Type()){
case NodeTypeEnum.Box:
box = new VisualElement();
extensionContainer.Add(box);
boxCount = int.Parse(extra[0]);
// 此处省略 box 样式设置
break;
}
}
}

通过 boxCount 来计数,有新的 Box 的时候才会赋值并且执行判断和自减,自减到 0 代表完全填入足量的 UI,这时候就需要新建一个 VisualElement 来装 Box 外的 UI。

如果以上方法无法满足你的 UI 构建需要的话,通过重写 CustomUI 来实现客制化 UI。

数据

节点内设有一个 ExpandoObject 类型字段 _data 来存储数据。

ExpandoObject 是个 dynamic 类型,可以通过点来设置和读取属性。

例如:

1
2
3
dynamic obj = new ExpandoObject();
obj.a = 1;
obj.b = "str";

因此我们可以利用这个动态类型来保存任意类型的数据。

保存数据

除了当前节点的数据,我们还需要保存节点的关系。因此我们用树来存储。

我们定义一个简单的数据节点类 GDataNode ,该类保存数据和子节点列表。

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
using System.Collections.Generic;

namespace GraphViewExtension
{
public class GDataNode
{
private List<GDataNode> _children = new List<GDataNode>();

private dynamic _data;

private string _type;

public void SetData(dynamic data)
{
_data = data;
}

public void SetNodeType(string type)
{
_type = type;
}

public void AddChild(GDataNode node)
{
if (!_children.Contains(node))
{
_children.Add(node);
}
}

public List<GDataNode> GetChildren()
{
return _children;
}

public dynamic GetData()
{
return _data;
}

public string GetNodeType()
{
return _type;
}
}
}

然后我们在保存数据的时候,通过遍历 _outputPortconnections ,也就是输出节点的连接,递归地保存到本节点的 GDataNode 中。

具体的流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// 保存数据
/// </summary>
/// <returns></returns>
public GDataNode SaveData()
{
// 保存基本数据
_data.guid = guid;
_data.pos = _defPos.ToString();
_data.size = (_curSize.Equals(Vector2.zero) ? _defSize : _curSize).ToString();
_dataNode.SetNodeType(_type.FullName);
// 保存额外数据,用户自定义
SetData();
_dataNode.SetData(_data);
// 遍历子节点
var connections = _outputPort.connections;
foreach (var edge in connections)
{
RootNode node = edge.input.node as RootNode;
_dataNode.AddChild(node?.SaveData());
}

return _dataNode;
}

其中 SetData 是用户自定义保存数据的方法。

读取数据

和用户自定义保存数据一样,我们读取数据的时候也需要自己实现数据的初始化。具体的函数为 ResetData

节点尺寸

本项目允许用户通过拖拽节点右下角进行节点尺寸的修改。

尺寸分为两个部分:

  • _defSize ,用于存储最小的大小。
  • _curSize ,用于存储当前大小。

当手动修改过大小后,部分功能就读取 _curSize ,否则读取 _defSize 的数据。

主要的原理就是监听鼠标的按下、移动和抬起事件,然后进行相应的操作。

鼠标按下

鼠标按下事件是在节点内监听的。

鼠标按下的时候我们判断鼠标在节点的坐标是否在节点右下角的范围内,本项目给了 10 个像素的空余。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 节点尺寸调节鼠标按下回调
/// </summary>
/// <param name="evt"></param>
private void ResizeStart(MouseDownEvent evt)
{
if (evt.button == 0)
{
// 判断改大小
if (IsResizeArea(evt.localMousePosition))
{
_mouseOffset = evt.mousePosition - layout.size;
_isResizing = true;
evt.StopPropagation();
}
}
}

evtlocalMousePosition 是鼠标在当前节点的坐标,以节点左上角为 (0,0)。

mousePosition 是鼠标的世界坐标,layout.size 是当前节点的尺寸。通过相减得到当前节点左上角的世界坐标。

鼠标移动

鼠标移动事件是在 GraphView 监听的。如果在节点监听,则在鼠标移出节点的时候会接收不到事件。

鼠标移动的时候,如果是尺寸修改状态,则计算新的大小并更新。计算大小的时候会和最小大小进行比较选择其中较大的一方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 节点尺寸调节鼠标移动回调
/// </summary>
/// <param name="evt"></param>
private void ResizeMove(MouseMoveEvent evt)
{
if (_isResizing)
{
Vector2 newSize = evt.mousePosition - _mouseOffset;
newSize = Vector2.Max(_defSize, newSize);
// 更新节点的大小
UpdateSize(newSize);

evt.StopPropagation();
}
}
鼠标抬起

鼠标抬起事件是在 GraphView 监听的。如果在节点监听,则在鼠标移出节点的时候会接收不到事件。

鼠标抬起的时候置尺寸调节状态为 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 节点尺寸调节鼠标抬起回调
/// </summary>
/// <param name="evt"></param>
private void ResizeEnd(MouseUpEvent evt)
{
// 尺寸调节部分的代码
if (_isResizing)
{
Debug.Log(layout.size);
_isResizing = false;
evt.StopPropagation();
}

// 描边用的值
_defPos = layout.position + new Vector2(_borderOffset * 0.5f, _borderOffset * 0.5f);
}

描边

本项目支持在点击节点的时候对节点进行描边,如果节点有前置节点,则前置节点也会描边,这样可以清楚地看到该节点的路线。

点击描边是在 GraphView 监听的,具体的逻辑请看 [图](# 图) 部分。

描边的实现

描边的实现很简单,只需要修改节点的 styleborder 相关的样式就可以。

由于描边的时候 VisualElement 是内描边,因此我们需要扩大节点的大小。

扩大大小的时候需要用 _curSize_defSize 加上描边的 ** 宽度 *2**,该值本项目存储为 _borderOffset 。还原的时候就把节点尺寸设为 _curSize_defSize 即可。

位置偏移

因为我们修改了节点大小,因此节点会产生视觉上的偏移(节点内大小不变,扩大的部分为描边,整个节点左上角坐标不变,因此视觉上会往右下偏移一点距离,距离为描边宽度)。

所以我们要对这个偏移进行修正。

修正的原理也很简单,只需要在描边的时候往左上移动描边宽度的距离,取消描边的时候把节点位置还原即可。

因此我们需要记录节点的原始坐标 _defPos

这里还有一个额外情况,由于本项目的 备注 UI 会在没手动调整节点尺寸的情况下自动更新节点尺寸,因此会在描边的时候造成重复修正偏移的情况,因此增加一个标识 _lastBordered 判断是否修正过坐标。

具体的实现如下:

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
/// <summary>
/// 描边
/// </summary>
private void DrawBorder()
{
if (_isBordered)
{
if (_curSize.Equals(Vector2.zero))
{
style.width = _defSize.x + _borderOffset;
style.height = _defSize.y + _borderOffset;
}
else
{
style.width = _curSize.x + _borderOffset;
style.height = _curSize.y + _borderOffset;
}

_defPos = new Vector2(style.left.value.value, style.top.value.value);

// 如果没描过边,则调整节点坐标位置以防止错位
if (_lastBordered != _isBordered)
{
style.left = _defPos.x - _borderOffset * 0.5f;
style.top = _defPos.y - _borderOffset * 0.5f;
}
}
else
{
if (_curSize.Equals(Vector2.zero))
{
style.width = _defSize.x;
style.height = _defSize.y;
}
else
{
style.width = _curSize.x;
style.height = _curSize.y;
}

// 还原节点默认位置
style.left = _defPos.x;
style.top = _defPos.y;
}

_lastBordered = _isBordered;
}

图(GraphView)

菜单

本项目在创建图的时候引入自定义的多级搜索菜单,并在菜单中注册选择监听事件。具体逻辑见 [多级搜索菜单](# 多级搜索菜单) 项。

然后我们注册图的 nodeCreationRequest 委托,使其在创建节点的时候打开该菜单。

1
2
3
4
5
6
7
8
9
10
11
12
public GGraph(EditorWindow editorWindow, GSearchWindow provider)
{
// 监听创建节点
nodeCreationRequest += (context) =>
{
// 打开搜索框
SearchWindow.Open(
new SearchWindowContext(context.screenMousePosition),
provider
);
};
}

监听事件

本项目除了 Unity 提供的基础的滚轮缩放、窗口拖动、选中节点移动、多节点框选功能外,额外监听了鼠标按下、鼠标移动、点击和按键抬起。

基础功能
1
2
3
4
5
6
7
8
9
10
11
public GGraph(EditorWindow editorWindow, GSearchWindow provider)
{
// 滚轮缩放
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
// 窗口内容拖动
this.AddManipulator(new ContentDragger());
// 选中 Node 移动功能
this.AddManipulator(new SelectionDragger());
// 多个 node 框选功能
this.AddManipulator(new RectangleSelector());
}
点击描边

其中监听鼠标按下、鼠标移动和点击是为了实现节点点击描边的功能。

其中鼠标移动是为了判断节点是否产生了拖拽,在拖拽的时候不产生描边。鼠标按下是为了判断是否对节点进行了操作,否则只监听移动的话不管鼠标是否按下都会判定。

描边判断的主要逻辑在点击监听内,具体逻辑如下。

flowchart TB;

    A["获取点击的 UI"] --> B["判断是否为空"]

    B --> C["为空,取消原节点描边"]
    B --> D["不为空,循环判断节点或节点的父节点是否为 RootNode"]

    D --> E["不是,取消原节点描边"]
    D --> F["是,判断节点或节点的父节点是否可交互"]

    F --> G["是,不做任何操作"]
    F --> H["不是,判断原节点是否存在并且当前节点不等于原节点并且不是 RootNode"]

    H --> I["是,取消原节点描边"]
    H --> J["不是,描边当前节点,并保存"]

代码如下:

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
public GGraph(EditorWindow editorWindow, GSearchWindow provider)
{
RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button == 0)
{
_isDown = true;
}
});

RegisterCallback<PointerMoveEvent>(evt =>
{
if (_isDown)
{
_isMoved = true;
}
});

// 监听点击的节点
RegisterCallback<ClickEvent>(evt =>
{
if (!_isMoved)
{
Vector2 localMousePosition = evt.position - new Vector3(layout.xMin, layout.yMin);
// 获取当前被选中的对象
var currentSelection = panel.Pick(localMousePosition);

if (currentSelection == null)
{
if (_clickNode != null)
{
_clickNode.UnSelected();
_clickNode = null;
}
}
else
{
bool isInput = false;
while (currentSelection != null && currentSelection is not RootNode)
{
if (IsInputField(currentSelection))
{
isInput = true;
break;
}
currentSelection = currentSelection.parent;
}

if (currentSelection == null)
{
if (_clickNode != null)
{
_clickNode.UnSelected();
_clickNode = null;
}
}
else
{
if (isInput) return;

if (_clickNode != null && currentSelection != _clickNode && currentSelection is not RootNode)
{
_clickNode.UnSelected();
_clickNode = null;
}
else
{
_clickNode?.UnSelected();
_clickNode = currentSelection as RootNode;
_clickNode?.Selected();
}
}
}
}

_isMoved = false;
_isDown = false;
});
}
组合键监听

监听组合键主要是为了监听 Ctrl+S 来保存文件(焦点需要在 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

public GGraph(EditorWindow editorWindow, GSearchWindow provider){
// 监听保存组合键
RegisterCallback<KeyUpEvent>(OnKeyUp);
}

private void OnKeyUp(KeyUpEvent evt)
{
// 检查是否按下了 Ctrl 键和 S 键
if (evt.keyCode == KeyCode.S && evt.ctrlKey)
{
if (_filePath == "")
{
Save();
return;
}

Debug.Log("Ctrl + S pressed. Saving...");
// 执行保存操作
List<GDataNode> list = SaveData();

List<SaveJson> listJson = new List<SaveJson>();

foreach (var data in list)
{
listJson.Add(ToJson(data));
}

string jsonData = JsonConvert.SerializeObject(listJson);

FileInfo myFile = new FileInfo(_filePath);
StreamWriter sw = myFile.CreateText();

foreach (var s in jsonData)
{
sw.Write(s);
}

sw.Close();
// 这里可以添加你的保存逻辑
evt.StopPropagation (); // 阻止事件传递,避免触发其他事件
}
}

数据存储与读取

数据存储与读取,重点在于数据的序列化与反序列化。

序列化数据

首先我们考虑节点可能并不一定从同一个根节点展开,因此需要保存成一个根节点列表。

由于我们定义的 GDataNode 具有额外的方法,所以我们提取其字段构成新的数据类 SaveJson

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections.Generic;

namespace GraphViewExtension
{
public class SaveJson
{
public List<SaveJson> children = new List<SaveJson>();

public dynamic data;

public string type;
}
}

本项目把数据保存为 Json 格式,利用 Unity 自带的 Newtonsoft.Json 来实现序列化。

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
private void Save()
{
if (_filePath == "")
{
_filePath = EditorUtility.SaveFilePanel("保存到本地", Application.dataPath + "/Json", "NewFile", "json");
}

SetFilePath(_filePath);

List<GDataNode> list = SaveData();

List<SaveJson> listJson = new List<SaveJson>();

foreach (var data in list)
{
listJson.Add(ToJson(data));
}

string jsonData = JsonConvert.SerializeObject(listJson);

FileInfo myFile = new FileInfo(_filePath);
StreamWriter sw = myFile.CreateText();

foreach (var s in jsonData)
{
sw.Write(s);
}

sw.Close();

_editorWindow.ShowNotification(new GUIContent("保存成功,路径为: " + _filePath));
}

/// <summary>
/// 转换为 JSON
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public SaveJson ToJson(GDataNode data)
{
SaveJson save = new SaveJson();
save.type = data.GetNodeType();
save.data = data.GetData();

foreach (var child in data.GetChildren())
{
save.children.Add(ToJson(child));
}

return save;
}
反序列化数据

反序列化同理,我们需要把 SaveJson 转为 GDataNode

由于数据在读取的时候都是 JObject 格式,并且在遍历赋值的时候都是 JProperty 格式,存入 ExpandoObject 的话会失去数据类型,因此我们根据数据的 JTokenType 手动转换为对应的类型。

读取数据还存在一种情况:已经读取过一次数据。

这时候我们就要弹窗提示用户是否重新打开,确定就清空当前内容并打开新的内容,取消就无事发生。

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
private void Open()
{
bool isOpen = GetOpen();
if (isOpen)
{
bool res = EditorUtility.DisplayDialog ("打开新文件", "是否打开一个新的文件,当前内容未保存的部分会消失。", "确定", "取消");
if (res)
{
ClearGraph();
}
else
{
return;
}
}

string filePath = EditorUtility.OpenFilePanel ("打开 ScriptableObject", "Assets/Json", "json");

if (filePath != "")
{
string jsonData = "";

StreamReader sr = File.OpenText(filePath);
while (sr.ReadLine() is { } nextLine)
{
jsonData += nextLine;
}
sr.Close();

List<SaveJson> json = JsonConvert.DeserializeObject<List<SaveJson>>(jsonData);

List<GDataNode> list = new List<GDataNode>();

foreach (var data in json)
{
list.Add(ToGDataNode(data));
}

SetFilePath(filePath);
OpenData(list);
}
}

/// <summary>
/// 转换为节点数据
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public GDataNode ToGDataNode(SaveJson json)
{
GDataNode data = new GDataNode();
data.SetNodeType(json.type);

dynamic obj = new ExpandoObject();

foreach (var res in (json.data as JObject).Properties())
{
JTokenType jType = res.Value.Type;
switch (jType)
{
case JTokenType.String:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<string>();
break;
case JTokenType.Float:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<float>();
break;
case JTokenType.Boolean:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<bool>();
break;
}
}

data.SetData(obj);

foreach (var child in json.children)
{
data.AddChild(ToGDataNode(child));
}

return data;
}

工具栏

工具栏我们可以使用 Unity 提供的 ToolbarToolbarButton 来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected virtual void SetupToolbar()
{
var toolbar = new Toolbar();
var openBtn = new ToolbarButton {text = "打开"};
openBtn.clicked += Open;
var saveBtn = new ToolbarButton {text = "保存"};
saveBtn.clicked += Save;
var closeBtn = new ToolbarButton {text = "关闭"};
closeBtn.clicked += Close;
toolbar.Add(openBtn);
toolbar.Add(saveBtn);
toolbar.Add(closeBtn);
Add(toolbar);
}

多级搜索菜单

多级搜索菜单类继承自 ScriptableObjectISearchWindowProvider

本类定义一个菜单数组 entries ,并且默认传入一个菜单组,也就是一打开显示的界面。

1
2
3
4
public List<SearchTreeEntry> entries = new List<SearchTreeEntry>()
{
new SearchTreeGroupEntry (new GUIContent ("创建节点"))
};

然后我们在构造函数中初始化菜单的配置文件,同样也是通过 Newtonsoft.Json 实现。

1
2
3
4
5
private void InitJson()
{
string json = EditorGUIUtility.Load("Assets/Editor/GraphViewExtension/Graph/Menu.json").ToString();
_menu = JArray.Parse(json);
}

最后我们根据结构创建菜单,其中 level 代表层级,高层级的 level 被低层级的 level 嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void CreateMenu(JToken obj, int level = 1)
{
string menuName = obj["name"].ToString();
string menuType = "GraphViewExtension." + obj["type"];
JToken children = obj["child"];

bool isChild = children?.Count() > 0;

if (isChild)
{
entries.Add(new SearchTreeGroupEntry(new GUIContent(menuName)) { level = level });
foreach (var child in children)
{
CreateMenu(child, level + 1);
}
}
else
{
entries.Add(new SearchTreeEntry(new GUIContent(menuName)) { level = level, userData = Type.GetType(menuType) });
}
}

整体代码如下:

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
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Plastic.Newtonsoft.Json;
using Unity.Plastic.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

namespace GraphViewExtension
{
public class GSearchWindow: ScriptableObject,ISearchWindowProvider
{
public delegate bool SelectHandle(SearchTreeEntry searchTreeEntry,
SearchWindowContext context);

public SelectHandle onSelectEntryHandler;

private bool _inited;

private JArray _menu;

public List<SearchTreeEntry> entries = new List<SearchTreeEntry>()
{
new SearchTreeGroupEntry (new GUIContent ("创建节点"))
};

private void BuildTree()
{
InitJson();

foreach (var token in _menu)
{
CreateMenu(token);
}

_inited = true;
}

private void InitJson()
{
string json = EditorGUIUtility.Load("Assets/Editor/GraphViewExtension/Graph/Menu.json").ToString();
_menu = JArray.Parse(json);
}

private void CreateMenu(JToken obj,int level = 1)
{
string menuName = obj["name"].ToString();
string menuType = "GraphViewExtension." + obj["type"];
JToken children = obj["child"];

bool isChild = children?.Count() > 0;

if (isChild)
{
entries.Add(new SearchTreeGroupEntry(new GUIContent(menuName)){level = level});
foreach (var child in children)
{
CreateMenu(child, level + 1);
}
}
else
{
entries.Add(new SearchTreeEntry(new GUIContent(menuName)){level = level,userData = Type.GetType(menuType)});
}
}

public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
if (!_inited)
{
BuildTree();
}
return entries;
}

public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
if (onSelectEntryHandler == null)
return false;
return onSelectEntryHandler(searchTreeEntry, context);
}
}
}

本项目对节点之间的边进行改动,边上会显示当前连接的子节点相对于父节点的索引。

首先我们创建了自定义的 EdgeCommentEdge

通过重写 UpdateEdgeControl 方法,我们在边更新的时候修正 Label 位置并且检查当前节点相对父节点的索引并更新。

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
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace GraphViewExtension
{
public class CommentEdge : Edge
{
private VisualElement _box;
private Label _commentLabel;

private bool _set = false;

public CommentEdge()
{
// 创建并设置 Label
_box = new VisualElement();
_box.style.backgroundColor = Color.white;
_box.style.borderBottomLeftRadius = 5;
_box.style.borderBottomRightRadius = 5;
_box.style.borderTopLeftRadius = 5;
_box.style.borderTopRightRadius = 5;
_box.style.position = Position.Absolute;

_commentLabel = new Label("");
_commentLabel.style.color = Color.black;

Add(_box);
_box.Add(_commentLabel);
}

public override bool UpdateEdgeControl()
{
if (input?.childCount > 0 && output?.childCount > 0 && hierarchy.parent != null)
{
// 获取GraphView的缩放比例
var scale = hierarchy.parent.worldTransform.m00;

// 计算 Label 的新位置,这里将其放在 Edge 的中间
var midpoint = (input.worldBound.position + output.worldBound.position) / 2;
_box.style.left = (midpoint.x - worldBound.x) / scale + _box.resolvedStyle.width;
_box.style.top = (midpoint.y - worldBound.y) / scale;


SetComment("索引" + GetOutputIndex());
// GetOutputIndex();
}

var res = base.UpdateEdgeControl();
return res;
}

public int GetOutputIndex()
{
if (output != null)
{
RootNode outputNode = output.node as RootNode;
if (outputNode != null)
{
var outputPorts = outputNode.GetOutput().connections;
int outputIndex = 0;

foreach (var port in outputPorts)
{
if (port.Equals(this))
{
break;
}

outputIndex++;
}

return outputIndex;
}
}

return -1;
}

// 设置说明文本的方法
public void SetComment(string comment)
{
_commentLabel.text = comment;
}
}
}

然后我们重写节点类的 InstantiatePort 方法,该方法用于创建接口 Port

1
2
3
4
5
public override Port InstantiatePort(Orientation orientation, Direction direction, Port.Capacity capacity, Type type)
{
var port = Port.Create<CommentEdge>(orientation, direction, capacity, type);
return port;
}

遇到的问题

描边问题

style 的描边是边线宽度,因此在大小不变的情况下会影响原来内容的大小,所以描边后对原大小和位置都做了修正。

输入框文字样式修改

输入框 TextField 本身是由两个元素组成,一个是 Label , 另一个是 TextInput

因此修改输入框样式就不能在 TextField 直接修改。

字体的颜色可以在 TextInput 修改,但是字体大小就要在下一层的 TextElement 修改。

如果允许多行显示的话,TextInput 下面会多一层 VisualElement ,因此为了正确获取修改的对象,前面要多获取一层 UI 元素。

即:单行输入框共三层嵌套,多行输入框共四层嵌套。多行比单行多的一层嵌套在 TextInputTextElement 之间。

点击事件的元素

ClickEvent 返回的 target 不是真正点击到的对象。

因此本项目使用 panel.Pick (Vector2) 来获取点击的目标对象。

GraphView 的鼠标按下事件

GraphView 无法监听 MouseDownEvent 的鼠标左键事件,因此本项目使用 PointerDownEvent 来监听指针事件。

数据反序列化

反序列化一开始没有单独做类型处理,结果返回的都是 JToken 数据,在使用数据的时候还需要手动做转换。后面在反序列化的时候直接处理了数据类型。

代码

GraphNode
GraphNode
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
using System;

namespace GraphViewExtension
{
[AttributeUsage(AttributeTargets.Field)]
public class GraphNode: Attribute
{
private NodeTypeEnum _type;

private string[] _extra;

public GraphNode(NodeTypeEnum type,params string[] extra)
{
_type = type;
_extra = extra;
}

public NodeTypeEnum Type()
{
return _type;
}

public string[] GetExtra()
{
return _extra;
}
}
}
GName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;

namespace GraphViewExtension
{
[AttributeUsage(AttributeTargets.Field)]
public class GName: Attribute
{
private string _name;

public GName(string name)
{
_name = name;
}

public string GetName()
{
return _name;
}
}
}
GColor
GColor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using UnityEngine;

namespace GraphViewExtension
{
[AttributeUsage(AttributeTargets.Field)]
public class GColor: Attribute
{
private Color color;

public GColor(float r,float g,float b,float a = 1)
{
color = new Color(r, g, b, a);
}

public Color GetColor()
{
return color;
}
}
}
GWidth
GWidth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using UnityEngine.UIElements;

namespace GraphViewExtension
{
[AttributeUsage(AttributeTargets.Field)]
public class GWidth: Attribute
{
private Length _length;

public GWidth(float width,LengthUnit type)
{
_length = new Length(width,type);
}

public Length GetLength()
{
return _length;
}
}
}
NodeTypeEnum
NodeTypeEnum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace GraphViewExtension
{
public enum NodeTypeEnum
{
Label,
Input,
Note,
Enum,
Slide,
Toggle,
Radio,
Box,
Color,
Texture
}
}
RootNode
RootNode
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
using System;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using AutoLayout;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace GraphViewExtension
{
public delegate void SetFieldDelegate(object target, object value);

public class RootNode : Node
{
/// <summary>
/// 所属的GraphView
/// </summary>
private GGraph _graph;

Port _inputPort;
Port _outputPort;

/// <summary>
/// 反射范围标记
/// </summary>
private readonly BindingFlags _flag = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static |
BindingFlags.Instance | BindingFlags.DeclaredOnly;

/// <summary>
/// 唯一标识符
/// </summary>
public string guid;

/// <summary>
/// 是否完成渲染初始化
/// </summary>
private bool _inited = false;

/// <summary>
/// 当前类类型
/// </summary>
private Type _type;

/// <summary>
/// 默认尺寸
/// </summary>
private Vector2 _defSize;

/// <summary>
/// 当前尺寸
/// </summary>
private Vector2 _curSize;

/// <summary>
/// 默认位置
/// </summary>
private Vector2 _defPos;

/// <summary>
/// 默认高度
/// </summary>
private float _defHeight;

/// <summary>
/// 鼠标按下时坐标
/// </summary>
private Vector2 _mouseOffset;

/// <summary>
/// 是否正在调整大小
/// </summary>
private bool _isResizing = false;

/// <summary>
/// 是否已描边
/// </summary>
private bool _isBordered = false;

/// <summary>
/// 之前的状态是否描边
/// </summary>
private bool _lastBordered = false;

/// <summary>
/// 默认字体尺寸
/// </summary>
private int _fontSize = 16;

/// <summary>
/// 描边补正
/// </summary>
private float _borderOffset = 0;

/// <summary>
/// 是否执行到当前节点
/// </summary>
private bool _isRun = false;

/// <summary>
/// 是否选中当前节点
/// </summary>
private bool _isSelected = false;

//----- 描边颜色 ------ start

protected Color _selectColor = Color.red;

protected Color _parentColor = Color.green;

protected Color _runColor = Color.green;

protected Color _passColor = Color.yellow;

//----- 描边颜色 ------ end

/// <summary>
/// 节点数据
/// </summary>
protected dynamic _data = new ExpandoObject();

/// <summary>
/// 是否设置了数据
/// </summary>
protected bool _isSet = false;

/// <summary>
/// 数据节点
/// </summary>
GDataNode _dataNode = new GDataNode();

public RootNode()
{
title = "默认节点";
_type = GetType();

//生成唯一标识
guid = GUID.Generate().ToString();

//添加输入端口
_inputPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(Node));
_inputPort.portName = "Parent";
inputContainer.Add(_inputPort);
//添加输出端口
_outputPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(Node));
_outputPort.portName = "Child";
outputContainer.Add(_outputPort);

//注册鼠标按下监听
RegisterCallback<MouseDownEvent>(ResizeStart);
//注册界面改变监听
RegisterCallback<GeometryChangedEvent>(OnEnable);
}

public override Port InstantiatePort(Orientation orientation, Direction direction, Port.Capacity capacity,
Type type)
{
var port = Port.Create<CommentEdge>(orientation, direction, capacity, type);
return port;
}

/// <summary>
/// 界面改变回调
/// </summary>
/// <param name="evt"></param>
private void OnEnable(GeometryChangedEvent evt)
{
Debug.Log("创建完成,开始渲染 " + layout.size);
//保存默认高度
_defHeight = titleContainer.layout.height + _outputPort.layout.height;
//保存默认位置
_defPos = layout.position;

//如果设置了数据(即通过打开文件创建的节点),则把数据填充到UI
if (_isSet)
{
ResetData();
}

//初始化UI
Init();
CustomUI();

//延迟更新节点大小
schedule.Execute(() =>
{
UpdateNodeSize();
_inited = true;
}).StartingIn(1);

//刷新 不然会有显示BUG
RefreshExpandedState();
RefreshPorts();

//注销界面改变监听,使该方法只执行一次
UnregisterCallback<GeometryChangedEvent>(OnEnable);
}

/// <summary>
/// 设置数据
/// </summary>
/// <param name="data"></param>
public void SetData(ExpandoObject data)
{
_data = data;
_isSet = true;
}

/// <summary>
/// 设置尺寸,该方法会保存默认尺寸
/// </summary>
/// <param name="size"></param>
public void SetSize(Vector2 size)
{
style.width = size.x;
style.height = size.y;
_defSize = size;
}

public void SetCurSize(Vector2 size)
{
if (_curSize.Equals(Vector2.zero))
{
_curSize.x += _defSize.x + size.x;
_curSize.y += _defSize.y + size.y;
}
else
{
_curSize.x += size.x;
_curSize.y += size.y;
}

_curSize = Vector2.Max(_curSize, _defSize);
}

/// <summary>
/// 在GraphView注册鼠标移动和抬起事件
/// 用于节点界面尺寸的修改
/// 在GraphView是为了能够在节点外响应事件
/// </summary>
/// <param name="graph"></param>
public void RegistResize(GGraph graph)
{
_graph = graph;
_graph.RegisterCallback<MouseMoveEvent>(ResizeMove);
_graph.RegisterCallback<MouseUpEvent>(ResizeEnd);
}

/// <summary>
/// 获取输入端口
/// </summary>
/// <returns></returns>
public Port GetInput()
{
return _inputPort;
}

/// <summary>
/// 获取输出端口
/// </summary>
/// <returns></returns>
public Port GetOutput()
{
return _outputPort;
}

/// <summary>
/// 设置运行状态,请在执行到该节点时调用
/// </summary>
/// <param name="run"></param>
public void SetRun(bool run)
{
if (run)
{
_isRun = true;
SetBorder(_runColor, 5);
}
else
{
if (_isRun)
{
SetBorder(_passColor, 5);
}
else
{
SetBorder(Color.white, 0);
}

_isRun = false;
}
}

/// <summary>
/// 清除所有运行状态
/// </summary>
public void ClearRunState()
{
//执行两次确保完全清除
SetRun(false);
SetRun(false);
//遍历节点
var connections = _outputPort.connections;
foreach (var edge in connections)
{
RootNode node = edge.input.node as RootNode;
node?.ClearRunState();
}
}

/// <summary>
/// 选中节点描边
/// </summary>
/// <param name="leaf"></param>
public void Selected(RootNode leaf = null)
{
if (leaf == null)
{
SetBorder(_selectColor, 5);
}
else
{
SetBorder(_parentColor, 5);
}

//遍历节点
var connections = _inputPort.connections;
foreach (var edge in connections)
{
RootNode node = edge.output.node as RootNode;
node?.Selected(node);
}
}

/// <summary>
/// 取消选中节点描边
/// </summary>
public void UnSelected()
{
SetBorder(_selectColor, 0);
//遍历节点
var connections = _inputPort.connections;
foreach (var edge in connections)
{
RootNode node = edge.output.node as RootNode;
node?.UnSelected();
}
}

/// <summary>
/// 判断是否有前置节点
/// </summary>
/// <returns></returns>
public bool HasParent()
{
return _inputPort.connections.Any();
}

/// <summary>
/// 保存数据
/// </summary>
/// <returns></returns>
public GDataNode SaveData()
{
//保存基本数据
_data.guid = guid;
_data.pos = _defPos.ToString();
_data.size = (_curSize.Equals(Vector2.zero) ? _defSize : _curSize).ToString();
_dataNode.SetNodeType(_type.FullName);
//保存额外数据,用户自定义
SetData();
_dataNode.SetData(_data);
//遍历子节点
var connections = _outputPort.connections;
foreach (var edge in connections)
{
RootNode node = edge.input.node as RootNode;
_dataNode.AddChild(node?.SaveData());
}

return _dataNode;
}

/// <summary>
/// 设置描边颜色,最好是在所有初始化完成后再调用
/// </summary>
/// <param name="color"></param>
private void SetBorder(Color color, float width)
{
style.borderBottomWidth = width;
style.borderBottomColor = color;
style.borderLeftWidth = width;
style.borderLeftColor = color;
style.borderRightWidth = width;
style.borderRightColor = color;
style.borderTopWidth = width;
style.borderTopColor = color;
style.borderBottomLeftRadius = 10;
style.borderBottomRightRadius = 10;
style.borderTopLeftRadius = 10;
style.borderTopRightRadius = 10;

_borderOffset = width * 2;

if (_inited)
{
UpdateBorderSize();
}
}

/// <summary>
/// 移除连线
/// </summary>
public void RemoveEdges()
{
foreach (var edge in _outputPort.connections)
{
_graph.RemoveElement(edge);
}
}

/// <summary>
/// 节点尺寸调节鼠标按下回调
/// </summary>
/// <param name="evt"></param>
private void ResizeStart(MouseDownEvent evt)
{
if (evt.button == 0)
{
//判断改大小
if (IsResizeArea(evt.localMousePosition))
{
_mouseOffset = evt.mousePosition;
_isResizing = true;
evt.StopPropagation();
}
}
}

/// <summary>
/// 节点尺寸调节鼠标移动回调
/// </summary>
/// <param name="evt"></param>
private void ResizeMove(MouseMoveEvent evt)
{
if (_isResizing)
{
var scale = hierarchy.parent.worldTransform.m00;

Vector2 newSize = (evt.mousePosition - _mouseOffset) / scale;

// 更新节点的大小
UpdateSize(newSize);

evt.StopPropagation();
}
}

/// <summary>
/// 节点尺寸调节鼠标抬起回调
/// </summary>
/// <param name="evt"></param>
private void ResizeEnd(MouseUpEvent evt)
{
if (_isResizing)
{
var scale = hierarchy.parent.worldTransform.m00;

Vector2 newSize = (evt.mousePosition - _mouseOffset) / scale;
SetCurSize(newSize);

_isResizing = false;
MarkDirtyRepaint();
evt.StopPropagation();
}

_defPos = layout.position + new Vector2(_borderOffset * 0.5f, _borderOffset * 0.5f);
}

/// <summary>
/// 是否在节点右下角区域
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
private bool IsResizeArea(Vector2 position)
{
// 根据需要定义resizer区域,这里以右下角为例
return position.x >= layout.width - 10f - _borderOffset &&
position.y >= layout.height - 10f - _borderOffset;
}

/// <summary>
/// 初始化UI
/// </summary>
private void Init()
{
InitConfig();

//添加GUID
Label lbGuid = new Label(guid);
lbGuid.style.position = Position.Absolute;
lbGuid.style.top = -20;
lbGuid.style.alignSelf = Align.Center;
Add(lbGuid);
//获得类型
_type = GetType();

FieldInfo[] fields = _type.GetFields(_flag);

//设置标题颜色
Label lbTitle = mainContainer.Q<Label>("title-label", (string)null);
lbTitle.style.color = Color.black;
lbTitle.style.fontSize = 18;
lbTitle.style.unityFontStyleAndWeight = FontStyle.Bold;

//固定titleContainer大小
titleContainer.style.minHeight = 25;
titleContainer.style.maxHeight = 25;

//mainContainer着色
mainContainer.style.backgroundColor = new Color(0.5f, 0.5f, 0.5f, 1f);
mainContainer.style.color = Color.black;
mainContainer.style.flexGrow = 1;

//extensionContainer自动高度
extensionContainer.style.height = StyleKeyword.Auto;
// extensionContainer.style.backgroundColor = new Color(0.55f, 1.00f, 0.78f, 0.8f);

//ui box 分割。没有box标签的情况下都在默认的第一个box中添加ui
int boxCount = -1;
var box = new VisualElement();
extensionContainer.Add(box);

foreach (var field in fields)
{
GraphNode attr = field.GetCustomAttribute<GraphNode>();

if (attr != null)
{
//辅助名称
GName gName = field.GetCustomAttribute<GName>();
string fieldName = field.Name.Replace("_", "");
string subName = gName != null
? gName.GetName()
: fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);

//容器宽度
GWidth gWidth = field.GetCustomAttribute<GWidth>();
Length len = gWidth != null ? gWidth.GetLength() : new Length(96, LengthUnit.Percent);

//判断是否需要增加ui box
if (boxCount == 0)
{
box = new VisualElement();
extensionContainer.Add(box);
}

if (boxCount > -1)
{
boxCount--;
}

object value = field.GetValue(this);

//添加当前UI类型的父容器
VisualElement ele = new VisualElement();
ele.style.width = len;
ele.style.marginTop = 8;
ele.style.alignSelf = Align.Center;

//辅助标签
Label foreLabel;

//非注释UI情况下,UI父容器底部描白边
if (attr.Type() != NodeTypeEnum.Note)
{
ele.style.borderBottomColor = Color.white;
ele.style.borderBottomWidth = 2;
ele.style.paddingBottom = 3;
}

//设置值的委托函数
SetFieldDelegate setValue =
(SetFieldDelegate)Delegate.CreateDelegate(typeof(SetFieldDelegate), field, "SetValue", false);

void SetData(object o)
{
setValue(this, o);
}

string[] extra = attr.GetExtra();
//创建UI
switch (attr.Type())
{
case NodeTypeEnum.Label:
Label label = new Label();
label.style.fontSize = _fontSize;
label.text = value.ToString();
ele.Add(label);
break;
case NodeTypeEnum.Input:
ele.style.flexDirection = FlexDirection.Row;

foreLabel = new Label();
foreLabel.style.fontSize = _fontSize;
foreLabel.text = subName;
ele.Add(foreLabel);

TextField text = new TextField();
text.style.flexGrow = 1;
text.style.height = _fontSize * 1.2f;
text.value = (string)value;

var fontChild = text.Children().FirstOrDefault().Children().FirstOrDefault();
fontChild.style.fontSize = _fontSize;

text.RegisterValueChangedCallback(evt => { SetData(evt.newValue); });
ele.Add(text);
break;
case NodeTypeEnum.Note:
//创建注释背景
ele.style.flexDirection = FlexDirection.Column;
ele.style.alignItems = Align.Center;
ele.style.justifyContent = Justify.Center;
ele.style.backgroundColor = new Color(0.64f, 0.78f, 1f, 1f);
ele.style.borderBottomLeftRadius = 5;
ele.style.borderBottomRightRadius = 5;
ele.style.borderTopLeftRadius = 5;
ele.style.borderTopRightRadius = 5;

//固定注释和可修改注释的情况
if (extra.Length > 0 && extra[0] == "Custom")
{
TextField note = new TextField();
note.value = value.ToString();
note.style.width = ele.style.width;
note.style.height = StyleKeyword.Auto;
note.multiline = true;
note.style.whiteSpace = WhiteSpace.Normal;
note.style.height = StyleKeyword.Auto;

var child = note.Children().FirstOrDefault();

child.style.backgroundColor = Color.clear;
child.style.borderBottomColor = Color.clear;
child.style.borderTopColor = Color.clear;
child.style.borderLeftColor = Color.clear;
child.style.borderRightColor = Color.clear;
child.style.borderBottomWidth = 0;
child.style.borderTopWidth = 0;
child.style.borderLeftWidth = 0;
child.style.borderRightWidth = 0;
child.style.color = Color.black;
child.style.unityTextAlign = TextAnchor.MiddleCenter;

fontChild = child.Children().FirstOrDefault().Children().FirstOrDefault();
fontChild.style.fontSize = _fontSize;

note.RegisterValueChangedCallback((evt) => { SetData(evt.newValue); });

note.RegisterCallback<FocusOutEvent>(evt => { UpdateNodeSize(); });

ele.Add(note);
}
else
{
Label note = new Label();
note.style.fontSize = _fontSize;
note.style.width = StyleKeyword.Auto;
note.style.height = StyleKeyword.Auto;
note.text = value.ToString();
note.style.whiteSpace = WhiteSpace.Normal;

ele.Add(note);
}

break;
case NodeTypeEnum.Enum:
ele.style.flexDirection = FlexDirection.Row;

foreLabel = new Label();
foreLabel.style.fontSize = _fontSize;
foreLabel.text = subName;
ele.Add(foreLabel);

EnumField enumField = new EnumField(value as Enum);
enumField.style.flexGrow = 1;
enumField.value = (Enum)value;

enumField.RegisterValueChangedCallback(evt => setValue(this, evt.newValue));

ele.Add(enumField);
break;

case NodeTypeEnum.Slide:
ele.style.flexDirection = FlexDirection.Row;

foreLabel = new Label();
foreLabel.style.fontSize = _fontSize;
foreLabel.text = subName;
ele.Add(foreLabel);

bool isInt = extra[0] == "Int";

Label num = new Label();
num.style.fontSize = _fontSize;
num.style.width = _fontSize * 3;
num.style.unityTextAlign = TextAnchor.MiddleRight;

if (isInt)
{
SliderInt slider = new SliderInt(int.Parse(extra[1]), int.Parse(extra[2]));

num.text = extra[1];

slider.style.flexGrow = 1;

slider.RegisterValueChangedCallback(evt =>
{
SetData(evt.newValue);
num.text = evt.newValue.ToString();
});

slider.value = (int)value;
num.text = value.ToString();

ele.Add(slider);
}
else
{
Slider slider = new Slider(float.Parse(extra[0]), float.Parse(extra[1]));

num.text = extra[0];

slider.style.flexGrow = 1;

slider.RegisterValueChangedCallback(evt =>
{
SetData(evt.newValue);
num.text = evt.newValue.ToString();
});

slider.value = (float)value;
num.text = value.ToString();

ele.Add(slider);
}

ele.Add(num);

break;
case NodeTypeEnum.Radio:

RadioButtonGroup radioButtonGroup = new RadioButtonGroup();
radioButtonGroup.value = 0;

ele.Add(radioButtonGroup);

int index = 0;

foreach (var selection in extra)
{
RadioButton radio = new RadioButton();
radio.Children().FirstOrDefault().style.flexGrow = 0;

Label lbRadio = new Label();
lbRadio.style.fontSize = _fontSize;
lbRadio.text = selection;
radio.Add(lbRadio);

radioButtonGroup.Add(radio);

if (index == (int)value)
{
radio.value = true;
}

index++;
}

radioButtonGroup.RegisterValueChangedCallback(evt => { SetData(evt.newValue); });
break;
case NodeTypeEnum.Toggle:

Toggle toggle = new Toggle();
toggle.style.width = StyleKeyword.Auto;
toggle.Children().FirstOrDefault().style.flexGrow = 0;

toggle.value = (bool)value;

toggle.RegisterValueChangedCallback(evt =>
{
SetData(evt.newValue);
Debug.Log(field.GetValue(this));
});

ele.Add(toggle);

foreLabel = new Label();
foreLabel.style.fontSize = _fontSize;
foreLabel.text = subName;
toggle.Add(foreLabel);

break;
case NodeTypeEnum.Box:

if (gName != null)
{
foreLabel = new Label();
foreLabel.style.fontSize = _fontSize * 0.8f;
foreLabel.text = subName;
ele.Add(foreLabel);
}

Color color = Color.yellow;
GColor gColor = field.GetCustomAttribute<GColor>();

if (gColor != null)
{
color = gColor.GetColor();
}

box = new VisualElement();
box.style.backgroundColor = color;
box.style.marginTop = 8;
box.style.width = new Length(96, LengthUnit.Percent);
box.style.alignSelf = Align.Center;
box.style.borderBottomLeftRadius = 5;
box.style.borderBottomRightRadius = 5;
box.style.borderTopLeftRadius = 5;
box.style.borderTopRightRadius = 5;
box.style.flexDirection = extra.Length > 1 && extra[1] == "Column"
? FlexDirection.Column
: FlexDirection.Row;
box.style.flexWrap = Wrap.Wrap;

extensionContainer.Add(box);

boxCount = int.Parse(extra[0]);

break;
}

box.Add(ele);
}
}
}

/// <summary>
/// 刷新尺寸
/// </summary>
/// <param name="size"></param>
public void UpdateSize(Vector2 size)
{
Vector2 cur = _curSize.Equals(Vector2.zero) ? _defSize : _curSize;
Vector2 temp;
if (_isBordered)
{
Vector2 border = new Vector2(_borderOffset, _borderOffset);
temp = cur + size + border;
temp = Vector2.Max(_defSize + border,temp);
}
else
{
temp = cur + size;
temp = Vector2.Max(_defSize,temp);
}


style.width = temp.x;
style.height = temp.y;
}

/// <summary>
/// 更新节点尺寸
/// </summary>
private void UpdateNodeSize()
{
//未手动调整过大小才自动更新
if (_curSize.Equals(Vector2.zero))
{
// 计算内容的总高度
float contentHeight = _defHeight + extensionContainer.layout.height + 10;

// 设置 Node 的新高度
SetSize(new Vector2(_defSize.x, contentHeight));
DrawBorder();
}
}

/// <summary>
/// 更新带描边的节点尺寸
/// </summary>
private void UpdateBorderSize()
{
if (_borderOffset == 0)
{
_isBordered = false;
DrawBorder();
}
else if (!_isBordered)
{
_isBordered = true;
DrawBorder();
}
}

/// <summary>
/// 描边
/// </summary>
private void DrawBorder()
{
if (_isBordered)
{
if (_curSize.Equals(Vector2.zero))
{
style.width = _defSize.x + _borderOffset;
style.height = _defSize.y + _borderOffset;
}
else
{
style.width = _curSize.x + _borderOffset;
style.height = _curSize.y + _borderOffset;
}

_defPos = new Vector2(style.left.value.value, style.top.value.value);

//如果没描过边,则调整节点坐标位置以防止错位
if (_lastBordered != _isBordered)
{
style.left = _defPos.x - _borderOffset * 0.5f;
style.top = _defPos.y - _borderOffset * 0.5f;
}
}
else
{
if (_curSize.Equals(Vector2.zero))
{
style.width = _defSize.x;
style.height = _defSize.y;
}
else
{
style.width = _curSize.x;
style.height = _curSize.y;
}

//还原节点默认位置
style.left = _defPos.x;
style.top = _defPos.y;
}

_lastBordered = _isBordered;
}

/// <summary>
/// 初始化配置
/// </summary>
protected virtual void InitConfig()
{
}

/// <summary>
/// 保存数据
/// </summary>
protected virtual void SetData()
{
}

/// <summary>
/// 还原数据
/// </summary>
protected virtual void ResetData()
{
}

/// <summary>
/// 自定义UI,如果有复杂的需求无法通过Attribute完成,则可以在这里拓展
/// </summary>
protected virtual void CustomUI()
{
}
}
}
CommentEdge
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
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace GraphViewExtension
{
public class CommentEdge : Edge
{
private VisualElement _box;
private Label _commentLabel;

private bool _set = false;

public CommentEdge()
{
// 创建并设置 Label
_box = new VisualElement();
_box.style.backgroundColor = Color.white;
_box.style.borderBottomLeftRadius = 5;
_box.style.borderBottomRightRadius = 5;
_box.style.borderTopLeftRadius = 5;
_box.style.borderTopRightRadius = 5;
_box.style.position = Position.Absolute;

_commentLabel = new Label("");
_commentLabel.style.color = Color.black;

Add(_box);
_box.Add(_commentLabel);
}

public override bool UpdateEdgeControl()
{
if (input?.childCount > 0 && output?.childCount > 0 && hierarchy.parent != null)
{
// 获取GraphView的缩放比例
var scale = hierarchy.parent.worldTransform.m00;

// 计算 Label 的新位置,这里将其放在 Edge 的中间
var midpoint = (input.worldBound.position + output.worldBound.position) / 2;
_box.style.left = (midpoint.x - worldBound.x) / scale + _box.resolvedStyle.width;
_box.style.top = (midpoint.y - worldBound.y) / scale;


SetComment("索引" + GetOutputIndex());
// GetOutputIndex();
}

var res = base.UpdateEdgeControl();
return res;
}

public int GetOutputIndex()
{
if (output != null)
{
RootNode outputNode = output.node as RootNode;
if (outputNode != null)
{
var outputPorts = outputNode.GetOutput().connections;
int outputIndex = 0;

foreach (var port in outputPorts)
{
if (port.Equals(this))
{
break;
}

outputIndex++;
}

return outputIndex;
}
}

return -1;
}

// 设置说明文本的方法
public void SetComment(string comment)
{
_commentLabel.text = comment;
}
}
}
GSearchWindow
GSearchWindow
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
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Plastic.Newtonsoft.Json;
using Unity.Plastic.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

namespace GraphViewExtension
{
public class GSearchWindow: ScriptableObject,ISearchWindowProvider
{
public delegate bool SelectHandle(SearchTreeEntry searchTreeEntry,
SearchWindowContext context);

public SelectHandle onSelectEntryHandler;

private bool _inited;

private JArray _menu;

public List<SearchTreeEntry> entries = new List<SearchTreeEntry>()
{
new SearchTreeGroupEntry (new GUIContent ("创建节点"))
};

private void BuildTree()
{
InitJson();

foreach (var token in _menu)
{
CreateMenu(token);
}

_inited = true;
}

private void InitJson()
{
string json = EditorGUIUtility.Load("Assets/Editor/GraphViewExtension/Graph/Menu.json").ToString();
_menu = JArray.Parse(json);
}

private void CreateMenu(JToken obj,int level = 1)
{
string menuName = obj["name"].ToString();
string menuType = "GraphViewExtension." + obj["type"];
JToken children = obj["child"];

bool isChild = children?.Count() > 0;

if (isChild)
{
entries.Add(new SearchTreeGroupEntry(new GUIContent(menuName)){level = level});
foreach (var child in children)
{
CreateMenu(child, level + 1);
}
}
else
{
entries.Add(new SearchTreeEntry(new GUIContent(menuName)){level = level,userData = Type.GetType(menuType)});
}
}

public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
if (!_inited)
{
BuildTree();
}
return entries;
}

public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
if (onSelectEntryHandler == null)
return false;
return onSelectEntryHandler(searchTreeEntry, context);
}
}
}
GGraph
GGraph
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using AutoLayout;
using Unity.Plastic.Newtonsoft.Json;
using Unity.Plastic.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace GraphViewExtension
{
public class GGraph : GraphView
{
/// <summary>
/// 所属编辑器
/// </summary>
private EditorWindow _editorWindow;

/// <summary>
/// 右键创建节点的可搜索多级菜单
/// </summary>
private GSearchWindow _seachWindow;

/// <summary>
/// 默认节点大小
/// </summary>
private Vector2 _defaultNodeSize = new Vector2(200, 102);

/// <summary>
/// 当前点击节点
/// </summary>
private RootNode _clickNode;

/// <summary>
/// 打开的文件路径
/// </summary>
private string _filePath = "";

/// <summary>
/// 是否打开一个文件
/// </summary>
private bool _isOpen = false;

/// <summary>
/// 是否在选择
/// </summary>
private bool _isSelect = false;

/// <summary>
/// 鼠标是否按下
/// </summary>
private bool _isDown = false;

/// <summary>
/// 鼠标是否移动了
/// </summary>
private bool _isMoved = false;

private Dictionary<string, RootNode> _allNodes = new Dictionary<string, RootNode>();


public GGraph(EditorWindow editorWindow, GSearchWindow provider)
{
_editorWindow = editorWindow;
_seachWindow = provider;

//在右键菜单中添加节点
provider.onSelectEntryHandler = (searchTreeEntry, searchWindowContext) =>
{
var windowRoot = _editorWindow.rootVisualElement;
var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot,
searchWindowContext.screenMousePosition - _editorWindow.position.position);
var graphMousePosition = contentViewContainer.WorldToLocal(windowMousePosition);
var type = searchTreeEntry.userData as Type;
CreateNode(type, graphMousePosition);
return true;
};

//监听创建节点
nodeCreationRequest += context =>
{
//打开搜索框
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), provider);
};

//监听节点删除
graphViewChanged += evt =>
{
if (evt.elementsToRemove != null)
{
foreach (var element in evt.elementsToRemove)
{
if (element is RootNode node)
{
_allNodes.Remove(node.guid);
}
}
}

return evt;
};

//尺寸和父控件相同
this.StretchToParentSize();
//滚轮缩放
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
//窗口内容拖动
this.AddManipulator(new ContentDragger());
//选中Node移动功能
this.AddManipulator(new SelectionDragger());
//多个node框选功能
this.AddManipulator(new RectangleSelector());

//加载样式表和网格
var styleSheet =
EditorGUIUtility.Load("Assets/Editor/GraphViewExtension/Graph/GridBackground.uss") as StyleSheet;
styleSheets.Add(styleSheet);
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();

SetupToolbar();

RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button == 0)
{
_isDown = true;
}
});

RegisterCallback<PointerMoveEvent>(evt =>
{
if (_isDown)
{
_isMoved = true;
}
});

//监听点击的节点
RegisterCallback<ClickEvent>(evt =>
{
if (!_isMoved)
{
Vector2 localMousePosition = evt.position - new Vector3(layout.xMin, layout.yMin);
// 获取当前被选中的对象
var currentSelection = panel.Pick(localMousePosition);

if (currentSelection == null)
{
if (_clickNode != null)
{
_clickNode.UnSelected();
_clickNode = null;
}
}
else
{
bool isInput = false;
while (currentSelection != null && currentSelection is not RootNode)
{
if (IsInputField(currentSelection))
{
isInput = true;
break;
}

currentSelection = currentSelection.parent;
}

if (currentSelection == null)
{
if (_clickNode != null)
{
_clickNode.UnSelected();
_clickNode = null;
}
}
else
{
if (isInput) return;

if (_clickNode != null && currentSelection != _clickNode &&
currentSelection is not RootNode)
{
_clickNode.UnSelected();
_clickNode = null;
}
else
{
_clickNode?.UnSelected();
_clickNode = currentSelection as RootNode;
_clickNode?.Selected();
}
}
}
}

_isMoved = false;
_isDown = false;
});

//监听保存组合键
RegisterCallback<KeyUpEvent>(OnKeyUp);
}

public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
//存储符合条件的兼容的端口
List<Port> compatiblePorts = new List<Port>();
//遍历Graphview中所有的Port 从中寻找
ports.ForEach(
(port) =>
{
if (startPort.node != port.node && startPort.direction != port.direction)
{
compatiblePorts.Add(port);
}
}
);
return compatiblePorts;
}


public RootNode CreateNode(Type type, Vector2 position)
{
RootNode node = Activator.CreateInstance(type) as RootNode;
//这里只用到了position
node.SetPosition(new Rect(position, Vector2.zero));
node.RegistResize(this);
node.SetSize(_defaultNodeSize);
AddElement(node);
_allNodes.Add(node.guid, node);
return node;
}

public RootNode CreateNode(Type type, string guid, Vector2 position, Vector2 size)
{
RootNode node = Activator.CreateInstance(type) as RootNode;
//这里只用到了position
node.SetPosition(new Rect(position, Vector2.zero));
node.RegistResize(this);
node.SetSize(_defaultNodeSize);
node.SetCurSize(size);
node.UpdateSize(Vector2.zero);
AddElement(node);
node.guid = guid;
_allNodes.Add(guid, node);

//创建节点标记为打开文件
_isOpen = true;

return node;
}


public Edge MakeEdge(Port oput, Port iput, int index)
{
Debug.Log("创建Edge");
var edge = new CommentEdge() { output = oput, input = iput };
// edge.SetComment("索引" + index);
edge?.input.Connect(edge);
edge?.output.Connect(edge);
AddElement(edge);
return edge;
}

public void ClearGraph()
{
foreach (var node in _allNodes)
{
node.Value.RemoveEdges();
RemoveElement(node.Value);
}

_allNodes.Clear();

_isOpen = false;

_filePath = "";
}

/// <summary>
/// 是否可输入UI
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool IsInputField(VisualElement element)
{
// 判断元素是否是BaseField<T>或其派生类的实例
return HasBaseField(element.GetType());
}

bool HasBaseField(Type type)
{
if (type.Name.IndexOf("BaseField") != -1)
{
return true;
}

// 检查当前类型的基类的字段
if (type.BaseType != null)
{
return HasBaseField(type.BaseType);
}

return false;
}

/// <summary>
/// 保存数据
/// </summary>
public List<GDataNode> SaveData()
{
List<GDataNode> list = new List<GDataNode>();
foreach (var nodeData in _allNodes)
{
RootNode node = nodeData.Value;

if (!node.HasParent())
{
GDataNode dataNode = node.SaveData();
list.Add(dataNode);
}
}

return list;
}


/// <summary>
/// 当前是否打开文件
/// </summary>
/// <returns></returns>
public bool GetOpen()
{
return _isOpen;
}

public void SetFilePath(string filePath)
{
_filePath = filePath;
}

public void OpenData(List<GDataNode> datas, RootNode parent = null)
{
int index = 0;
foreach (var data in datas)
{
Type type = Type.GetType(data.GetNodeType());
dynamic nodeData = data.GetData();

string[] pos = nodeData.pos.Trim('(', ')').Split(',');
string[] size = nodeData.size.Trim('(', ')').Split(',');

RootNode newNode = CreateNode(type, nodeData.guid,
new Vector2(float.Parse(pos[0]), float.Parse(pos[1])),
new Vector2(float.Parse(size[0]), float.Parse(size[1])));

newNode.SetData(nodeData);

if (parent != null)
{
//连线
MakeEdge(parent.GetOutput(), newNode.GetInput(), index);
}

OpenData(data.GetChildren(), newNode);
}
}

protected virtual void SetupToolbar()
{
var toolbar = new Toolbar();
var openBtn = new ToolbarButton { text = "打开" };
openBtn.clicked += Open;
var saveBtn = new ToolbarButton { text = "保存" };
saveBtn.clicked += Save;
var closeBtn = new ToolbarButton { text = "关闭" };
closeBtn.clicked += Close;
var layoutBtn = new ToolbarButton { text = "自动布局" };
layoutBtn.clicked += Layout;

toolbar.Add(openBtn);
toolbar.Add(saveBtn);
toolbar.Add(closeBtn);
toolbar.Add(layoutBtn);
Add(toolbar);
}

private void Open()
{
bool isOpen = GetOpen();
if (isOpen)
{
bool res = EditorUtility.DisplayDialog("打开新文件", "是否打开一个新的文件,当前内容未保存的部分会消失。", "确定", "取消");
if (res)
{
ClearGraph();
}
else
{
return;
}
}

string filePath = EditorUtility.OpenFilePanel("打开ScriptableObject", "Assets/Json", "json");

if (filePath != "")
{
string jsonData = "";

StreamReader sr = File.OpenText(filePath);
while (sr.ReadLine() is { } nextLine)
{
jsonData += nextLine;
}

sr.Close();

List<SaveJson> json = JsonConvert.DeserializeObject<List<SaveJson>>(jsonData);

List<GDataNode> list = new List<GDataNode>();

foreach (var data in json)
{
list.Add(ToGDataNode(data));
}

SetFilePath(filePath);
OpenData(list);
}
}

private void Save()
{
if (_filePath == "")
{
_filePath = EditorUtility.SaveFilePanel("保存到本地", Application.dataPath + "/Json", "NewFile", "json");
if (_filePath == "")
{
return;
}
}

SetFilePath(_filePath);

List<GDataNode> list = SaveData();

List<SaveJson> listJson = new List<SaveJson>();

foreach (var data in list)
{
listJson.Add(ToJson(data));
}

string jsonData = JsonConvert.SerializeObject(listJson);

FileInfo myFile = new FileInfo(_filePath);
StreamWriter sw = myFile.CreateText();

foreach (var s in jsonData)
{
sw.Write(s);
}

sw.Close();

_editorWindow.ShowNotification(new GUIContent("保存成功,路径为: " + _filePath));
}

private void Close()
{
ClearGraph();
}

private void Layout()
{
foreach (var node in _allNodes)
{
node.Value.UnSelected();
}

schedule.Execute(() =>
{
AutoLayoutUtils.hSpace = 100;
AutoLayoutUtils.vSpace = 30;

float bonus = 0;

foreach (var data in _allNodes)
{
if (!data.Value.GetInput().connections.Any())
{
bonus = AutoLayoutUtils.Layout(data.Value, bonus);
}
}
}).StartingIn(1);
}

private void OnKeyUp(KeyUpEvent evt)
{
// 检查是否按下了 Ctrl 键和 S 键
if (evt.keyCode == KeyCode.S && evt.ctrlKey)
{
if (_filePath == "")
{
Save();
return;
}

Debug.Log("Ctrl + S pressed. Saving...");
// 执行保存操作
List<GDataNode> list = SaveData();

List<SaveJson> listJson = new List<SaveJson>();

foreach (var data in list)
{
listJson.Add(ToJson(data));
}

string jsonData = JsonConvert.SerializeObject(listJson);

FileInfo myFile = new FileInfo(_filePath);
StreamWriter sw = myFile.CreateText();

foreach (var s in jsonData)
{
sw.Write(s);
}

sw.Close();
// 这里可以添加你的保存逻辑
evt.StopPropagation(); // 阻止事件传递,避免触发其他事件
}
}

/// <summary>
/// 转换为JSON
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public SaveJson ToJson(GDataNode data)
{
SaveJson save = new SaveJson();
save.type = data.GetNodeType();
save.data = data.GetData();

foreach (var child in data.GetChildren())
{
save.children.Add(ToJson(child));
}

return save;
}

/// <summary>
/// 转换为节点数据
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public GDataNode ToGDataNode(SaveJson json)
{
GDataNode data = new GDataNode();
data.SetNodeType(json.type);

dynamic obj = new ExpandoObject();

foreach (var res in (json.data as JObject).Properties())
{
JTokenType jType = res.Value.Type;
switch (jType)
{
case JTokenType.String:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<string>();
break;
case JTokenType.Float:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<float>();
break;
case JTokenType.Integer:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<int>();
break;
case JTokenType.Boolean:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<bool>();
break;
}
}

data.SetData(obj);

foreach (var child in json.children)
{
data.AddChild(ToGDataNode(child));
}

return data;
}
}
}
背景 uss
GridBackground
1
2
3
4
5
6
GridBackground {
--grid-background-color : #222222;
--line-color: rgba(193,196,192,0.1);
--thick-line-color: rgba(193,196,192,0.1);
--spacing: 25;
}
菜单配置文件格式
菜单配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
"name": "装饰节点",
"child": [
{
"name": "胜利节点",
"type": "DNodeSuccess"
},
{
"name": "失败节点",
"type": "DNodeFail"
},
{
"name": "反转节点",
"type": "DNodeReverse"
}
]
},
{
"name": "测试节点",
"type": "TestNode"
}
]
GDataNode
GDataNode
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
using System.Collections.Generic;

namespace GraphViewExtension
{
public class GDataNode
{
private List<GDataNode> _children = new List<GDataNode>();

private dynamic _data;

private string _type;

public void SetData(dynamic data)
{
_data = data;
}

public void SetNodeType(string type)
{
_type = type;
}

public void AddChild(GDataNode node)
{
if (!_children.Contains(node))
{
_children.Add(node);
}
}

public List<GDataNode> GetChildren()
{
return _children;
}

public dynamic GetData()
{
return _data;
}

public string GetNodeType()
{
return _type;
}
}
}
SaveJson
SaveJson
1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections.Generic;

namespace GraphViewExtension
{
public class SaveJson
{
public List<SaveJson> children = new List<SaveJson>();

public dynamic data;

public string type;
}
}

测试编辑器

编辑器
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
using UnityEditor;

namespace GraphViewExtension
{
[EName ("测试工具")]
public class TestEditor : BaseEditor<TestEditor>
{
private GGraph _view;

[MenuItem("GraphView/Editor")]
public static void ShowWindow()
{
var window = GetWindow<TestEditor>();
window.Show();
window.InitGraph();
}

public void InitGraph()
{
var provider = CreateInstance<GSearchWindow>();
var graph = new GGraph(this, provider);
rootVisualElement.Add(graph);
_view = graph;
}
}
}

项目仓库

更新日志

2024-06-21

  1. 修复节点大小调整 bug。
  2. 新增自动布局,详见 Unity-树结构自动布局
  3. 调整自动布局逻辑。

2024-06-20

  1. 更新 edge 索引标签,可以知道连接的先后顺序。

2024-06-17

  1. 更新保存按钮逻辑,打开的图保存的时候不再需要选择新的存储位置。

2024-05-13

  1. 更新基本内容。

评论