rustum 发表于 2022-1-11 20:15

Unity之GraphView

前言

随着Unity的不断升级,推出了不少可视化编辑器,像Shader Graph,Visual Effect Graph等。所以想着在Unity中创建一个自定义的可视化编辑器,来便捷的处理数据逻辑关系。查找了很多资料进行了解和学习,现在已YouTube上一位大神分享的开源代码为例,介绍一下如何实现自定义可视化界面。
自定义可视化界面

Editor窗口

需要创建一个Editor窗口作为载体,然后依次往这个窗口中添加元素即可。继承于EditorWindow的类SelfGraph,都有一个rootVisualElement属性,可以得到VisualElement对象,将元素添加到这个对象中去即可。
StyleSheet

样式表应用于视觉元素以控制用户界面的布局和视觉外观。
使用方式:通过Create/UI Toolkit/StyleSheet菜单创建uss文件,修改其中各项设置,然后在代码中将uss资源进行加载,添加到VisualElement中的styleSheets属性中,即可使对应视觉元素显示自定义布局和外观。



uss面板

GraphVIew

可视化界面主视图类。创建一个继承于GraphVIew的类SelfGrapgView,将其添加到SelfGraph的rootVisualElement中即可显示在窗口中。可以调用GraphView中的StretchToParentSize函数,使其适配于父容器的大小。
视图缩放
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);为了使视图能够随着鼠标滑轮或其他方式的滚动进行缩放,需要调用此函数,并设置最大、最小缩放值。
拖拽和选择
//ContentDragger,允许鼠标拖动一个或多个元素的操控器
this.AddManipulator(new ContentDragger());
//SelectionDragger,选项拖动程序操控器
this.AddManipulator(new SelectionDragger());
//RectangleSelector,矩形选择框操控器
this.AddManipulator(new RectangleSelector());
//FreehandSelector,自由选择工具
this.AddManipulator(new FreehandSelector());添加控制器,以支持在视图界面拖动背景、节点,以及可以使用鼠标点击选中和鼠标拖拽范围选中功能。
背景网格化
var grid = new GridBackground();
Insert(0, grid);可以让背景显示为网格状,Insert中第一个参数为子元素索引,越小深度越低。
节点菜单
nodeCreationRequest = context =>
                SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), nodeSearchWindow);nodeCreationRequest是GraphView中定义的一个显示节点创建窗口的委托,可以传入自定义的节点搜索窗口类作为参数。
NodeSearchWindow

自定义的节点搜索窗口。需要继承自ISearchWindowProvider接口,接口中有两个函数,分别用于创建搜索菜单树,以及菜单中某一项点击时的触发处理。
搜索菜单函数:
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
    var tree = new List<SearchTreeEntry>
    {
      new SearchTreeGroupEntry(new GUIContent("Create Node"), 0),
      new SearchTreeGroupEntry(new GUIContent("Dialogue"), 1),
      new SearchTreeEntry(new GUIContent("Dialogue Node", _indentationIcon))
      {
            level = 2, userData = new DialogueNode()
      },
      new SearchTreeEntry(new GUIContent("Comment Block",_indentationIcon))
      {
            level = 1,
            userData = new Group()
      }
    };

   return tree;
}SearchTreeGroupEntry为菜单组项,点击可显示下一等级的菜单项;SearchTreeEntry是菜单功能项,点击触发OnSelectEntry函数,所以需要设置userdata数据,以便OnSelectEntry函数获得具体的参数。代码块中创建的菜单为:Create Node -> Dialogue -> Dialogue Node;Create Node ->Comment Block。
菜单功能项选择函数:
public bool OnSelectEntry(SearchTreeEntry SearchTreeEntry, SearchWindowContext context)
{
    //位置转换
    var mousePosition = _window.rootVisualElement.ChangeCoordinatesTo(_window.rootVisualElement.parent,
                context.screenMousePosition - _window.position.position);
    var graphMousePosition = _graphView.contentViewContainer.WorldToLocal(mousePosition);
    switch (SearchTreeEntry.userData)
    {
      case DialogueNode dialogueNode:
             //todo 在视图中创建节点
             return true;
      case Group group:
             //todo 在视图中创建组
             return true;
    }
    return false;
}首先需要根据上下文参数context中的screenMousePosition屏幕鼠标位置,将其转换为视图空间坐标系下的位置,才能正确的在视图上,按照鼠标点击的位置创建相应的元素。再根据在CreateSearchTree函数中传入给SearchTreeEntry的usedata数据,进行分类处理,以创建不同的元素。
Node

