参考链接:https://www.youtube.com/watch?v=7KHGH0fPL84&ab_channel=MertKirimgeri

GraphView介绍

Unity在2018.1的版本开始加入了一个节点绘制系统,类似于XNode,它不需要在Unity里安装任何Package或者像XNode一样添加任何脚本,只需要使用Unity的官方API即可。Unity里的Shader Graph,VFX Graph和Visual Scripting都是通过Graph View API实现的。这玩意儿适合做Unity的相关编辑器。

相关的API都在对应的命名空间下UnityEditor.Experimental.GraphView

PS: 这一块内容其实是Unity的UI Elements的子集,了解了UI Element,再来学Graph View会更容易上手

下面做一个Demo,在这个Demo里进行Graph View API的学习,这个例子利用GraphView API和Unity的UIElements创建了一个用于人物对话的节点编辑系统,有点类似于蓝图。

1. 创建GraphView和Node的底层类

下面会利用Graph View API创建一个dialogue node system,一个节点系统的图是由图本身和其内部的节点构成的,所以这里创建两个类,分别对应着UI图的类,和UI节点的类,每个类各自对应一个脚本,代码如下:

// 创建dialogue graph的底层类
public class DialogueGraphView: GraphView {// 在构造函数里,对GraphView进行一些初始的设置public DialogueGraphView() {// 允许对Graph进行Zoom in/outSetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);// 允许拖拽Contentthis.AddManipulator(new ContentDragger());// 允许Selection里的内容this.AddManipulator(new SelectionDragger());// GraphView允许进行框选this.AddManipulator(new RectangleSelector());}
}
// 创建dialogue graph的底层节点类
public class DialogueNode : Node {public string GUID;public string Text;public bool Entry = false;
}

2. 创建空的Editor Window

在创建完GraphView和Node底层数据之后,要想把它们显示出来,到窗口上,则还需要一个EditorWindow,相关代码如下:

// 代表放置GraphView这个Canvas的EditorWindow
public class DialogueGraphWindow: EditorWindow {// 通过Menu即可打开对应window, 注意这种函数必须是static函数[MenuItem("Graph/Open Dialogue Graph View")]public static void OpenDialogueGraphWindow(){// 定义了创建并打开Window的方法var window = GetWindow<DialogueGraphWindow>();window.titleContent = new GUIContent( "Dialogue Graph");}
}

然后现在点击menu下的Graph->Open Dialogue Graph View,就可以打开一个空窗口了,如下图所示:

3. 将GraphView作为Canvas,展示到对应的EditorWindow里

目前这个Window实际上跟前面的GraphView和Node类没有任何关系,只是一个空窗口,下面可以在Window类里定义GraphView为其数据成员,然后在其OnEnter函数里,对GraphView进行创建和初始化等操作:

public class DialogueGraphWindow : EditorWindow
{private DialogueGraphView _graphView;...//原本的打开窗口的函数不变private void OnEnable() {Debug.Log("New GraphView");_graphView = new DialogueGraphView{name = "Dialogue Graph"};// 让graphView铺满整个Editor窗口_graphView.StretchToParentSize();// 把它添加到EditorWindow的可视化Root元素下面rootVisualElement.Add(_graphView);}// 关闭窗口时销毁graphViewprivate void OnDisable(){rootVisualElement.Remove(_graphView);}
}// 再在GraphView的构造函数里, 做一些初始化的工作:
public DialogueGraphView()
{// 允许对Graph进行Zoom in/outSetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);// 允许拖拽Contentthis.AddManipulator(new ContentDragger());// 允许拖拽Selection里的内容this.AddManipulator(new SelectionDragger());// GraphView允许进行框选this.AddManipulator(new RectangleSelector());
}

此时再打开Window,就能在里面展示GraphView了,由于在前面的GraphView的ctor里new了RectangleSelector,所以可以在里面进行框选操作,如下图所示:


4.创建初始节点,作为EntryPoint

