本人在第一家公司负责剧情工作流的搭建,随想到了节点编辑器,节点编辑器对树状数据有非常强的处理能力,而游戏剧情可能有不同选项,不同的选项对应相同或者不同的剧情分支,树形结构数据可以几乎完美的承载这一类型的剧情数据。

节点编辑器的能力远不止于编辑剧情。

在下面我将给出列出一个基于unity实现节点编辑器的基本思路。

基本思路最初来源:感谢这位老哥的搬运,在Unity中创建基于Node节点的编辑器 

基本思路最初来源原文地址:http://gram.gs/gramlog/creating-node-based-editor-unity/

实现基础界面的思路大家可以移步原文地址,作者思路写的非常的清晰。

我在基本界面的基础上增加以下功能

1.数据编辑的接入

2.放大缩小面板

3.多选节点

4.数据存档打包方案

说明

本人写这篇博客的主要目的是记录自己入门工具仔的首作,分享一些工具拓展的基本思路,并不是推荐大家直接使用我的源码。

事实上成熟的节点编辑插件已有不少(如:xnode,NodeGraphProcessor),我们在考虑做功能的时候某种程度上来说应该优先考虑更成熟的代码结构以及数据编辑框架。

源码

NodeGraph

节点图形数据

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace MDDGameFramework.MDDNodeEditor
{
    [Serializable]
    public class NodeGraph
    {
        /// <summary>
        /// 节点大小与位置信息
        /// </summary>
        public Rect rect;
        public int ID;

        //状态
        public bool isDragged;
        public bool isSelected;

        private GUIStyle style;
        private GUIStyle defaultNodeStyle;
        private GUIStyle selectedNodeStyle;

        private Action<NodeGraph> OnRemoveNode;

        //输入点连接节点列表和输出点连接节点列表
        public List<int> inPointList = new List<int>();
        public List<int> outPointList = new List<int>();//可能之后最常用的是outPoint

        //输入点和输出点
        public ConnectionPoint inPoint;//输入点
        public ConnectionPoint outPoint;//输出点

        public NodeGraph() { }

        public NodeGraph(NodeGraph nodeGraph)
        {
            
        }

        public NodeGraph(Vector2 position, float width, float height,
        GUIStyle nodeStyle, GUIStyle selectedStyle, GUIStyle inPointStyle, GUIStyle outPointStyle,
        Action<ConnectionPoint> OnClickInPoint, Action<ConnectionPoint> OnClickOutPoint,Action<NodeGraph> OnClickRemoveNode,
        int ID)
        {
            rect = new Rect(position.x, position.y, width, height);
            style = nodeStyle;
            inPoint = new ConnectionPoint(this, ConnectionPointType.In, inPointStyle, OnClickInPoint);
            outPoint = new ConnectionPoint(this, ConnectionPointType.Out, outPointStyle, OnClickOutPoint);
            defaultNodeStyle = nodeStyle;
            selectedNodeStyle = selectedStyle;
            OnRemoveNode = OnClickRemoveNode;
            this.ID = ID;
        }

        public void InitNode(GUIStyle nodeStyle, GUIStyle selectedStyle,
       Action<NodeGraph> OnClickRemoveNode)
        {
            //inPoint.OnClickConnectionPoint = OnClickInPoint;
            //outPoint.OnClickConnectionPoint = OnClickOutPoint;

            style = nodeStyle;
            defaultNodeStyle = nodeStyle;
            selectedNodeStyle = selectedStyle;
            OnRemoveNode = OnClickRemoveNode;
        }


        public void Drag(Vector2 delta)
        {
            rect.position += delta;
        }

        string title = "";

        public void Draw()
        {
            Matrix4x4 matrix4X4 = GUI.matrix;
            title = ID.ToString() + " ";
            //title += name;
            GUI.skin.label.fontSize = 7;
            GUI.skin.label.fontStyle = FontStyle.Normal;
            GUI.skin.label.alignment = TextAnchor.MiddleCenter;
            rect.size = (new Vector2(50f, 12.5f));
            GUI.Box(rect, "", style);
            GUI.Label(rect, title);
            inPoint.Draw();
            outPoint.Draw();
            GUIUtility.ScaleAroundPivot(new Vector2(0.25f, 0.25f), rect.center);
            GUI.skin.label.fontSize = 12;
            GUI.skin.label.alignment = TextAnchor.MiddleCenter;
          
            GUI.matrix = matrix4X4;


            if (isSelected)
            {
                style = selectedNodeStyle;
            }
            else
            {
                style = defaultNodeStyle;
            }
        }


        public bool ProcessEvents(Event e)
        {
            switch (e.type)
            {
                case EventType.MouseDown:
                    if (e.button == 0)
                    {
                        if (rect.Contains(e.mousePosition))
                        {
                            isDragged = true;
                            GUI.changed = true;
                            isSelected = true;
                            style = selectedNodeStyle;
                            //NewEditorWindow.instance.RegisterNode(this);
                        }
                        else
                        {
                            GUI.changed = true;
                            isSelected = false;
                            style = defaultNodeStyle;
                        }
                    }
                    break;
                case EventType.MouseUp:
                    isDragged = false;
                    break;

                case EventType.MouseDrag:
                    if (e.button == 0 && isDragged)
                    {
                        Drag(e.delta);
                        e.Use();
                        return true;
                    }
                    break;
            }

            return false;
        }

    }
}

