Unity 可视化节点编辑器(GraphView、编辑器扩展)

前言

  前几天把导师的项目打包发布交了一稿,这半个星期除了再把项目缝缝补补外(说实话项目做到后边实在有些无聊,都是些琐碎的东西而且自己也学不到什么,纯粹是浪费消磨时间)无聊逛Unity商店发现了个有意思的东西,说实话一开始我以为只是单纯绘制的2D动画:

但再一看才发现其实这是三维模型渲染出来的画面,右边是模型:

  其实场景中的卡通渲染模型倒是比较普通,但这个最终渲染效果对我而言却可以说是相当惊艳了,于是第一反应就是看看怎么实现的然后做到我未来的项目里(当然以我的水平和学习方向,细节大概是看不懂的,但可以看一下大体的思路,如果以后有机会可以学习学习相关内容)。先翻了一下Frame Debugger,看得半懂不懂。然后翻了翻就发现了这个东西Visual Compositor,也是跟文章主题相关的内容:一个流程配置节点图。

  然后发现其实基本就是各种图像混合后处理效果叠加,包括描边、边缘光、图像的颜色过渡、补光、模糊混合等,真要说的话其实并没有什么复杂的(专业搞美术的做出来的东西第一印象就很好,羡慕 ),但并不完全适合自由视角的卡通渲染。不过这个节点图确实是我很久以前就想做的一个编辑器扩展内容,方便项目里面对话文件的配置(原来用的多个ScriptableObject连接成一串对话,用起来没那么方便)。简单搜了一下发现这个节点图Unity提供的有现成的类方便扩展,关键词:GraphView,于是参考了网上的博客和Unity商店的插件,在自己的项目中实现了一个简单的对话配置系统。

效果展示

  实现起来主要包括四个部分:整个节点图(基类GraphView)、单独的一个节点(基类Node)、显示图的窗口(基类EditorWindow)、用于存储每个对话数据的文件(基类ScriptableObject)。(当然还需要一个用于读取对话数据并显示的对话系统,但这是另外的部分)
  最终实现的效果如下:

根据实际项目需求,节点内可以放入各种配置数据。在本项目里,每个节点都对应了某个人(不是人也行)说的多句话,并且可以点击按钮新增、删除对话内容等;右键点击可以呼出菜单,在鼠标位置新建节点;右键点击节点,除开始节点外,每个节点都可以添加或删除此段对话结束后可能出现的选项(Output);通过点击左上角或者Ctrl+S可以手动保存配置数据至ScriptableObject。

实现细节

  网上搜GraphView会有不少教程和完整代码,Unity商店的插件也是很好的学习资料,所以这里只简单讲一下项目里部分功能的实现方式。
  1. 右键点击呼出菜单栏的功能有两种实现方式,一种是使用GraphView.RegisterCallback<MouseDownEvent>()自己建立一个菜单(因为整个窗口是被GraphView填满的,项目里Ctrl+S保存用的同样的方式实现),另一种是重写Node中的BuildContextualMenu向菜单中加入选项,项目中实际使用了第二种,将创建节点、接口等放在了其中。

	graphView.RegisterCallback<MouseDownEvent>(MouseDown);
    private void MouseDown(MouseDownEvent _event)
    {
        if (_event.button == 1)
        {
            Vector2 _mousePos = _event.mousePosition;
            GenericMenu _menu = new GenericMenu();
            _menu.AddItem(new GUIContent("Create/New Node"), false, () => { /* do something */ });
            _menu.ShowAsContext();
        }
    }
	// in Node class
	public override void BuildContextualMenu(ContextualMenuPopulateEvent _event)
    {
        base.BuildContextualMenu(_event);
        _event.menu.AppendAction("Port/Add Port", (_a) => { /* do something */ });
    }

  2. 节点里各种UI元素通过Container.Add(inputContainer、mainContainer等)添加,布局可以采用默认的布局(如果没有需求的话),也可以使用uss、uxml自定义布局,在Create->UI Toolkit中可以创建,项目里使用了uss(其实基本就是css)定义布局。在uss里同样也是可以通过VisualElement的类型(比如Button、Label等)、名称(在代码里对应着VisualElement.name,虽然官方文档不建议多个元素命名相同,但项目里还是这么做了)、类(如果使用uxml预定义布局的话可以设置,在代码里不清楚怎么设置)进行布局。

	// .cs
	TextField _contentText = new TextField("", 400, true, false, '*');
    _contentText.name = "ContentText";
	/* .uss */
	TextField {
	    width: 303px;
	}
	
	#ContentText {
	    width: 280px;
	    height: 55px;
	}

  3. Unity无法直接序列化这类数据进行存储,这里可以采用Unity可以序列化的数据结构存储图,在需要编辑图时再从存储的数据中加载。项目里采用了List记录所有节点的数据,并使用List中节点的索引记录节点之间的连接。存储数据这部分主要就是通过GraphView.nodes.ToList()获取图中所有的节点,通过GraphView.edges.ToList()或者((Port)Node.outputContainer[i]).connections获取节点间连接的边(Edge),再通过Edge.input.nodeEdge.output.node获取节点间的连接关系。记得使用[System.Serializable]将自定义的存储类声明为可序列化,并在存储完数据后使用EditorUtility.SetDirty(ScriptableObject)让Unity知道文件中的数据被改变了,需要进行保存。

        List<DialogGraphNode> _dialogNodeList = graphView.nodes.ToList().Cast<DialogGraphNode>().ToList();

        dialogNodeSave = new List<DialogNodeSaveData>();
        foreach (DialogGraphNode _node in _dialogNodeList)
        {
            DialogNodeSaveData _nodeSaveData = new DialogNodeSaveData(_node.dialogNodeData, _node.title, _node.GetPosition());
            dialogNodeSave.Add(_nodeSaveData);
            if (_node == graphView.startNode) startNode = _nodeSaveData;
        }
		
		// ...
		
		EditorUtility.SetDirty(this);