前面在创建Window时,new出来对应的GraphView。接下来就是创建和展示里面的Nodes了,正常的操作应该是,一个GraphView有一个初始节点,剩下的节点都可以从该节点拖拽出来。所以,初始节点的创建可以放到GraphView的Ctor里进行,代码如下所示:

public class DialogueGraphView: GraphView
{public DialogueGraphView() {...// Add Manipulator相关的代码// 1. 创建StartNode,并设置好其positionvar startNode = GenEntryPointNode();// 2. 把node加入到GraphView里AddElement(startNode);// 3. 给StartNode添加Output Portvar port = GenPortForNode(startNode, Direction.Output);// 4. 给output改名port.portName = "Next";// 5. 加入到StartNode的outputContainer里startNode.outputContainer.Add(port);}// 比较简单,相当于new了一个Nodeprivate DialogueNode GenEntryPointNode() {DialogueNode node = new DialogueNode{title = "START",GUID = Guid.NewGuid().ToString(),// 借助System的Guid生成方法Text = "ENTRYPOINT",Entry = true};node.SetPosition(new Rect(x: 100, y: 200, width: 100, height: 150));return node;}// 为节点n创建input port或者output port// Direction: 是一个简单的枚举,分为Input和Output两种private Port GenPortForNode(Node n, Direction portDir, Port.Capacity capacity = Port.Capacity.Single) {// Orientation也是个简单的枚举,分为Horizontal和Vertical两种,port的数据类型是floatreturn n.InstantiatePort(Orientation.Horizontal, portDir, capacity, typeof(float));}
}

此时再打开GraphView,就可以看到对应的StartNode了,如下图所示,Next可以往外拖出Edge,而且Start节点也可以四处拖拽:

仔细看一下,发现上面的Start的左边部分还是不大对劲,这是因为在为node添加port之后,需要调用对应的refresh函数来刷新layout,所以只需要在添加port之后加上refresh的代码即可:

...//创建startNode的相关操作
startNode.outputContainer.Add(port);
// 调用两个refresh函数
startNode.RefreshExpandedState();
startNode.RefreshPorts();

然后布局就会变成正常的样子了:


5. 添加菜单工具栏,点击工具栏可以添加更多的Node

为了实现新功能,需要做两件事情:

  • 设计一个函数,函数可以产生一个Node,函数接收一个string,作为新的DialogueNode的Text
  • 为GraphView添加工具栏,点击工具栏上的Add Node,即调用第一步创建的函数

第一步,写一个可以创建Node的函数,跟前面提到的GenEntryNode的方式类似,无非就是多一个Input的port,代码如下:

// ====================== 在DialogueGraphView类内 ==========================
public void AddDialogueNode(string nodeName)
{// 1. 创建NodeDialogueNode node = new DialogueNode{title = nodeName,GUID = Guid.NewGuid().ToString(),Text = nodeName,Entry = false};node.SetPosition(new Rect(x: 100, y: 200, width: 100, height: 150));// 2. 为其创建InputPortvar iport = GenPortForNode(node, Direction.Input, Port.Capacity.Multi);iport.portName = "input";node.inputContainer.Add(iport);node.RefreshExpandedState();node.RefreshPorts();AddElement(node);
}

第二步,创建用于添加Node的UI按钮,即Unity的Button对象,这里把button放到统一的一行里了(即toolbar),如下图所示:

相关代码如下:

// =========== 在GraphViewWindow类的OnEnable函数里 ============//  相关内容涉及到菜单设置,所以应该放到DialogueGraphWindow类下
// 这个Toolbar类在UnityEditor.UIElements下
Toolbar toolbar = new Toolbar();
//创建lambda函数,代表点击按钮后发生的函数调用
Button btn = new Button(clickEvent: () => { _graphView.AddDialogueNode("Dialogue"); });
btn.text = "Add Dialogue Node";
toolbar.Add(btn);
rootVisualElement.Add(toolbar);

最后的效果就是下图这样了,点击按钮可以在EntryNode相同的地方创建新的Node,可以拖拽出来:

6. 让节点之间可以拖拽连接起来

目前的StartNode节点和创建的节点是不可以连接起来的,根据视频里说的,这是因为还没有对新创建的Node的Input Port作类型要求。

// StartNode的output接口是这么写的:
// 一个水平连线的输出接口,类型好像是float
var startP = n.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(float));// 而新添加的Node的input接口是这么写的:
var newNodeP = n.InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(float));