Connection

节点之间的连接线数据类

using System;
using UnityEditor;
using UnityEngine;

namespace MDDGameFramework.MDDNodeEditor
{
    [Serializable]
    public class Connection
    {
        public ConnectionPoint inPoint;
        public ConnectionPoint outPoint;
        private Action<Connection> OnClickRemoveConnection;
        int i;

        public Connection(ConnectionPoint inPoint, ConnectionPoint outPoint, Action<Connection> OnClickRemoveConnection)
        {
            this.inPoint = inPoint;
            this.outPoint = outPoint;
            this.OnClickRemoveConnection = OnClickRemoveConnection;
        }
    }
}

ConnectionPoint

节点两侧连接点数据类

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MDDGameFramework.MDDNodeEditor
{
    [Serializable]
    public class ConnectionPoint
    {
        public int id;
        public Rect rect;

        public NodeGraph node;
        private GUIStyle inPointStyle;
        private GUIStyle outPointStyle;
        private GUIStyle pointStyle;

        public ConnectionPointType type;

        public Action<ConnectionPoint> OnClick;

        public ConnectionPoint(NodeGraph node, ConnectionPointType type, GUIStyle pointStyle, Action<ConnectionPoint> OnClick)
        {
            id = Guid.NewGuid().GetHashCode();

            this.node = node;

            this.pointStyle = pointStyle;

            this.OnClick = OnClick;

            this.type = type;

            rect = new Rect(0, 0, 2.8f, 5f);
        }

        public void InitConnectionPoint(GUIStyle style, Action<ConnectionPoint> OnClickConnectionPoint)
        {
            this.pointStyle = style;
            this.OnClick = OnClickConnectionPoint;
        }


        public void Draw()
        {
            rect.y = node.rect.y + (node.rect.height * 0.5f) - rect.height * 0.5f;

            switch (type)
            {
                case ConnectionPointType.In:
                    rect.x = node.rect.x - rect.width + 2f;
                    break;

                case ConnectionPointType.Out:
                    rect.x = node.rect.x + node.rect.width - 2f;
                    break;
            }


            if (GUI.Button(rect, "", pointStyle))
            {
                Debug.LogError(OnClick.Method.Name);

                if (OnClick != null)
                {
                    OnClick(this);
                }
            }
        }



    }

    [Serializable]
    public enum ConnectionPointType
    {
        In,
        Out
    }
}

INodeData

节点数据类接口


namespace MDDGameFramework.MDDNodeEditor
{
    public interface INodeData
    {
        int id { get; }
        void OnEnter();
        void OnUpdate();
        void OnExit();
    }
}

NodeData

节点数据类

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace MDDGameFramework.MDDNodeEditor
{
    public class NodeData:INodeData
    {

        public int ID;

        public string text;
        public Dictionary<int, string> butDic;
        public List<int> outPointList;
        public List<int> intPointList;



        public int id
        {
            get { return ID; }
        }

        public NodeData() { }

        public NodeData(int id,string text,List<int> outPointList, List<int> inPointList)
        {
            this.ID = id;
            this.text = text;
            this.outPointList = outPointList;
            this.intPointList = inPointList;

            butDic = new Dictionary<int, string>();
        }

        public void OnEnter()
        {
            
        }

        public void OnExit()
        {
            
        }

        public void OnUpdate()
        {
            
        }
    }

    public class NodeDataList
    {
        public List<NodeData> nodeDatas = new List<NodeData>();

        public NodeDataList() { }

        public NodeDataList(List<NodeData> list)
        {
            nodeDatas = list;
        }
    }
      
}

