zoukankan      html  css  js  c++  java
  • 迷宫地图编辑器:Xnode插件实践

    前言


    在unity开发游戏编辑器扩展中,我们常常会用到节点编辑器。节点编辑器就是用节点和节点连接的图形话编辑器,比如unity自带的shadergraph就是一个节点编辑器。用户通过节点之间的拖拖拉拉,连线就能得到想要的结果。

    在一段时间里,打算做一个迷宫的地图编辑器,每个地图有上下左右4个门,可以和其他地图连接。于是打算开发一个编辑器,通过可视化的方式去生成整个迷宫的数据。查了很多的节点插件,xnode作为轻量级的节点编辑器就非常适合用来做二次开发

    Xnode(https://github.com/Siccity/xNode)是开源的轻量级的节点编辑器插件。所以理解和开发起来非常方便

    最终开发效果如下。可以在窗口内创建节点,设置四个门的跳转关系,最终点击导出保存为一个指定的数据文件

    屏幕截图 2021-09-25 193709


    安装Xnode


    从github下载这个插件,解压出来放在Assets目录下即可

    不过这里我做了一点点改动。考虑到只想要xnode的编辑器功能,不打算打包的时候把xnode的代码带到最终的包里面,所以把runtime的代码全部放到了Editor目录里面。这样就可以保证打包时候,我们游戏的工程是和xnode相互独立的。

    如图所示,现在工程里面代码全部在Editor下面

    屏幕截图 2021-09-25 194330


    定义图和节点


    xnode是非常轻量级的非常易开发的。它的概念就是图Graph,节点Node,端口Port。端口我们可以先不关心。先来定义一下图

    [CreateAssetMenu(fileName = "mazelayer", menuName = "UmGame/场景/迷宫编辑器数据")]
    public class MazeLayerGraph : NodeGraph
    {
    
    }
    

    就是这么简单,继承一下NodeGraph即可。

    这里给MazeLayerGraph加了一个CreateAssetMenu标签。这样就可以在Project面板中,通过右键菜单的方式创建出这个图文件。

    屏幕截图 2021-09-25 195638

    创建之后,就可以通过双击这个文件或者在Inspector面板中点击Open或者点击EditGraph都可以打开

    屏幕截图 2021-09-25 200003

    打开的界面什么都没有,右键菜单里面也没什么东西

    屏幕截图 2021-09-25 200210

    这是因为还没有定义节点Node,所以开始定义一下Node

    也很简单,继承Node即可。这里因为后面打算要画一个迷宫房间的图,还要记录房间的ID,所以定义了一些变量。回到unity,右键就可以看到有一个菜单了

    屏幕截图 2021-09-25 200917

    选择这个菜单就可以创建出一个节点了

    屏幕截图 2021-09-25 201637

    但是这些数据不是我想在图中展示的,所以打算重写节点的绘制。


    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,在节点的右键菜单的时候加上一些动态加门的方法

    屏幕截图 2021-09-25 203058

    如图,添加4个门之后,就在节点周围画出了4个入口和4个出口

    多加几个节点,然后连接起来

    屏幕截图 2021-09-25 203300

    这个曲线不怎么满意,怎么办。


    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表示直线

    屏幕截图 2021-09-25 203756

    OK。那大部分就完成了。最后就是保存了。我们在MazeLayerGraphEditor中的OnGUI可以写一个buttom,然后把相关数据写到自定义的数据就可以了。



    个人博客请访问:http://www.cnblogs.com/CodeGize/
  • 相关阅读:
    [ PyQt入门教程 ] Qt Designer工具的使用
    [ PyQt入门教程 ] PyQt5开发环境搭建和配置
    Notepad++提升工作效率小技巧
    思考:测试人员如何快速成长
    Linux /tmp目录下执行脚本失败提示Permission denied
    使用Quartz实现定时任务
    算法篇(前序)——Java的集合
    深入理解 JVM(上)
    Linux集锦
    秒杀系统实现高并发的优化
  • 原文地址:https://www.cnblogs.com/CodeGize/p/15335459.html
Copyright © 2011-2022 走看看