要让节点之间可以连线,需要重载函数GetCompatiblePort,代码如下所示:

// =============== 在GraphView类里 =============
// 这个函数是在GraphView里定义的接口, Summary: Get all ports compatible with given port.
// 应该是返回StartPort里可以用于连接的接口
public virtual List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter);// =============== 在DialogueGraphView类里 =============
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter adapter)
{List<Port> compatiblePorts = new List<Port>();// 继承的GraphView里有个Property:ports, 代表graph里所有的portports.ForEach((port) =>{// 对每一个在graph里的port,进行判断,这里有两个规则:// 1. port不可以与自身相连// 2. 同一个节点的port之间不可以相连if (port != startPort && port.node != startPort.node){compatiblePorts.Add(port);}});// 在我理解,这个函数就是把所有除了startNode里的port都收集起来,放到了List里// 所以这个函数能让StartNode的Output port与任何其他的Node的Input port相连(output port应该默认不能与output port相连吧)return compatiblePorts;
}

OK,现在就可以把StartNode跟其他的Node相连了,不过一个Output口目前好像只能连一个Node,如下图所示:

7. 为节点添加output port

一个节点其output port的数量应该是可以通过节点的GUI来调整的,预期是做出下图这样的情况,当点击Add Output时,会添加Output端口:

相关的行为也可以分为两步:

  • 设计一个函数,这个函数的参数是Node,它会为Node添加一个Output port
  • 相关的GUI设计,当点击Node的title上的button时,调用第一步设计的函数

第一步,设计函数,代码如下:

// ============== DialogueGraphView类里 ==================
private void AddOutputPort(DialogueNode node)
{var outPort = GenPortForNode(node, Direction.Output);// 根据node的outport的数目给新的outport命名var count = node.outputContainer.Query("connector").ToList().Count;string name = $"Output {count}";outPort.portName = name;node.outputContainer.Add(outPort);node.RefreshExpandedState();node.RefreshPorts();
}

第二步,实现对应的GUI部分,相关的代码可以直接放到Node的创建函数里,代码如下:

// ================= DialogueGraphView类 ==============
private DialogueNode GenDialogueNode(string nodeName)
{// 1. 创建Node...// 2. 为其创建InputPort...// 3. 为其在title上创建btn, 点击btn时会调用函数Button btn = new Button(() => {AddOutputPort(node);});btn.text = "Add Output Port";node.titleContainer.Add(btn);return node;
}

这样就能实现前面图里面贴出来的效果了

8. 为Graph添加背景的网格框架

为了更完善Graph窗口,这里介绍一种为其产生背景网格线的方法,如下图所示:

首先,在Editor文件夹下创建Resources文件夹,然后在Project View里选择右键,Create->UIElements->Editor Window,取消勾选C#和UXML,如下图所示:

文件里面写:

GridBackground {--grid-background-color: #282828;--line-color: rgba(193, 196, 192, 0.1);--thick-line-color: rgba(193, 196, 192, 0.1);
}

最后在创建对应的GrahView的Init阶段,读取该uss文件作为StyleSheet即可:

public DialogueGraphView()
{// 不知道为啥没有起作用, 这里是s是读取到了东西的, 原因RemainStyleSheet s = Resources.Load<StyleSheet>("DialogueGraph");styleSheets.Add(s);...
}

9. 创建更多的菜单工具栏选项

对话节点编辑器的设计思路是这样的,在Unity的Window里进行创建和编辑,然后可以把它存储起来,在Runtime下给游戏去读取。当在Editor下点击该文件时,对应的GraphView也会蹦出来。

为了补充Save和Load功能,可以先把UI做好,这里设计了两个Button,用于点击Save和Load,又设计了一个TextField,用于指定存储的文件的名字。