NodeGraphWindow

节点绘制面板

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Linq;

namespace MDDGameFramework.MDDNodeEditor
{
    public class NodeGraphWindow : EditorWindow
    {
        public static EditorWindow currentWindow;

        private int ID = 0;
        private float menuBarHeight = 20f;
        private Rect menuBar;
        private String SaveFileName;
        private String LoadFileName;

        private GUIStyle nodeStyle;
        private GUIStyle inPointStyle;
        private GUIStyle outPointStyle;
        private GUIStyle selectedNodeStyle;

        private List<NodeGraph> nodes;
        private List<Connection> connections;

        private List<NodeData> nodeDatas;

        private Vector2 drag;
        private Vector2 offset;
        private bool SSS;


        private ConnectionPoint selectedInPoint;
        private ConnectionPoint selectedOutPoint;

        private Vector2 scale = new Vector2(4, 4);

        [MenuItem("Tools/剧情编辑器 _&W")]
        public static void OpenScenePointEditor()
        {
            currentWindow = GetWindow(typeof(NodeGraphWindow));

            NodeEditorWindow.OpenEditorWindow();
        }

        private void OnFocus()
        {
            isFocus = true;
            if (currentWindow == null)
            {
                currentWindow = GetWindow(typeof(NodeGraphWindow));
                Debug.Log("OnFocus Refresh Current Window Because of Rebuild the Script!");
            }
        }

        private void OnEnable()
        {
            ID = 0;//每次开启ID归零

            nodeStyle = new GUIStyle();
            nodeStyle.normal.background = EditorGUIUtility.Load("Assets/PlotEditor/Picture/黑色.jpg") as Texture2D;
            nodeStyle.border = new RectOffset(12, 12, 12, 12);

            selectedNodeStyle = new GUIStyle();
            selectedNodeStyle.normal.background = EditorGUIUtility.Load("Assets/PlotEditor/Picture/粉色.jpg") as Texture2D;
            selectedNodeStyle.border = new RectOffset(12, 12, 12, 12);

            inPointStyle = new GUIStyle();
            inPointStyle.normal.background = EditorGUIUtility.Load("Assets/PlotEditor/Picture/水泡泡.jpg") as Texture2D;
            inPointStyle.active.background = EditorGUIUtility.Load("Assets/PlotEditor/Picture/水泡泡.jpg") as Texture2D;
            //inPointStyle.border = new RectOffset(4, 4, 12, 12);

            outPointStyle = new GUIStyle();
            outPointStyle.normal.background = EditorGUIUtility.Load("Assets/PlotEditor/Picture/水泡泡.jpg") as Texture2D;
            outPointStyle.active.background = EditorGUIUtility.Load("Assets/PlotEditor/Picture/水泡泡.jpg") as Texture2D;
            //outPointStyle.border = new RectOffset(4, 4, 12, 12);
        }

        private void OnGUI()
        {
            Matrix4x4 oldMatrix = GUI.matrix;
            GUIUtility.ScaleAroundPivot(scale, new Vector2(position.width / 2, position.height / 2));
            ProcessNodeEvents(Event.current);
            ProcessEvents(Event.current);
            DrawGrid(20, 0.2f, Color.black);
            DrawGrid(100, 0.4f, Color.black);
            DrawNodes();
            DrawConnections();
            DrawConnectionLine(Event.current);
            DrawRectEvent(Event.current);
            GUI.matrix = oldMatrix;
            DrawMenuBar();
            if (GUI.changed) Repaint();
        }

        private void DrawNodes()//通过 nodes List 的数据绘画出节点 
        {
            if (nodes != null)
            {
                for (int i = 0; i < nodes.Count; i++)
                {
                    nodes[i].Draw();
                }
            }
        }

        bool isFocus;       

