前言

本人一直对节点编辑器比较感兴趣,最近看到有网友已经基于GraphView做出了自己的剧情编辑器,技能编辑器等,我也手痒难耐,并且眼馋GraphView的颜值很久了,决定来学习一下,并且准备把自己的技能编辑器移植过来。

NodeGraphProcessor版本:https://github.com/alelievr/NodeGraphProcessor/commit/bea17d70217f44509c30086ec04a4cfbe1836751

NodeGraphProcessor + Odin:https://github.com/wqaetly/NodeGraphProcessor

正文

GraphView介绍

GraphView是Unity推出的一个基于UIElement的节点编辑器UI模块,基建很完全,有多选,拖动,缩放,Group等功能,但是我没有找到官方的文档和示例,大家可以跟着这个UP主学习一下怎么从零开始使用GraphView做一个节点编辑器:Mert Kirimgeri

image-20210403143630771

他还是 NodeBasedDialogueSystem 的作者

image-20210403143824555

UIElement介绍

UIElement(现更名为UI ToolKit但是程序集名称还是UIElement,雀食没话说)是Unity新推出的一种UI解决方案,目标是一站式解决Editor+Runtime的UI设计需求,使用C# + HTML的形式进行开发,其中HTML用以定义UI样式和内容,C#引用HTML定义内容+绑定数据,并且与GamePlay进行交互。

我们来看看他的底层是怎么样的:

image-20210403130335944

  • UIElement使用了名为UIElements Renderer(UIR)的渲染后台,为UIElement渲染量身定制,尽可能提高性能。
  • 使用Retained Mode GUI (RMGUI)以及按需更新的模式,当UI元素没有发生变化的时候几乎0消耗。
  • 渲染时分配一个大的VB/IB缓冲区,使用特制的Uber Shader,减少渲染状态切换,一个DrawCall即可完成整个UIElements的绘制(即使UI元素发生变化,也会将发生变化的UI元素的VB/IB放入一开始分配的那个大的VB/IB缓冲区)。
  • 性能优化,把更多的工作下放到GPU去做,减少CPU开销,例如UI元素的属性会被存储在GPU内存上,GPU加速Clip,DynamicTransform(GPU加速UI元素的平移,缩放,旋转操作),GPU加速UI视口操作(缩放,平移,更改分辨率)。

此部分更多内容参见:Built for performance: the UIElements Renderer – Unite Copenhagen 2019

RMGUI和IMGUI对比

上面提到UIElement使用RMGUI的模式,并且基于其做了按需更新的模式达到性能优化的效果,但目前Unity编辑器拓展主流仍然是Immediate Mode GUI (IMGUI)的形式,他不保存任何状态写起来非常爽快,但是问题就是需要每帧无差别的收集所有VB/IB然后进行绘制操作,相比RMGUI按需更新的模式就比较消耗性能。

节点编辑器选型对比

经过上面的描述,我们可以得知RMGUI模式的UIElement是更加契合我们的节点编辑器,因为我们会有节点非常多的情况,并且对节点进行拖拽,对整个视口进行缩放这种频繁的操作,GraphView的渲染模式无疑更占优势。

那么当前比较流行的节点编辑器有哪些呢?

开源的有

Node_Editor_Framework:最早期的节点编辑器之一,我自己Moba项目的技能编辑器就是使用它来制作的,但是历史悠久一样为他带来了巨大的历史包袱,向下兼容导致的代码臃肿,功能冗余,不是很推荐了。

image-20210403132404909

xNode:算是比较现代化的一个节点编辑器了,一些设计理念和基建都比Node_Editor_Framework先进和完善的多,而且代码也比较清爽,比较推荐。

image-20210403132806360

NodeGraphProcessor:前面两个节点编辑器都是传统的IMGUI绘制,而NodeGraphProcessor是基于Unity GraphView的,它享受所有GraphView的特性和基建,性能和轻便性皆为上乘,但是需要自己处理序列化相关的内容,接入Odin相比较于前两者要麻烦一些,而且作者对于这部分的处理确实让人头大,很多地方为了支持序列化绕了一大圈,但这是小问题,芜锁胃,接入Odin可以减少非常多的不必要反射操作并剔除很多冗余代码,并且从官方的技术发展路线来看,NodeGraphProcessor是时代发展的必然结果,所以就决定是你了!

image-20210403133101269

NodeGraphProcessor架构分析

先来看看NodeGraphProcessor的运行架构,了解了之后才能更好的魔改

NodeGraphProcessor

Odin接入

由于NodeGraphProcessor使用了Unity自带的Json方案进行序列化反序列化,所以有非常多类型不支持(Dic,HashSet等),这点几乎是致命的,因为在游戏业务中这些泛型集合是非常常用的。Odin就可以很好的解决这个问题。

需要特别注意的有几点

  • 我们要把一些原本继承自ScriptableObject的对象改为继承Odin的SerializedScriptableObject,否则序列化反序列化支持不完善
  • 弃用SerializeField,Serializable,SerializeReference,ISerializationCallbackReceiver等Unity原生序列化相关Attribute和接口,他会与Odin的序列化冲突导致数据损坏,丢失等问题

步骤主要为:

  • 修改BaseGraph继承SerializedScriptableObject,并去除其中主要字段的Attribute标记,达到全盘Odin托管的目的,去掉为BaseGraph编写的CustomEditor(GraphAssetInspector),使其使用Odin的Inspector面板

  • 去掉为NodeInspectorObject的CustomEditor(NodeInspectorObjectEditor),因为NodeGraphProcessor本身使用了一种奇淫方法来绘制Node到Inspector上:当选中Node时,会查找其中是否有被ShowInInspectorAttribute标记的字段,如果有的话,就将这个节点加入NodeInspectorObject的selectedNodes中,并且将Selection.activeObject设置为NodeInspectorObject达成绘制的目的。当然如果想要所有Node都可以被Odin绘制在Inspector的话也非常简单

    1
    2
    // if (showInInspector != null)
    _needsInspector = true;
  • 还有一些零碎的地方需要修改,主要是访问权限和序列化相关的修改(哪里报错改哪里,多数是由于空引用),大家要多注意测试从Editor进入PlayMode之后Graph的变化,很有可能序列化方式不对导致数据被GC产生报错。

我的NodeGraphProcessor版本

我Fork了一份NodeGraphProcessor,并且接入了Odin插件,供参考

https://github.com/wqaetly/NodeGraphProcessor

QQ截图20210404172603

QQ截图20210404172629

QQ截图20210404172705

QQ截图20210404172717

QQ截图20210404172725

QQ截图20210404172956

更多

其实上面那些操作也只是完成了一个可视化节点编辑器而已,而更加具体的业务比如技能编辑器,任务编辑器的数据导出等操作,因为不具备太强的通用性,大家可以自行实现

可以参考一下我之前的一个知乎回答:如何实现一个RPG游戏中的对话树系统?

参考

Mert Kirimgeri的GraphView教程

NodeBasedDialogueSystem

Built for performance: the UIElements Renderer – Unite Copenhagen 2019

文刀秋二的IMGUI相关知乎回答

Unity UIElement文档

Node_Editor_Framework

xNode

NodeGraphProcessor

如何实现一个RPG游戏中的对话树系统?