之前只在菜单栏对应的toolbar里添加了一个button,代码如下:

// 在EditorWindow的继承类里这么写
Toolbar toolbar = new Toolbar();
Button btn = new Button(clickEvent: () => { _graphView.AddDialogueNode("Dialogue"); });
btn.text = "Add Dialogue Node";
toolbar.Add(btn);
rootVisualElement.Add(toolbar);

现在添加更多的选项,一种仍然是Button,另一种则是TextField,代码如下所示:

// 添加TextField
TextField fileNameTextField = new TextField(label: "File Name");
fileNameTextField.SetValueWithoutNotify(_fileName);// 类内私有成员_fileName = "New Narrative";
fileNameTextField.MarkDirtyRepaint();
fileNameTextField.RegisterValueChangedCallback(evt => _fileName = evt.newValue);
toolbar.Add(fileNameTextField);// 添加两Button
// 不熟悉这种写法,text是Button的数据成员
// LodaData和SaveData两个函数暂时还没实现
toolbar.Add(new Button(() => SaveData()) { text = "Save Data" });
toolbar.Add(new Button(() => LoadData()) { text = "Load Data" });

之后的toolbar就会变成这样,多了三个元素,两个Button用于存储和读取数据,TextFiled用于指定文件路径:

然后就可以创建具体的底层代码了,从设计角度上,SaveData和LoadData没有必要放在DialogueGraphView类里,这里创建了一个GraphSaveUtility类,代码如下:

public class GraphSaveUtility
{private DialogueGraphView _dialogueGraphView;// 每次从类里获取edges和nodes时,都会去取graphView里的内容,并进行转型private List<Edge> edges => _dialogueGraphView.edges.ToList();private List<DialogueNode> nodes => _dialogueGraphView.nodes.ToList().Cast<DialogueNode>().ToList();public static GraphSaveUtility GetInstance(DialogueGraphView graphView){return new GraphSaveUtility{_dialogueGraphView = graphView};}public void SaveData(){}public void LoadData(){}
}

为了实现SaveData和LoadData函数,先要实现相关的Runtime下的存储文件类,这里使用ScriptableObject作为存储DialogugGraph的存储数据文件:

// 感觉这个类跟之前创建的DialogueNode类非常类似
[Serializable]
public class DialogueNodeData
{public string nodeGUID;// 对应node的GUIDpublic string nodeText;// 对应node的textpublic Vector2 position;
}// 用于存储Node之间的连接关系, 在GraphView里是通过Port连接起来的,
// 存储的时候要额外创建一个NodeLink
[Serializable]
public class DialogueNodeLinkData
{public string baseNodeGuid;public string portName;public string targetNodeGuid;
}// 这里贴一下之前创建的DialogueNode类, 方便前后比对
public class DialogueNode : Node {public string guid;public string text;public bool entry = false;
}

在创建好了Node和NodeLink对应的可序列化的数据结构后,就可以创建整个Graph对应的可序列化的数据结构了,代码如下所示:

[Serializable]
public class DialogueContainer : ScriptableObject
{// 创建两个List, 分别代表nodes和nodeLinkspublic List<DialogueNodeData> nodesData = new List<NodeData>();public List<DialogueNodeLinkData> nodeLinksData = new List<NodeLinkData>();
}

有了这些,就可以实现SaveData和LoadData函数了:

// Save函数的核心代码
public void SaveData(string filePath)
{if (!edges.Any())return;DialogueContainer container = ScriptableObject.CreateInstance<DialogueContainer>();// 遍历所有的Edge, 找到里面有Input的Edge, 组成一个数组Edge[] hasInputEdges = edges.Where(x => x.input.node != null).ToArray();// 注意, edge的Input在右边, Output在左边for (int i = 0; i < hasInputEdges.Length; i++){Edge e = hasInputEdges[i];DialogueNode inputNode = e.input.node as DialogueNode;DialogueNode outputNode = e.output.node as DialogueNode;container.nodeLinksData.Add(new DialogueNodeLinkData(){// 注意,这里的nodeLink是以output作为开始点的// 这样才能保证从左到右的顺序baseNodeGuid = outputNode.GUID,portName = e.output.portName,targetNodeGuid = inputNode.GUID});}// 获取所有不为Entry的Node, 这样的Node既有input,也有outputDialogueNode[] regularNodes = nodes.Where(x => (!x.Entry)).ToArray();for (int i = 0; i < regularNodes.Length; i++){DialogueNode n = regularNodes[i];container.nodesData.Add(new DialogueNodeData(){nodeGuid = n.GUID,nodeText = n.Text,position = n.GetPosition().position});}// 注意这种写字符串的写法AssetDatabase.CreateAsset(container, $"Assets/Resources/{filePath}.asset");AssetDatabase.SaveAssets();
}

附录:一些例子里没提到的拓展操作

使用API将两个Node相连接

代码如下:

============== 在GraphView的派生类里实现 ===============
private void AddEdgeByPorts(Port _outputPort, Port _inputPort)
{Edge tempEdge = new Edge(){output = _outputPort,input = _inputPort};tempEdge.input.Connect(tempEdge);tempEdge.output.Connect(tempEdge);Add(tempEdge);
}

获取Node的InputPort和OutputPort

其实前面提到了,InputPort和OutputPort应该都在对应的Container里:

// 这种写法,我不太理解,也是对C#还不够了解
Port outP = outputNode.outputContainer[0].Q<Port>();
Port inP = rootPlayableNode.inputContainer[0].Q<Port>();

获取GraphViwe对应的窗口大小

GraphView里有一个参数叫Rect layout,可以用于表示Graph代表的窗口大小,layout一般是从0,0为Rect最小点,如果最大点为(500, 400),说明整个窗口的大小是500pixel*400pixel

读写Graph里的Node的位置

这个应该很简单,前面其实提到了

// 注意,这里的Pos都是用Rect表示的,而不是Vector2
node.SetPosition(new Rect(x: 400, y: 200, width: 100, height: 150));
Rect pos = node.GetPosition();

改变Zoom缩放比例

有时Zoom In的时候觉得放的不够大,可以改这个函数:

// 原来是这么写的
// public static readonly float DefaultMinScale = 0.25f;
// public static readonly float DefaultMaxScale = 1;
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);// 实际上是(0.25f, 1)// 后来可以这么写, 前面的数字越小, 可以缩小的比例越大, 后面的数字越大, 放大的倍数越多
SetupZoom(0.15f, 3.0f);

创建Comment Block(代码里叫做Group)

代码如下:

// 创建Comment Block
var group = new Group
{autoUpdateGeometry = true,title = "Comment Block\n 快乐吗\n 很快乐"
};
m_GraphView.AddElement(group);
group.SetPosition(new Rect(500, 20, 200, 100));

效果如图:


创建不随着整体窗口变化的固定位置的窗口(类似于Anchor UI)

这么写就可以了,这其实是UI Element的内容

// 类的数据成员
private Group m_FixedGroup;
...
// 创建Comment Block
m_FixedGroup = new InspectorGroup
{//autoUpdateGeometry = true,title = "Comment Block\n sss \n fff" +"\nfddddddddd" +"\nfdsafdsafdsa" +"\nfdsafdsa" +"\nfdsafdsa" +"dsafd"
};// 可以改layer, 但是这里默认就比GraphView的优先级高,应该是深度递归的顺序导致
//m_FixedGroup.layer = int.MaxValue;
m_FixedGroup.style.position = Position.Absolute;
m_FixedGroup.style.right = 0;
m_FixedGroup.style.top = 21;// 21是工具栏的高度
// 尤其注意这句话,是加到EditorWindow里
rootVisualElement.Add(m_FixedGroup);// 我之前是把这个FixedGroup加到了GraphView的AddElement下面
// 结果在GraphView里进行Zomm的时候,不管怎么样都会带着m_FixedGroup变化
// 这样写就舒服了