        Vector3 begin;
        bool beginRect = false;
        bool firstRect = true;
        private void ProcessEvents(Event e)//  编辑器的各种点击事件处理
        {
            drag = Vector2.zero;          
            switch (e.type)
            {
                case EventType.MouseDown:
                    if (e.button == 1 && !SSS)
                    {
                        ProcessContextMenu(e.mousePosition);
                    }
                    break;
                case EventType.MouseDrag:
                    if (e.button == 0)
                    {
                        drag = e.delta;
                        if (isFocus)
                        {
                            OnDrag(Vector2.zero);
                            isFocus = false;
                        }
                        else
                        {
                            OnDrag(drag);
                        }
                    }

                    break;
                case EventType.KeyDown:
                    if (e.keyCode.ToString() == "Q")
                    {
                        OnClickAddNode(e.mousePosition);
                    }
                    break;
                case EventType.ScrollWheel:
                    //e.delta.normalized.y 下滑为1, 上滚为-1
                    if (e.delta.normalized.y < 0 && scale.x < 4)
                    {
                        scale += new Vector2(0.08f, 0.08f);
                    }
                    else if (e.delta.normalized.y > 0 && scale.x > 1.1)
                    {
                        scale -= new Vector2(0.08f, 0.08f);
                    }
                    break;
            }
        }

        //返回框内的节点
        private bool ReturnInsert(float x, float x1, float value)
        {
            bool isIn = false;
            if (x >= x1)
            {
                if (value < x && value >= x1)
                {
                    isIn = true;
                }
                else
                {
                    isIn = false;

                }
            }
            else
            {
                if (value < x1 && value >= x)
                {
                    isIn = true;
                }
                else
                {
                    isIn = false;
                }
            }

            return isIn;
        }


        private void DrawRectEvent(Event e)
        {
            if (nodes == null)
                return;

            if (e.type == EventType.KeyDown)
            {
                if (e.keyCode.ToString() == "A")
                {
                    beginRect = true;
                }
            }

            if (e.type == EventType.KeyUp && e.keyCode.ToString() == "A")
            {
                beginRect = false;
                firstRect = true;


                for (int i = 0; i < nodes.Count; i++)
                {
                    if (ReturnInsert(begin.x, e.mousePosition.x, nodes[i].rect.center.x) && ReturnInsert(begin.y, e.mousePosition.y, nodes[i].rect.center.y))
                    {
                        nodes[i].isSelected = true;
                    }
                    else
                    {
                        nodes[i].isSelected = false;
                    }
                }
            }
            if (beginRect)
            {
                if (firstRect)
                {
                    begin = e.mousePosition;
                    firstRect = false;
                }
                DrawRect(begin, e.mousePosition);
            }
        }

        private void ProcessContextMenu(Vector2 mousePosition)//右键弹出 上下文菜单  (非节点)
        {
            GenericMenu genericMenu = new GenericMenu();
            genericMenu.AddItem(new GUIContent("Add node"), false, () => OnClickAddNode(mousePosition));
            genericMenu.ShowAsContext();
        }


        private void ProcessNodeEvents(Event e)//点击节点的事件处理
        {
            if (nodes != null)
            {
                for (int i = nodes.Count - 1; i >= 0; i--)
                {
                    bool guiChanged = nodes[i].ProcessEvents(e);
                    if (nodes[i].isSelected)
                    {
                        foreach (var v in nodeDatas)
                        {
                            if (v.id == nodes[i].ID)
                            {
                                NodeEditorWindow.nodeEditorWindow.RegisterNode(nodeDatas[i]);
                            }
                        }

                      
                    }
                    if (nodes[i].isSelected && e.keyCode.ToString() == "F" && e.type == EventType.KeyDown)
                    {
                        //OnClickAddNode(e.mousePosition);
                        ID += 1;

                        //nodes.Add(new NodeGraph(e.mousePosition, nodes[i], nodeStyle, selectedNodeStyle, inPointStyle, outPointStyle, OnClickInPoint, OnClickOutPoint, OnClickRemoveNode, ID));
                    }
                    if (nodes[i].isSelected && e.keyCode.ToString() == "Delete" && e.type == EventType.KeyDown)
                    {
                        OnClickRemoveNode(nodes[i]);
                    }
                    if (e.button == 1 && nodes[i].isSelected && nodes[i].rect.Contains(e.mousePosition))
                    {
                        SSS = true;
                        ProcessContextMenuNode(nodes[i]);
                    }
                    if (e.isScrollWheel)
                    {
                        SSS = false;
                    }
                    if (guiChanged)
                    {
                        GUI.changed = true;
                    }
                }
            }
        }

        private void ProcessContextMenuNode(NodeGraph node)
        {
            GenericMenu genericMenu = new GenericMenu();
            genericMenu.AddItem(new GUIContent("Remove node"), false, () => OnClickRemoveNode(node));
            genericMenu.ShowAsContext();
        }

