NodeGraphProcessor整合Odin
前言
本人一直对节点编辑器比较感兴趣,最近看到有网友已经基于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
他还是 NodeBasedDialogueSystem 的作者
UIElement介绍
https://www.lfzxb.top/unity-ui-element-total
RMGUI和IMGUI对比
上面提到UIElement使用RMGUI的模式,并且基于其做了按需更新的模式达到性能优化的效果,但目前Unity编辑器拓展主流仍然是Immediate Mode GUI (IMGUI)的形式,他不保存任何状态写起来非常爽快,但是问题就是需要每帧无差别的收集所有VB/IB然后进行绘制操作,相比RMGUI按需更新的模式就比较消耗性能。
节点编辑器选型对比
经过上面的描述,我们可以得知RMGUI模式的UIElement是更加契合我们的节点编辑器,因为我们会有节点非常多的情况,并且对节点进行拖拽,对整个视口进行缩放这种频繁的操作,GraphView的渲染模式无疑更占优势。
那么当前比较流行的节点编辑器有哪些呢?
开源的有
Node_Editor_Framework:最早期的节点编辑器之一,我自己Moba项目的技能编辑器就是使用它来制作的,但是历史悠久一样为他带来了巨大的历史包袱,向下兼容导致的代码臃肿,功能冗余,不是很推荐了。
xNode:算是比较现代化的一个节点编辑器了,一些设计理念和基建都比Node_Editor_Framework先进和完善的多,而且代码也比较清爽,比较推荐。
NodeGraphProcessor:前面两个节点编辑器都是传统的IMGUI绘制,而NodeGraphProcessor是基于Unity GraphView的,它享受所有GraphView的特性和基建,性能和轻便性皆为上乘,但是需要自己处理序列化相关的内容,接入Odin相比较于前两者要麻烦一些,但从官方的技术发展路线来看,NodeGraphProcessor是时代发展的必然结果,所以就决定是你了!
NodeGraphProcessor架构分析
先来看看NodeGraphProcessor的运行架构,了解了之后才能更好的魔改
Odin接入
由于NodeGraphProcessor使用了Unity的默认方案进行序列化反序列化,所以有非常多类型不支持(Dic,HashSet等),这点几乎是致命的,因为在游戏业务中这些泛型集合是非常常用的。Odin就可以很好的解决这个问题。
需要特别注意的有几点
-
我们要把一些原本继承自ScriptableObject的对象改为继承Odin的SerializedScriptableObject,否则序列化反序列化支持不完善
-
弃用一部分SerializeField,Serializable,SerializeReference,ISerializationCallbackReceiver等System或Unity原生序列化相关Attribute和接口,他会与Odin的序列化冲突导致数据损坏,丢失等问题
-
由于原仓库使用了SerializeReference序列化
List<BaseNode>
引用,这个特性会递归序列化所有引用字段,所以BaseNode中的一些集合字段例如1
2
3
4
5
6
7
8
9
10
11[ ]
public NodeInputPortContainer inputPorts;
[ ]
public NodeOutputPortContainer outputPorts;
[ ]
internal Dictionary< string, NodeFieldInformation > nodeFields = new Dictionary< string, NodeFieldInformation >();
[ ]
internal Dictionary< Type, CustomPortTypeBehaviorDelegate> customPortTypeBehaviorMap = new Dictionary<Type, CustomPortTypeBehaviorDelegate>();
Stack<PortUpdate> fieldsToUpdate = new Stack<PortUpdate>();
HashSet<PortUpdate> updatedFields = new HashSet<PortUpdate>();
//////////////////////////////////////////////////////////////////////依旧会被作为引用序列化,但如果使用Odin序列化方案,这些内容并不会被序列化并且在进入PlayMode后会进行GC置空,但好在这些字段都是实时计算的,所以我们只需要在BaseNode初始化的时候对这些字段进行初始化即可。
步骤主要为(具体修改内容参见此Commit:https://github.com/wqaetly/NodeGraphProcessor/commit/2a68396239d92773e827a63c0c44efb5383cccfe ):
-
修改BaseGraph继承SerializedScriptableObject,并去除其Serializable特性,达到Odin托管序列化的目的,修改OnBeforeSerialize()和OnAfterDeserialize()为SerializedScriptableObject中的保护虚函数重写版本,去掉为BaseGraph编写的CustomEditor(GraphAssetInspector),使其使用Odin的Inspector面板
-
修改BaseNode,去除其Serializable特性,达到Odin托管序列化的目的,在Initialize函数中进行字段的初始化
1
2
3
4
5
6
7
8
9
10
11public void Initialize(BaseGraph graph)
{
this.graph = graph;
ExceptionToLog.Call(() => Enable());
inputPorts = new NodeInputPortContainer(this);
outputPorts = new NodeOutputPortContainer(this);
nodeFields = new Dictionary<string, NodeFieldInformation>();
customPortTypeBehaviorMap = new Dictionary<Type, CustomPortTypeBehaviorDelegate>();
InitializeInOutDatas();
InitializePorts();
}修改UpdatePortsForField函数,初始化fieldsToUpdate和updatedFields字段
1
2
3
4
5public bool UpdatePortsForField(string fieldName, bool sendPortUpdatedEvent = true)
{
bool changed = false;
fieldsToUpdate ??= new Stack<PortUpdate>();
updatedFields ??= new HashSet<PortUpdate>(); -
去掉为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
更多
其实上面那些操作也只是完成了一个可视化节点编辑器而已,而更加具体的业务比如技能编辑器,任务编辑器的数据导出等操作,因为不具备太强的通用性,大家可以自行实现
可以参考一下我之前的一个知乎回答:如何实现一个RPG游戏中的对话树系统?
参考
Built for performance: the UIElements Renderer – Unite Copenhagen 2019