最后效果如下:


创建自定义的Graph窗口

通过VisualElement的Layout可以更改位置,但是好像结果总是不大对,我看到项目里用.uss来进行布局和颜色的定义:
相关类的定义如下:

// 通过继承GraphElment创建自己的图中的Element
public class PinnedElementView : GraphElement
{}

相关.uss文件内容如下

.pinnedElement {position:absolute;border-left-width: 1px;border-top-width: 1px;border-right-width: 1px;border-bottom-width: 1px;border-radius: 5px;flex-direction: column;background-color: #4b1b6b;border-color: #191919;min-width: 100px;min-height: 100px;
}.pinnedElement.scrollable {position: absolute;
}.pinnedElement:selected {border-color: #44C0FF;
}.pinnedElement > .mainContainer {flex-direction: column;align-items: stretch;
}.pinnedElement.scrollable > .mainContainer {position: absolute;top:0;left:0;right:0;bottom:0;
}.pinnedElement > .mainContainer > #content {flex-direction: column;align-items: stretch;
}.pinnedElement.scrollable > .mainContainer > #content {position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;flex-direction: column;align-items: stretch;
}.pinnedElement > .mainContainer > #content > ScrollView {flex: 1 0 0;
}.pinnedElement > .mainContainer > #content > #contentContainer {min-height: 50px;padding-left: 0px;padding-top: 0px;padding-right: 0px;padding-bottom: 6px;flex-direction: column;align-items: stretch;
}.pinnedElement > .mainContainer > #content > #header {flex-direction: row;align-items: stretch;background-color: #393939;border-bottom-width: 1px;border-color: #212121;border-top-right-radius: 4px;border-top-left-radius: 4px;padding-left: 1px;padding-top: 4px;padding-bottom: 2px;
}.pinnedElement > .mainContainer > #content > #header > #labelContainer {flex: 1 0 0;flex-direction: column;align-items: stretch;
}.pinnedElement > .mainContainer > #content > #header > #addButton {align-self:center;font-size: 20px;background-image: none;padding-left: 0px;padding-top: 0px;padding-right: 0px;padding-bottom: 0px;margin-top:3px;margin-bottom:3px;margin-left:4px;margin-right:4px;border-left-width:6px;border-top-width:4px;border-right-width:6px;border-bottom-width:4px;
}.pinnedElement > .mainContainer > #content > #header > #addButton:hover {background-image: resource("Builtin Skins/DarkSkin/Images/btn.png");
}.pinnedElement > .mainContainer > #content > #header > #addButton:hover:active {background-image: resource("Builtin Skins/DarkSkin/Images/btn act.png");
}.pinnedElement > .mainContainer > #content > #header > #labelContainer > #titleLabel {font-size : 14px;color: #c1c1c1;
}.pinnedElement > .mainContainer > #content > #header > #labelContainer > #subTitleLabel {font-size: 11px;color: #606060;
}

改变节点端口的颜色

本来想改变Edge的颜色的,但是它的颜色好像是两个端口的自动生成的渐变色

node.outputContainer[0].Q<Port>().portColor = Color.white;

改变节点的title的高度

对应在node.titlecontainer.style里:

node.titleContainer.style.height = 80;
node.titleContainer.style.unityTextAlign = TextAnchor.UpperCenter;

GraphView提供的Element类型

看了下,我知道的图形类,一共有这么些类型:

  • GraphView类

  • Node类

  • Edge类

  • Pill类:类似一个Capsule,可以放text、icon,两个可选的child VisualElement,如下图所示:

  • GraphElement

  • GraphViewBlackboardWindow

  • GraphViewEditorWindow

  • GraphViewMinimapWindow

  • GraphViewToolWindow

  • GridBackground

  • Group

  • IconBadge

  • MiniMap

  • Placemat

  • Stack

  • StickyNote(2020.1开始支持)

  • Token Node: 类似Capsule

额外注意,还有很多基本的UI Element可以用,都是好东西,比如:

  • Box
  • Label
  • Rect
  • Bounds
    等等。。