        private void OnClickAddNode(Vector2 mousePosition)//添加一个新的节点
        {
            if (nodes == null)
            {
                nodes = new List<NodeGraph>();
                nodeDatas = new List<NodeData>();
            }
            ID += 1;

            nodes.Add(new NodeGraph(mousePosition, 200, 50, nodeStyle, selectedNodeStyle, inPointStyle, outPointStyle, OnClickInPoint, OnClickOutPoint, OnClickRemoveNode, ID));
            nodeDatas.Add(new NodeData(ID,"",nodes[nodes.Count-1].outPointList, nodes[nodes.Count - 1].inPointList));
        }

        [System.Obsolete]
        private void DrawConnections()//绘画 贝塞尔曲线
        {
            if (connections != null)
            {
                for (int i = 0; i < connections.Count; i++)
                {
                    DrawC(connections[i].inPoint, connections[i].outPoint, connections[i]);
                }
            }
        }
        [Obsolete]
        public void DrawC(ConnectionPoint inPoint, ConnectionPoint outPoint, Connection connectionT)
        {
            Handles.DrawBezier(
                inPoint.rect.center,
                outPoint.rect.center,
                inPoint.rect.center + Vector2.left * 12.5f,
                outPoint.rect.center - Vector2.left * 12.5f,
                Color.white,
                null,
                0.5f
            );

            if (Handles.Button((inPoint.rect.center + outPoint.rect.center) * 0.5f, Quaternion.identity, 1, 8, Handles.RectangleCap))
            {
                OnClickRemoveConnection(connectionT);
            }
        }



        private void OnClickInPoint(ConnectionPoint inPoint)//点击input节点
        {
            selectedInPoint = inPoint;

            if (selectedOutPoint != null)//如果此时另一实时索引点不为空  则创建连接
            {
                if (selectedOutPoint.node != selectedInPoint.node)
                {
                    CreateConnection();
                    ClearConnectionSelection();
                }
                else
                {
                    ClearConnectionSelection();
                }
            }
        }
        private void OnClickOutPoint(ConnectionPoint outPoint)//点击output节点
        {
            selectedOutPoint = outPoint;

            if (selectedInPoint != null)//如果此时另一实时索引点不为空  则创建连接
            {
                if (selectedOutPoint.node != selectedInPoint.node)
                {
                    CreateConnection();
                    ClearConnectionSelection();
                }
                else
                {
                    ClearConnectionSelection();
                }
            }
        }

        private void CreateConnection()// 正式 创建一条新的贝塞尔曲线
        {
            if (connections == null)
            {
                connections = new List<Connection>();
            }

            connections.Add(new Connection(selectedInPoint, selectedOutPoint, OnClickRemoveConnection));

            //selectedOutPoint.node.OutString.Add("");
            //确立创建贝塞尔曲线之后的索引关系
            selectedInPoint.node.inPointList.Add(selectedOutPoint.node.ID);
            selectedOutPoint.node.outPointList.Add(selectedInPoint.node.ID);

            if (selectedOutPoint.type == ConnectionPointType.Out)
            {
                foreach (var v in nodeDatas)
                {
                    if (v.id == selectedOutPoint.node.ID)
                    {
                        v.butDic.Add(selectedInPoint.node.ID, "");
                    }
                }
            }
            else if (selectedInPoint.type == ConnectionPointType.Out)
            {
                foreach (var v in nodeDatas)
                {
                    if (v.id == selectedInPoint.node.ID)
                    {
                        v.butDic.Add(selectedOutPoint.node.ID, "");
                    }
                }
            }

        }

        private void OnClickRemoveConnection(Connection connection)//移除节点之间的关联曲线
        {
            //同时移除相对应的选项文本
            //connection.outPoint.node.r.Remove(connection.outPoint.node.OutString[connection.outPoint.node.outPointList.IndexOf(connection.inPoint.node.ID)]);

            // 删除贝塞尔曲线的同时删除所连两端节点的索引关系
            connection.outPoint.node.outPointList.Remove(connection.inPoint.node.ID);
            connection.inPoint.node.inPointList.Remove(connection.outPoint.node.ID);

            foreach (var v in nodeDatas)
            {
                if (v.id == connection.outPoint.node.ID)
                {
                    v.butDic.Remove(connection.inPoint.node.ID);
                }
            }

            connections.Remove(connection);
        }


