在之前的引擎工具总结中提到了可以用XNode来写可视化,正好荷兰冬假期间就之前写的简易可视化对话编辑器来介绍下XNode以及相关使用。

关于XNode

  XNode是一个Unity可视化编辑器制作插件,同类的还有NodeGraphProcessor。个人因比较喜欢XNode的界面选择了(自称)轻量,用户友好的XNode。这里放下图也方便各位对比下。

XNode

NodeGraphProcessor

使用XNode

  XNode中分为Graph(整个可视化的图),Node(自定义的节点)和Port(节点间连线的端口)。为创建自定义的可视化编辑器,需要Graph脚本(创建对应的图),Node脚本(定义节点的内容),NodeEditor脚本(非必须,定义节点中的元素在图中如何绘制)。可以完全接入Odin并交由其控制,也可以使用Unity进行序列化并调用GUI进行绘制,个人选择的是后者(没钱买Odin.jpg)。另外虽然官方给了相应的演示工程,但那些加减乘除太简单了根本没用到定制,所以后续我会结合我代码内容进行简单讲解(需要细节而基础的可以移步官方文档)。

节点

  先从节点开始讲起吧,这是XNode最关键且必不可少的部分。但这部分XNode封装的比较好,继承Node后使用其给定的属性就可。大概就是类前可加[NodeWidth]限定宽度,类内元素加[Input], [Output]以决定是输入还是输出端。[OutPut]部分的ConnectionType.Multiple表示其作为输出节点可以把值传给多个输入节点,而对于列表输出则需要置真dynamicPortList以供XNode管理各节点连接信息。

1
2
3
4
5
6
7
8
[Serializable] [NodeWidth(350)]
public class DialogueNode : Node
{
[Input] public Type before;
[Output(connectionType = ConnectionType.Multiple)] public Type after;
[Output(dynamicPortList = true, connectionType = ConnectionType.Multiple)]
public List<Type> optionList = new ();
}

编辑器

  然后是编辑器,虽然非必须但如果你想整点什么花活就不得不修改这个。对于我这个对话编辑器来说就是单个对话项的输入输出端口数不定,这就需要继承NodeEditor去修改编辑器。另外值得一提的是这里比较底层Unity的EditorGUILayout是无效的,老老实实用EditorGUI自己搓对齐或者拿Odin来点科技与狠活吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[CustomNodeEditor(typeof(DialogueNode))]
public class DialogueNodeEditor : NodeEditor
{
DialogueNode node; DialogueGraph dialogGraph;
NodePort before, after;

public override void OnCreate()
{
base.OnCreate();
node = serializedObject.targetObject as DialogueNode;
}

public override void OnBodyGUI()
{
NodeEditorGUILayout.DynamicPortList("dialogueList", typeof(DialogueInfo), serializedObject,
IO.Output, ConnectionType.Override, TypeConstraint.Inherited, InitList);
}

//这里是在定义list中元素的排列以及给每个列表项赋予输入输出节点
void InitList(ReorderableList list)
{
int reorderableListIndex = -1;
SerializedProperty arrayData = serializedObject.FindProperty("dialogueList");
if (dialogGraph == null) dialogGraph = window.graph as DialogueGraph;

//列表项高度以及其中元素的排列
list.elementHeightCallback = (index) => {};
list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {};

//移动,添加,删除时自定义节点也要跟着变化
list.onReorderCallback = (ReorderableList list) => {};
list.onAddCallback = (ReorderableList list) => {};
list.onRemoveCallback = (ReorderableList list) => {};
}
}

  最后图这边其实也没什么说的,继承NodeGraph后CreateAssetMenu给下名称就行。但对于一个编辑器而言,得在这定义如何读取你图中那些节点以及如何传递信息,这点就交由各位去头疼了。个人仅简单的将获取信息和事件移动拆开,感觉这样逻辑清晰些?

1
2
3
4
5
6
7
[Serializable, CreateAssetMenu(fileName = "New Dialogue Graph", menuName = "Dialogue Graph")]
public class DialogueGraph : NodeGraph
{
void Init() {}
void GetInfo() {}
void MoveOn() {}
}

  上张最终完成的对话节点图,内容选取的是《胆怯色上发光的画布》中的一段。算是挺有感触的。前进一步亦或后退一步,你又会作何选择呢?

胆怯色上发光的画布

写在最后

  XNode的简单说明就到此结束了,编辑器工具看似简单,但真做起来也是极为费事,毕竟后续我还引入了对变量和函数的反射以实现检索和事件节点(InstanceID和GUID存储,GenericMenu显示)并封装DialogueSystem靠Next()读取图中信息。完成后也学XNode上架Unity Assets的同时将工具和源码就开源到Github,希望对后来者有些许帮助。

对话系统的四种节点