添加ObjectField

也是UI Element的内容

var objField = new ObjectField
{objectType = typeof(GameObject),allowSceneObjects = false,value = prefabNode.output,
};// 给Object添加预览图
var preview = new Image();
objField.RegisterValueChangedCallback(v => {prefabNode.output = objField.value as GameObject;UpdatePreviewImage(preview, objField.value);
});void UpdatePreviewImage(Image image, Object obj)
{image.image = AssetPreview.GetAssetPreview(obj) ?? AssetPreview.GetMiniThumbnail(obj);
}

效果如下图所示:

清空GraphView里的nodes和edges

像这种写法是不行的:

// Node是class类型
List<Node> nodes = new List<Node>();
Node n = new Node();m_GraphView.Add(n);
nodes.Add(n);// 如果想要清空nodes
m_GraphView.DeleteElements(nodes); 错误的写法

虽然Node是引用类型,但是我看nodes和m_GraphView里的nodes不是同一份数据,所以得这么写:

// Node是class类型
List<Node> nodes = new List<Node>();
Node n = new Node();m_GraphView.Add(n);
nodes.Add(n);DeleteElements(nodes.ToList());
DeleteElements(edges.ToList());m_Nodes.Clear();

代码实现按F的效果
在GraphView的窗口中,按F能合理的显示所有Node,这里的替代的函数为:

GraphView.FrameAll();

改变Edge颜色并强制更新

// Edge本身的颜色不可以直接更改,需要获取其两端的port, 对port的portColor进行修改// 强迫更新:
Edge edge;//假设已有Edge
edge.UpdateEdgeControl();

Runtime改变Group字体的大小

应该是可以写uss调整的,但我这里找到了代码控制的方法,具体的Hierarchy信息我是通过UI Debugger看清楚的,相关代码如下:

// 方法一
Group group = new Group();
group.title = "A Group";
Stack<VisualElement> stack = new Stack<VisualElement>();
stack.Push(group.headerContainer);// 在hierarchy里反复查找Label的物体
while (stack.Any())
{VisualElement ve = stack.Pop();Label label = ve as Label;if (label != null){label.style.fontSize = 5;break;}var veList = ve.hierarchy.Children().ToList();foreach (var item in veList){stack.Push(item);}
}// 方法二, 先找到TemplateContainer, Label就是它的Children(层级关系是从UI Debugger里看到的)
List<VisualElement> list = group.headerContainer.hierarchy.Children().ToList();TemplateContainer item = (TemplateContainer)group.headerContainer.hierarchy.Children().ToList().Find(x => x is TemplateContainer);List<VisualElement> list = item.hierarchy.Children().ToList();
foreach (var itemsss in list)
{var ss = itemsss as Label;if (ss != null){ss.style.fontSize = 5;}
}

高亮GraphView的节点

我发现选中Node时,它自然会显示会高亮的节点状态,所以Select Node,就可以将其高亮

一开始我这么写的:

MyGraphView.selection.Clear();
MyGraphView.selection.Add(MyNode);

发现这样写不对,其实GraphView有对应的API,应该这么写:

MyGraphView.ClearSelection();
MyGraphView.AddToSelection(MyNode);