        private void OnClickRemoveNode(NodeGraph node)//移除 整个节点
        {
            if (connections != null)
            {
                List<Connection> connectionsToRemove = new List<Connection>();

                for (int i = 0; i < connections.Count; i++)
                {
                    if (connections[i].inPoint == node.inPoint || connections[i].outPoint == node.outPoint)
                    {
                        connectionsToRemove.Add(connections[i]);
                    }
                }

                for (int i = 0; i < connectionsToRemove.Count; i++)
                {
                    OnClickRemoveConnection(connectionsToRemove[i]);
                }

                connectionsToRemove = null;
            }

            nodes.Remove(node);
        }


        private void ClearConnectionSelection()//清除 实时操作索引点
        {
            selectedInPoint = null;
            selectedOutPoint = null;
        }

        private void OnDrag(Vector2 delta)//用于实时拖动整个面板
        {
            drag = delta;

            if (nodes != null)
            {
                for (int i = 0; i < nodes.Count; i++)
                {
                    nodes[i].Drag(delta);
                }
            }
            GUI.changed = true;
        }

        private void DrawConnectionLine(Event e)//增强操作体验用  实时绘画鼠标所指位置的贝塞尔曲线
        {
            if (selectedInPoint != null && selectedOutPoint == null)
            {
                Handles.DrawBezier(
                    selectedInPoint.rect.center,
                    e.mousePosition,
                    selectedInPoint.rect.center + Vector2.left * 12.5f,
                    e.mousePosition - Vector2.left * 12.5f,
                    Color.white,
                    null,
                    0.5f
                );

                GUI.changed = true;
            }

            if (selectedOutPoint != null && selectedInPoint == null)
            {
                Handles.DrawBezier(
                    selectedOutPoint.rect.center,
                    e.mousePosition,
                    selectedOutPoint.rect.center - Vector2.left * 12.5f,
                    e.mousePosition + Vector2.left * 12.5f,
                    Color.white,
                    null,
                    0.5f
                );

                GUI.changed = true;
            }
        }

        //private Vector2 scale = new Vector2(4, 4);
        private void DrawGrid(float gridSpacing, float gridOpacity, Color gridColor)//绘制背景网格方法
        {
            int widthDivs = Mathf.CeilToInt(position.width / gridSpacing);
            int heightDivs = Mathf.CeilToInt(position.height / gridSpacing);

            Handles.BeginGUI();
            Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, gridOpacity);
            //GUIUtility.ScaleAroundPivot(scale,new Vector2(position.width/2,position.height/2));
            offset += drag * 0.5f;
            Vector3 newOffset = new Vector3(offset.x % gridSpacing, offset.y % gridSpacing, 0);

            for (int i = 0; i < widthDivs; i++)
            {
                Handles.DrawLine(new Vector3(gridSpacing * i, -gridSpacing, 0) + newOffset, new Vector3(gridSpacing * i, position.height, 0f) + newOffset);
            }

            for (int j = 0; j < heightDivs; j++)
            {
                Handles.DrawLine(new Vector3(-gridSpacing, gridSpacing * j, 0) + newOffset, new Vector3(position.width, gridSpacing * j, 0f) + newOffset);
            }

            Handles.color = Color.white;
            Handles.EndGUI();
        }

        public void DrawRect(Vector2 beginVec2, Vector2 endVec2)
        {
            Vector3 x1y1 = new Vector3(beginVec2.x, beginVec2.y, 0);
            Vector3 x2y2 = new Vector3(endVec2.x, endVec2.y, 0);
            Vector3 x1y2 = new Vector3(beginVec2.x, endVec2.y, 0);
            Vector3 x2y1 = new Vector3(endVec2.x, beginVec2.y, 0);

            Handles.BeginGUI();
            Handles.color = Color.white;//new Color(Color.black.r, Color.black.g, Color.black.b, 0.4f);
            Handles.DrawLine(x1y1, x2y1);
            Handles.DrawLine(x2y1, x2y2);
            Handles.DrawLine(x1y2, x2y2);
            Handles.DrawLine(x1y1, x1y2);
            Handles.EndGUI();

            // Debug.LogError($"--x1y1---{x1y1}--x2y2--{x2y2}--x1y2--{x1y2}--x2y1--{x2y1}-----------");

        }

        private void save()
        {
            NodeDataList nodeDataList = new NodeDataList(nodeDatas);
            string cnodeDataList = LitJson.JsonMapper.ToJson(nodeDataList);
            File.WriteAllText(Application.streamingAssetsPath + "\\..\\PlotEditor\\Plot\\" + SaveFileName +"_Client"+ ".txt", cnodeDataList);

            GraphNodeData nodeData = new GraphNodeData(nodes, connections);
            string NodeDataStr = JsonUtility.ToJson(nodeData);
            File.WriteAllText(Application.streamingAssetsPath + "\\..\\PlotEditor\\Plot\\" + SaveFileName +".txt", NodeDataStr);
        }

