前言
在unity开发游戏编辑器扩展中,我们常常会用到节点编辑器。节点编辑器就是用节点和节点连接的图形话编辑器,比如unity自带的shadergraph就是一个节点编辑器。用户通过节点之间的拖拖拉拉,连线就能得到想要的结果。
在一段时间里,打算做一个迷宫的地图编辑器,每个地图有上下左右4个门,可以和其他地图连接。于是打算开发一个编辑器,通过可视化的方式去生成整个迷宫的数据。查了很多的节点插件,xnode作为轻量级的节点编辑器就非常适合用来做二次开发
Xnode(https://github.com/Siccity/xNode)是开源的轻量级的节点编辑器插件。所以理解和开发起来非常方便
最终开发效果如下。可以在窗口内创建节点,设置四个门的跳转关系,最终点击导出保存为一个指定的数据文件
安装Xnode
从github下载这个插件,解压出来放在Assets目录下即可
不过这里我做了一点点改动。考虑到只想要xnode的编辑器功能,不打算打包的时候把xnode的代码带到最终的包里面,所以把runtime的代码全部放到了Editor目录里面。这样就可以保证打包时候,我们游戏的工程是和xnode相互独立的。
如图所示,现在工程里面代码全部在Editor下面
定义图和节点
xnode是非常轻量级的非常易开发的。它的概念就是图Graph,节点Node,端口Port。端口我们可以先不关心。先来定义一下图
[CreateAssetMenu(fileName = "mazelayer", menuName = "UmGame/场景/迷宫编辑器数据")] public class MazeLayerGraph : NodeGraph
{
}
就是这么简单,继承一下NodeGraph即可。
这里给MazeLayerGraph加了一个CreateAssetMenu标签。这样就可以在Project面板中,通过右键菜单的方式创建出这个图文件。
创建之后,就可以通过双击这个文件或者在Inspector面板中点击Open或者点击EditGraph都可以打开
打开的界面什么都没有,右键菜单里面也没什么东西
这是因为还没有定义节点Node,所以开始定义一下Node
也很简单,继承Node即可。这里因为后面打算要画一个迷宫房间的图,还要记录房间的ID,所以定义了一些变量。回到unity,右键就可以看到有一个菜单了
选择这个菜单就可以创建出一个节点了
但是这些数据不是我想在图中展示的,所以打算重写节点的绘制。
Node重绘
xnode中实现Node重绘非常简单,只要类似unity写一个CustomEditor就可以。对二次开发非常友好。
namespace Game {
[CustomNodeEditor(typeof(MazeLayerRoomNode))]
public class MazeLayerRoomNodeEditor : NodeEditor
{
public override void OnHeaderGUI()
{
var tar = (MazeLayerRoomNode)target;
var head = tar.RoomID.ToString();
if (!string.IsNullOrEmpty(tar.RoomName))
head += ":" + tar.RoomName;
EditorGUILayout.LabelField(head, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
}
public override int GetWidth()
{
return 115;
}
public Rect[] Points = new Rect[4];
public override void OnBodyGUI()
{
var tar = (MazeLayerRoomNode)target;
var width = GetWidth();
if (tar.Tex == null)
tar.Tex = AssetDatabase.LoadAssetAtPath<Texture2D>(
$"Assets/Game/Art/Map/levelmap/{tar.TerrianID}.png");
GUILayout.Label("", GUILayout.Width(width - 20), GUILayout.Height(210 / 2f));
var r = new Rect(20, 35, 150 / 2f, 210 / 2f);
if (tar.Tex != null)
EditorGUI.DrawPreviewTexture(r, tar.Tex);
else
EditorGUI.DrawRect(r, Color.white);
var height = GUILayoutUtility.GetLastRect().yMax;
EditorGUILayout.LabelField("", GUILayout.Height(height));
var rect = new Rect(0, 0, 16, 16);
foreach (var port in target.DynamicPorts)
{
var rport = port;
var strs = rport.fieldName.Split('_');
if (strs.Length != 2)
continue;
var doorid = (EmDoorID)int.Parse(strs[0]);
switch (doorid)
{
case EmDoorID.Bottom:
rect.y = height;
if (rport.IsInput)
rect.x = width / 2f + 8;
else
rect.x = width / 2f - 8 - 16;
break;
case EmDoorID.Left:
rect.x = 0;
if (rport.IsInput)
rect.y = height / 2f - 8 - 16;
else
rect.y = height / 2f + 8;
rect.y += 15;
break;
case EmDoorID.Right:
rect.x = width - 16;
if (rport.IsInput)
rect.y = height / 2f + 8;
else
rect.y = height / 2f - 8 - 16;
rect.y += 15;
break;
case EmDoorID.Top:
rect.y = 0;
if (rport.IsInput)
rect.x = width / 2f - 8 - 16;
else
rect.x = width / 2f + 8;
break;
default:
throw new ArgumentOutOfRangeException();
}
var color = rport.IsInput ? Color.red : Color.green;
NodeEditorGUILayout.DrawPortHandle(rect, Color.black, color);
portPositions[rport] = rect.center;
}
}
public override void AddContextMenuItems(GenericMenu menu)
{
base.AddContextMenuItems(menu);
var tar = (MazeLayerRoomNode)target;
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node)
{
menu.AddItem(new GUIContent("增加门/↑上"), false, () => tar.AddDoor(EmDoorID.Top));
menu.AddItem(new GUIContent("增加门/↓下"), false, () => tar.AddDoor(EmDoorID.Bottom));
menu.AddItem(new GUIContent("增加门/上下"), false, () =>
{
tar.AddDoor(EmDoorID.Bottom);
tar.AddDoor(EmDoorID.Top);
});
menu.AddItem(new GUIContent("增加门/←左"), false, () => tar.AddDoor(EmDoorID.Left));
menu.AddItem(new GUIContent("增加门/→右"), false, () => tar.AddDoor(EmDoorID.Right));
menu.AddItem(new GUIContent("增加门/左右"), false, () =>
{
tar.AddDoor(EmDoorID.Right);
tar.AddDoor(EmDoorID.Left);
});
menu.AddItem(new GUIContent("增加门/上下左右"), false, () =>
{
tar.AddDoor(EmDoorID.Bottom);
tar.AddDoor(EmDoorID.Top);
tar.AddDoor(EmDoorID.Right);
tar.AddDoor(EmDoorID.Left);
});
}
}
}
}
1、继承NodeEditor,比用CustomNodeEditor和我们自定义的Node相关联
2、复写了OnHeaderGUI,让节点头部显示房间的ID和名称,这样查看比较方便
3、复写GetWidth。因为我知道画的图的大小,其他不想显示,所以固定为115
4、复写OnBodyGUI。这个是重点。因为在节点画出图,所以在这个函数里面,先去AssetDataBase里面找到图,然后使用DrawPreviewTexture画出来。最后我要画出门的出入口,所以使用Node的DynamicPorts存储门的数据。最后把这些port的位置记录到portPositions数组中,这样到时候去就可以去做连线了。
5、复写AddContextMenuItems,在节点的右键菜单的时候加上一些动态加门的方法
如图,添加4个门之后,就在节点周围画出了4个入口和4个出口
多加几个节点,然后连接起来
这个曲线不怎么满意,怎么办。
Graph重绘
在xnode中,不仅Node可以重绘,Graph也可以
[CustomNodeGraphEditor(typeof(MazeLayerGraph))] public class MazeLayerGraphEditor : NodeGraphEditor {
public override NoodlePath GetNoodlePath(NodePort output, NodePort input)
{
return NoodlePath.Straight;
}
}
类似的,我们只要继承NodeGraphEditor,并用CustomNodeGraphEditor和我们自定义的Graph关联就可以了。
为了实现曲线变直线,我们只要复写GetNoodlePath即可,NoodlePath.Straight表示直线
OK。那大部分就完成了。最后就是保存了。我们在MazeLayerGraphEditor中的OnGUI可以写一个buttom,然后把相关数据写到自定义的数据就可以了。