自定义节点类需要继承于Node基类。自定义节点类可以编写任意属性,但是需要有一个GUID,唯一标识,用来区分节点。这个GUID可以在创建节点时,使用Guid.NewGuid()函数进行获取。



自定义对话节点

如上图所示,一个节点除了节点名以及自定义的属性外,还有输入、输出端口。端口的创建代码如下:
var inputPort = node.InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(float));
inputPort.portName = "Input";
tempDialogueNode.inputContainer.Add(inputPort);
var outputPort = node.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(float));
outputPort.portName = "Output";
tempDialogueNode.outputContainer.Add(outputPort);
tempDialogueNode.RefreshExpandedState();
tempDialogueNode.RefreshPorts();
tempDialogueNode.SetPosition(new Rect(position, DefaultNodeSize));利用Node的InstantiatePort函数,可以创建一个特定于此节点的新端口。InstantiatePort函数函数的第一个参数指定端口的排序方向,横行或纵向等;第二个参数指定端口的类型,输入或是输出端口;第三个参数指定端口是单线连接还是可以多线连接。创建好端口之后,只需要将其放入节点的输入或者输出端口容器中。然后根据之前转换的鼠标点击位置,设置节点的显示位置。最后调用两个刷新接口即可正确显示节点。
Edge

边缘元素,用于两个节点端口间的连接。代码如下:
var tempEdge = new Edge()
{
    output = outputSocket,
    input = inputSocket
};
tempEdge?.input.Connect(tempEdge);
tempEdge?.output.Connect(tempEdge);端口的Connect函数,将端口连接到边缘元素上。上述代码相当于,先利用输出端口和输入端口(线是从输出端口连接到输入端口的),创建一条线,然后将线绑定在输出端口和输入端口上。
保存

这是比较重要的部分,因为我们要保存在视图中设置好的节点数据以及节点之间的关系数据,才能在游戏中使用。利用ScriptableObject进行数据保存,存储所有节点数据以及节点连接数据。
节点数据

public class DialogueNodeData
{
    public string NodeGUID;
    public string DialogueText;
    public Vector2 Position;
}

private List<DialogueNode> Nodes => _graphView.nodes.ToList().Cast<DialogueNode>().ToList();
foreach (var node in Nodes.Where(node => !node.EntyPoint))
{
    dialogueContainerObject.DialogueNodeData.Add(new DialogueNodeData
    {
      NodeGUID = node.GUID,
      DialogueText = node.DialogueText,
      Position = node.GetPosition().position
    });
}定义一个节点数据类,用于存放节点数据。然后使用GraphView中的nodes属性获得当前视图中所有的节点,从中筛选出符合条件的节点,将节点的GUID和其他自定义属性,存放到节点数据类中,最后将数据类添加到ScriptableObject对应属性即可。
节点线数据

public class NodeLinkData
{
    public string BaseNodeGUID;
    public string PortName;
    public string TargetNodeGUID;
}

private List<Edge> Edges => _graphView.edges.ToList();
var connectedSockets = Edges.Where(x => x.input.node != null).ToArray();
for (var i = 0; i < connectedSockets.Count(); i++)
{
    var outputNode = (connectedSockets.output.node as DialogueNode);
    var inputNode = (connectedSockets.input.node as DialogueNode);
    dialogueContainerObject.NodeLinks.Add(new NodeLinkData
    {
         BaseNodeGUID = outputNode.GUID,
         PortName = connectedSockets.output.portName,
         TargetNodeGUID = inputNode.GUID
    });
}定义一个节点连接数据,在其中需要保存连接线起始节点GUID,终止节点GUID的属性。然后使用GraphView中的edges属性获得当前视图中所有的连接线,然后找出连接线的input端口即终止端口所对应节点不为空的有效连接线,将连接线即Edge的input和output(input和output跟正常理解有点区别,不是从input指向outpot,而是相反的,因为节点连接,是从一个节点的output端口,连接到另一个节点的input端口的)的node数据保存到节点连接数据中,添加到ScriptableObject对应属性即可。
数据保存之后,下次即可利用这些数据,在打开视图时,恢复上一次保存的节点及节点的连接。恢复时,节点通过节点数据,可以在上一次的位置创建对应的节点,并设置好节点数据。节点连接关系,需要遍历节点,然后找出起始节点GUID与当前节点GUID相同的所有节点连接数据,再遍历得到的节点数据集合,根据索引,将当前节点第索引个output端口(因为output端口可能有多个)和当前索引节点数据的input端口连接(目前input端口只有一个,如果有多个,连接代码需要修改)。这样就可以得到完整之前打开的视图。
参考


[*]Unity API - GraphView
[*]YouTube大神
[*]对话节点视图工程
页: [1]
查看完整版本: Unity之GraphView