        private void Load()
        {
            if (nodes == null)
            {
                nodes = new List<NodeGraph>();
            }
            if (connections == null)
            {
                connections = new List<Connection>();
            }
            nodes.Clear();
            connections.Clear();
            string NodeDataStr = File.ReadAllText(Application.streamingAssetsPath + "\\..\\PlotEditor\\Plot\\" + SaveFileName + ".txt");
            GraphNodeData nodeData = JsonUtility.FromJson<GraphNodeData>(NodeDataStr);

            nodes = nodeData.nodes;
            ID = nodes[nodes.Count - 1].ID;
            for (int i = 0; i < nodeData.connections.Count; i++)
            {
                if (connections == null)
                {
                    connections = new List<Connection>();
                }

                connections.Add(new Connection(nodes.First(n => n.inPoint.id == nodeData.connections[i].inPoint.id).inPoint,
                    nodes.First(n => n.outPoint.id == nodeData.connections[i].outPoint.id).outPoint, OnClickRemoveConnection));
            }

            for (int i = 0; i < nodeData.nodes.Count; i++)
            {
                nodes[i].inPoint.node = nodes[i];
                nodes[i].inPoint.InitConnectionPoint(inPointStyle, OnClickInPoint);
                nodes[i].outPoint.node = nodes[i];
                nodes[i].outPoint.InitConnectionPoint(outPointStyle, OnClickOutPoint);
                nodes[i].InitNode(nodeStyle, selectedNodeStyle, OnClickRemoveNode);
            }

            string cNodeDataStr = File.ReadAllText(Application.streamingAssetsPath + "\\..\\PlotEditor\\Plot\\" + SaveFileName + "_Client" + ".txt");
            NodeDataList nodeDataList = LitJson.JsonMapper.ToObject<NodeDataList>(cNodeDataStr);
            nodeDatas = nodeDataList.nodeDatas;
        }


        private void DrawMenuBar()//  save  load  绘制方法 以及方法触发
        {
            menuBar = new Rect(0, 0, position.width, menuBarHeight);

            GUILayout.BeginArea(menuBar, EditorStyles.toolbar);
            GUILayout.BeginHorizontal();



            if (GUILayout.Button(new GUIContent("Save"), EditorStyles.toolbarButton, GUILayout.Width(35)))
            {
                //SaveStoryWindow.OpenSavePrefabWindow(save);
                bool isSave = EditorUtility.DisplayDialog("二次确认", "是否保存当前文本", "确定", "取消");
                if (isSave)
                {
                    save();
                }
                else
                {
                    Debug.Log("<color=red>取消保存</color>");
                }
            }

            GUILayout.Space(5);
            if (GUILayout.Button(new GUIContent("Load"), EditorStyles.toolbarButton, GUILayout.Width(35)))
            {
                //LoadStoryWindow.OpenLoadPrefabWindow(Load);
                bool isLoad = EditorUtility.DisplayDialog("二次确认", $"是否加载剧情 {SaveFileName} 的文本", "确定", "取消");
                if (isLoad)
                {
                    Load();
                }
                else
                {
                    Debug.Log("<color=red>取消加载</color>");
                }
            }

            GUI.skin.label.alignment = TextAnchor.UpperLeft;
            SaveFileName = EditorGUILayout.TextField("文件名", SaveFileName);
            GUILayout.EndArea();
        }

        void OnInspectorUpdate()
        {
            this.Repaint();
        }

    }   
}


NodeEditorWindow

节点数据编辑面板

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

namespace MDDGameFramework.MDDNodeEditor
{
    public class NodeEditorWindow : EditorWindow
    {
        public static NodeEditorWindow nodeEditorWindow;

        /////////////////////////

        public NodeData nodeData;

        /////////////////////////


        /////////////////////////
       

        /////////////////////////

        public static void OpenEditorWindow()
        {
            NodeEditorWindow window = (NodeEditorWindow)EditorWindow.GetWindow(typeof(NodeEditorWindow), false, "编辑窗口");

            window.Show();

            NodeEditorWindow.nodeEditorWindow = window;
        }


        public void RegisterNode(NodeData nodeData)
        {
            this.nodeData = nodeData;
        }

        private void RefreshNodeData()
        {
            
        }