Unity的GraphView相关推荐

  1. Unity基于GraphView的行为树编辑器

    这里写自定义目录标题 概述 基于GitHub上: 目前这只是做了一些比较基础的功能节点开发,仅仅用于学习交流,非完成品. 项目GitHub连接:[https://github.com/Hengyuan ...

  2. Unity从零开始构建能力体系 Unity Ability System

    从零开始构建能力体系 你会学到什么 如何实施能力体系 如何使用用户界面工具包创建用户界面 如何使用Unity的GraphView API 如何实现保存系统 MP4 |视频:h264,1280×720 ...

  3. 【Unity】PlayableGraph监控工具

    [Unity]PlayableGraph监控工具 Unity官方曾发布过一个用于查看当前PlayableGraph状态的工具 Graph Visualizer ,可以实时查看引擎中的PlayableG ...

  4. [Unity WWW] 跨域访问解决方法

    什么是跨域访问 域(Domain)是Windows网络中独立运行的单位,域之间相互访问则需要建立信任关系(即Trust Relation).信任关系是连接在域与域之间的桥梁.当一个域与其他域建立了信任 ...

  5. unity人物旋转移动代码_Unity3D研究院之脚本实现模型的平移与旋转(六)

    123 说: 雨松大大,有个问题想请教一下,我用UNET构建了个小场景,在电脑上可以客户端可以连接到服务器,Windows和Linux都可以,发布到安卓缺连不了,这是问什么呢 说: 求教一下,刚刚接触 ...

  6. unity课设小游戏_Unity制作20个迷你小游戏实例训练视频教程

    本教程是关于Unity制作20个迷你小游戏实例训练视频教程,时长:20小时,大小:3.8 GB,MP4高清视频格式,教程使用软件:Unity,附源文件,作者:Raja Biswas,共97个章节,语言 ...

  7. steamvr unity 连接眼镜_150度FOV,自研显示方案,Kura公布全新AR眼镜Gallium

    去年11月,一家名为Kura的美国AR初创公司就曝光了一款视场角135度.亮度2000nit的AR光波导原型,其视场角和亮度数据远超现有AR方案,当时获了业内广泛关注. 近期,Kura创始人兼CEO ...

  8. Hololens Unity 开发入门 之 Hello HoloLens

    Hololens Unity 开发入门 之 Hello HoloLens~ 本文主要记录 HoloLens Unity 开发入门 ~ 一.说在前面的话 Unity 对 VR AR 甚至 将来的 MR ...

  9. Unity导出apk出现的问题,JDK,Android SDK,NDK,无“安装模块”

    导出apk失败 使用unity导出apk文件,会出现提示:需要合适版本的JDK.Android SDK和Android NDK,要找到.下载和安装好合适的版本非常耗费时间, 网上很多教程指出可以直接在 ...

最新文章

  1. python数据分析实训大纲,数据分析大赛考纲:(二)Python数据分析应会部分
  2. iOS开发中用到的一些第三方库
  3. Android开发实践:为什么要继承onMeasure()
  4. 一个复杂系统的拆分改造实践!
  5. HDU2853(最大权完美匹配)
  6. 通过base标签实现全网页新窗口链接。
  7. centos编译apache php mysql,在CentOS6.7中编译安装 apache php mysql
  8. 想在创建虚拟机的时候指定ip调研
  9. 学了阿里中台,却依然做不好系统? 聊聊阿里的项目管理
  10. 【其他】编程技巧之常用缩写
  11. 洛谷 [P2964] 硬币的游戏
  12. 烂泥:dnsmasq搭建简易DNS服务器
  13. Windows无法安装到这个磁盘 选中的磁盘具有MBR分区表解决方法
  14. 从“制造”到“智造”,南高齿携手锐捷打造“智能工厂”
  15. 软件测试睡眠原理,测一测你的睡眠质量
  16. 解析大数据智能分析平台开发
  17. win的反义词_趣味记忆—小学英语必须掌握的120组反义词
  18. 用这几种方式清理Mac缓存,你的Mac就不会卡了
  19. 会声会影 2020 23.2.0.587 旗舰版
  20. 数据结构课程设计报告-职工信息管理系统

热门文章

  1. 高温老化测试的原理和试验机
  2. MySQL列名是否区分大小写
  3. 王者荣耀米莱狄契约魔法皮肤特效一览
  4. Final Cut Pro中文新手教程(二一) 如何调整视频速度
  5. 1、windows下VScode修改PYTHONPATH变量方便导入模块
  6. 【吉比特】G-bits2018校园春季招聘技术类岗位笔试经验
  7. 16免费人格测试软件,16personalities
  8. Ciena为电信运营商推出开放式D-NFV解决方案
  9. 测试电梯的测试用例_电梯功能的测试用例和测试方案
  10. 美国海关称区块链生物识别技术可用于旅行安全