        void OutPassageWay()//跳转通路 动态初始化 选页卡数据传输
        {
            if (nodeData != null)
            {
                for (int i = 0; i < nodeData.butDic.Count; i++)
                {
                    GUILayout.BeginHorizontal();
                    GUILayout.Space(10);
                    GUI.skin.label.alignment = TextAnchor.UpperLeft;
                    GUI.skin.label.fontSize = 13;
                    GUILayout.Label("跳转至" + nodeData.outPointList[i].ToString() + "节点");
                    nodeData.butDic[nodeData.outPointList[i]] = EditorGUILayout.TextArea(nodeData.butDic[nodeData.outPointList[i]], GUILayout.Width(270));
                    GUILayout.Space(90);
                    GUILayout.EndHorizontal();
                }
            }

        }

        GUILayoutOption[] 文本框GUI = { GUILayout.MaxWidth(160), GUILayout.MaxHeight(25) };

        public void OnGUI()
        {
            GUILayout.BeginVertical();

            if(nodeData != null)
            nodeData.ID = int.Parse(EditorGUILayout.TextArea(nodeData.ID.ToString(), 文本框GUI));

            OutPassageWay();

            GUILayout.EndVertical();
        }

        void OnInspectorUpdate()
        {
            //Debug.Log("窗口面板的更新");
            //这里开启窗口的重绘,不然窗口信息不会刷新
            this.Repaint();
        }
    }
}


GraphNodeData

节点图形数据存储包

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MDDGameFramework.MDDNodeEditor
{
    [Serializable]
    public class GraphNodeData//节点可视化数据存储模版
    {
        public List<NodeGraph> nodes = new List<NodeGraph>();
        public List<Connection> connections = new List<Connection>();

        public GraphNodeData() { }

        public GraphNodeData(List<NodeGraph> Enodes, List<Connection> Econnections)
        {
            for (int i = 0; i < Enodes.Count; i++)
            {
                nodes.Add(Enodes[i]);
            }

            if (Econnections != null)
            {
                for (int i = 0; i < Econnections.Count; i++)
                {
                    connections.Add(Econnections[i]);
                }
            }
        }
    }
}

小结-需要注意的地方

引用关系处理

处理图形界面的序列化与反序列时需要注意引用关系的重新建立

因为在序列化再反序列化的过程中引用关系会丢失

同时在写代码的过程中,一定要注意值类型,引用类型数据的引用关系处理

事实上编辑任何数据的时候,我们都应该关注深拷贝,浅拷贝,直接引用的使用区别。

如果没有理清,很可能会在编辑数据过程中产生逻辑上的混乱

数据分离

要关注数据与界面的数据分离,界面是界面的数据,节点数据是节点数据本身的数据

在游戏实际运行中,我们一般只需要我们编辑的节点数据

所以在存储数据时,我们有两份文件,一份是图形数据存档,一份是纯节点数据存档。

序列化方式

因为绘制界面我们用了大量的rect等unity原生类型,所以序列化界面的过程我直接选用了unity内置的json处理器

但是 unity内置的json处理器功能除了支持unity原生类的序列化在其他的方面的支持是在拉了,例如不支持序列化字典。

所以序列化节点数据的时候我接入了litjson,同时修改了一下litjson的源码让litjson反序列化字典是支持key 为int float等类型数据。

处理编辑后的数据还有多种方式以及改良空间。

节点编辑的意义

节点编辑是数据可视化编辑的一种方式,比如在编辑剧情时,就比用表格配置id控制跳转逻辑直观1w倍

它的本质是对各种数据编辑的一种方式,线性结构,树,图,流程逻辑,节点编辑都可以有良好的表现

现在已经有不少小伙伴以此为基准,做出了技能编辑器,剧情编辑器,行为树等插件。

展望

后续本人还是会以这个这个编辑器为基准,继续拓展,分享一下剧情文本如何适配动画,选项逻辑如何适配动画进度,如何让一个游戏的剧情流成型,多种剧情方式如何适配(剧情战,主线剧情)等。

本人也在慢慢向泛用技能编辑器进军,因为入行及接触了节点编辑的概念,实在是想为推动改变国内游戏万物皆配表的现状尽一点绵薄之力,希望未来更直观更舒适效率的数据配置方式能够推广开来,为游戏的游戏性变的更好起到一定的助力。

源文件

链接:https://pan.baidu.com/s/1c0dAusKLd1sGcV-xRXXb-A
提取码:1pem


记录历程,整理思路,共享知识,分享思维。