【Unity3D】实现可视化链式结构数据(节点数据)

关键词:UnityEditor、可视化节点编辑、Unity编辑器自定义窗口工具

使用Newtonsoft.Json、UnityEditor相关接口实现

主要代码:

Handles.DrawBezier(起点,终点,起点切线向量,终点切线向量,颜色,null, 线粗度) 绘制贝塞尔曲线

Handles.DrawAAPolyLine(线粗度,顶点1, 顶点2, ...) 根据线段点绘制不规则线段

GUI.Window(id, rect, DrawNodeWindow, 窗口标题);

void DrawNodeWindow(int id) 传入的id即GUI.Window第一个参数,一般传节点唯一标识ID。

LinkObj类是节点类,里面有一个位置pos数据是存储该节点窗口位于编辑器的位置SerializableVector2类型是一个可被Json序列化的Vector2,不然无法被序列化。

cs 复制代码
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

public class LinkObj
{
    public int id;
    public List<int> preList;
    public List<int> nextList;
    public SerializableVector2 pos;//位于编辑窗口的位置
    public LinkObj(int _id, SerializableVector2 _pos)
    {
        id = _id;
        pos = _pos;
        preList = new List<int>();
        nextList = new List<int>();
    }
}

public static class PathConfig
{
    public static string SaveJsonPath = Application.dataPath + "/MyGraphicsEditorDemo/Editor/LinkList.json";
}

public class MyGraphicsEditorWindow : EditorWindow
{
    private Dictionary<int, LinkObj> linkObjDict = new Dictionary<int, LinkObj>();
    private int tempAddId;
    private Vector2 scrollViewPos;
    private Dictionary<int, Rect> linkObjRectDict = new Dictionary<int, Rect>();
    private LinkObj currentSelectLinkObj;
    private Color defaultColor;
    private Vector2 detailScrollViewPos;
    private bool isLoaded;

    [MenuItem("Tools/可视链结构编辑器")]
    private static void ShowWindow()
    {
        Debug.Log("打开可视链结构编辑器");
        var window = EditorWindow.GetWindow(typeof(MyGraphicsEditorWindow)) as MyGraphicsEditorWindow;
        window.minSize = new Vector2(1280, 500);
        window.Show(true);
        window.isLoaded = false;
    }

    MyGraphicsEditorWindow()
    {
        this.titleContent = new GUIContent("可视链结构编辑器");
    }

    private void OnGUI()
    {
        defaultColor = GUI.color;
        EditorGUILayout.BeginHorizontal(GUILayout.Width(position.width), GUILayout.Height(position.height));
        {
            //左面板(操作)
            EditorGUILayout.BeginVertical(GUILayout.Width(80));
            {
                EditorGUILayout.BeginHorizontal();
                {
                    if (GUILayout.Button("加载"))
                    {
                        //如果本地没有对应的json 文件,重新创建
                        if (File.Exists(PathConfig.SaveJsonPath))
                        {
                            string json = File.ReadAllText(PathConfig.SaveJsonPath);
                            linkObjDict = JsonConvert.DeserializeObject<Dictionary<int, LinkObj>>(json);
                            if (linkObjDict == null)
                            {
                                linkObjDict = new Dictionary<int, LinkObj>();
                            }
                            isLoaded = true;
                        }
                        else
                        {
                            isLoaded = false;
                            Debug.LogError("加载失败,尚未存在json文件:" + PathConfig.SaveJsonPath);
                        }
                    }
                    bool isExistFile = File.Exists(PathConfig.SaveJsonPath);
                    if ((isLoaded || !isExistFile) && GUILayout.Button("保存"))
                    {
                        //如果本地没有对应的json 文件,重新创建
                        if (!isExistFile)
                        {
                            File.Create(PathConfig.SaveJsonPath);
                        }
                        AssetDatabase.Refresh();
                        string json = JsonConvert.SerializeObject(linkObjDict);
                        File.WriteAllText(PathConfig.SaveJsonPath, json);
                        Debug.Log("保存成功:" + json);
                        AssetDatabase.SaveAssets();
                    }
                }
                EditorGUILayout.EndHorizontal();
                EditorGUILayout.BeginVertical();
                {
                    if (GUILayout.Button("添加节点"))
                    {
                        LinkObj obj = new LinkObj(tempAddId, new SerializableVector2(scrollViewPos));
                        if (!linkObjDict.ContainsKey(tempAddId))
                        {
                            linkObjDict.Add(tempAddId, obj);
                        }
                        else
                        {
                            Debug.LogError("节点ID已存在:" + tempAddId);
                        }
                    }
                    tempAddId = int.Parse(EditorGUILayout.TextField("节点ID", tempAddId.ToString()));
                }
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndVertical();

            //中间面板(可视节点)
            EditorGUILayout.BeginVertical(GUILayout.Width(position.width - 500));
            {
                EditorGUILayout.LabelField(string.Format("所有链节点"), EditorStyles.boldLabel);
                EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height));
                {
                    scrollViewPos = EditorGUILayout.BeginScrollView(scrollViewPos, GUILayout.Height(position.height));
                    {
                        BeginWindows();

                        if (linkObjDict != null && linkObjDict.Count > 0)
                        {
                            foreach (var item in linkObjDict)
                            {
                                int id = item.Key;
                                var linkObj = item.Value;
                                Rect oRect;
                                if (!linkObjRectDict.TryGetValue(id, out oRect))
                                {
                                    Rect windowRect = new Rect(180, 50, 180, 100);
                                    windowRect.x = linkObj.pos.x;
                                    windowRect.y = linkObj.pos.y;
                                    linkObjRectDict.Add(id, windowRect);
                                }
                                string str = string.Format("{0}-[节点]", id);


                                if (currentSelectLinkObj != null && currentSelectLinkObj.id == id)
                                    GUI.color = Color.yellow;
                                else if (currentSelectLinkObj != null && currentSelectLinkObj.preList.Exists((int x) => x == id))
                                    GUI.color = Color.blue;
                                else if (currentSelectLinkObj != null && currentSelectLinkObj.nextList.Exists((int x) => x == id))
                                    GUI.color = Color.green;

                                //绘画窗口
                                linkObjRectDict[id] = GUI.Window(id, linkObjRectDict[id], DrawNodeWindow, str);
                                GUI.color = defaultColor;

                                foreach (int nextId in linkObj.nextList)
                                {
                                    Rect nextRect;
                                    if (linkObjRectDict.TryGetValue(nextId, out nextRect))
                                    {
                                        DrawNodeCurve(linkObjRectDict[id], nextRect, Color.red);
                                    }
                                }
                            }
                        }
                        EndWindows();
                    }
                    EditorGUILayout.EndScrollView();
                }
                EditorGUILayout.EndHorizontal();
            }
            EditorGUILayout.EndVertical();

            //右面板(编辑选中节点)
            EditorGUILayout.BeginVertical(GUILayout.Width(250));
            {
                EditorGUILayout.LabelField("节点属性", EditorStyles.boldLabel);
                EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height));
                {
                    EditorGUILayout.BeginVertical();
                    {
                        detailScrollViewPos = EditorGUILayout.BeginScrollView(detailScrollViewPos, GUILayout.Height(position.height));
                        {
                            if (currentSelectLinkObj != null)
                            {
                                DrawCurrentLinkObj();
                            }
                        }
                        EditorGUILayout.EndScrollView();
                    }
                    EditorGUILayout.EndVertical();
                }
                EditorGUILayout.EndHorizontal();
            }
            EditorGUILayout.EndVertical();
        }
        EditorGUILayout.EndHorizontal();
    }

    //绘画窗口函数
    private void DrawNodeWindow(int id)
    {
        EditorGUILayout.LabelField(string.Format("节点ID:{0}", linkObjDict[id].id), EditorStyles.boldLabel);
        EditorGUILayout.BeginHorizontal();
        {
            //创建一个GUI Button
            if (GUILayout.Button("选择"))
            {
                currentSelectLinkObj = linkObjDict[id];
            }

            GUI.color = Color.red;
            if (GUILayout.Button("删除"))
            {
                if (EditorUtility.DisplayDialog("询问", "确认删除?", "确认", "取消"))
                {
                    linkObjDict.Remove(id);
                    linkObjRectDict.Remove(id);
                    return;
                }
            }
            GUI.color = defaultColor;
        }
        EditorGUILayout.EndHorizontal();

        //设置改窗口可以拖动
        GUI.DragWindow();

        var oItem = linkObjDict[id];
        Rect oRect;
        if (oItem != null && linkObjRectDict.TryGetValue(id, out oRect))
        {
            oItem.pos = new SerializableVector2(linkObjRectDict[id].position);
        }
    }

    //***描绘连线
    private void DrawNodeCurve(Rect start, Rect end, Color color, float fValue = 4)
    {
        //根据不同相对位置决定线条的起点和终点 (看似复杂,实际简单,可优化写法)
        float startX, startY, endX, endY;
        //start左 end右时, 起点是start右侧中点, 终点是end左侧中点
        if (start.x < end.x && Mathf.Abs(start.x + start.width / 2 - end.x - end.width / 2) > 50)
        { startX = start.x + start.width; endX = end.x; startY = start.y + start.height / 2; endY = end.y + end.height / 2; }
        //start右 end左时, 起点是start左侧中点, 终点是end右侧中点
        else if (start.x >= end.x && Mathf.Abs(start.x + start.width / 2 - end.x - end.width / 2) > 50)
        { startX = start.x; endX = end.x + end.width; startY = start.y + start.height / 2; endY = end.y + end.height / 2; }
        else
        {
            //start上 end下时, 起点是start下侧中点, 终点是end上侧中点
            if (start.y > end.y)
            { startX = start.x + start.width / 2; startY = start.y; endX = end.x + end.width / 2; endY = end.y + end.height; }
            //start下 end上时, 起点是start上侧中点, 终点是end下侧中点
            else
            { startX = start.x + start.width / 2; startY = start.y + start.height; endX = end.x + end.width / 2; endY = end.y; }
        }

        Vector3 startPos = new Vector3(startX, startY, 0);
        Vector3 endPos = new Vector3(endX, endY, 0);
        //根据起点和终点偏向给出不同朝向的Tan切线
        Vector3 startTan, endTan;
        if (start.x < end.x)
        {
            startTan = startPos + Vector3.right * 50;
            endTan = endPos + Vector3.left * 50;
        }
        else
        {
            startTan = startPos + Vector3.left * 50;
            endTan = endPos + Vector3.right * 50;
        }

        //绘制线条 color颜色 fValue控制粗细
        Handles.DrawBezier(startPos, endPos, startTan, endTan, color, null, fValue);

        //绘制线条终点的2条斜线 形成箭头
        Handles.color = color;
        Vector2 to = endPos;
        Vector2 v1, v2;
        //与上方大同小异,根据相对位置得出不同的箭头线段点
        if (start.x < end.x && Mathf.Abs(start.x + start.width / 2 - end.x - end.width / 2) > 50)
        {
            v1 = new Vector2(-8f, 8f);
            v2 = new Vector2(-8f, -8f);
        }
        else if (start.x >= end.x && Mathf.Abs(start.x + start.width / 2 - end.x - end.width / 2) > 50)
        {
            v1 = new Vector2(8f, 8f);
            v2 = new Vector2(8f, -8f);
        }
        else
        {
            if (start.y > end.y)
            {
                v1 = new Vector2(-8f, 8f);
                v2 = new Vector2(8f, 8f);
            }
            else
            {
                v1 = new Vector2(-8f, -8f);
                v2 = new Vector2(8f, -8f);
            }
        }
        //fValue粗细绘制由3个点构成的线段形成箭头
        Handles.DrawAAPolyLine(fValue, to + v1, to, to + v2);
    }

    // 当前选中节点详情编辑页面
    private void DrawCurrentLinkObj()
    {
        EditorGUILayout.LabelField(string.Format("节点ID:{0}", currentSelectLinkObj.id), EditorStyles.boldLabel);

        EditorGUILayout.Space(10);
        EditorGUILayout.LabelField("下一个节点");
        DrawListMember(currentSelectLinkObj.nextList);

        EditorGUILayout.Space(10);
        EditorGUILayout.LabelField("上一个节点");
        DrawListMember(currentSelectLinkObj.preList);
    }

    //列表显示
    private void DrawListMember(List<int> lst, bool isOnlyRead = false)
    {
        EditorGUILayout.BeginVertical();
        {
            if (lst.Count != 0)
            {
                for (int i = 0; i < lst.Count; i++)
                {
                    EditorGUILayout.BeginHorizontal();
                    {
                        GUILayout.Label((i + 1).ToString(), GUILayout.Width(25));
                        lst[i] = EditorGUILayout.IntField(lst[i]);
                        GUI.color = Color.red;
                        if (GUILayout.Button("-", GUILayout.Width(30)))
                        {
                            lst.RemoveAt(i);
                        }
                        GUI.color = defaultColor;
                    }
                    EditorGUILayout.EndHorizontal();
                }
            }
            if (GUILayout.Button("+"))
            {
                lst.Add(lst.Count);
            }
            EditorGUILayout.EndVertical();
        }
    }
}
cs 复制代码
using Newtonsoft.Json;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class SerializableVector2
{
    public float x;
    public float y;

    [JsonIgnore]
    public Vector2 UnityVector
    {
        get
        {
            return new Vector2(x, y);
        }
    }

    public SerializableVector2(Vector2 v)
    {
        x = v.x;
        y = v.y;
    }

    public static List<SerializableVector2> GetSerializableList(List<Vector2> vList)
    {
        List<SerializableVector2> list = new List<SerializableVector2>(vList.Count);
        for (int i = 0; i < vList.Count; i++)
        {
            list.Add(new SerializableVector2(vList[i]));
        }
        return list;
    }

    public static List<Vector2> GetSerializableList(List<SerializableVector2> vList)
    {
        List<Vector2> list = new List<Vector2>(vList.Count);
        for (int i = 0; i < vList.Count; i++)
        {
            list.Add(vList[i].UnityVector);
        }
        return list;
    }
}
相关推荐
王源骏8 小时前
unity接入coze智能体
unity·游戏引擎
每日出拳老爷子10 小时前
【Unity】【VR开发】实现VR屏幕共享应用的几个重要插件和参考资料分享
unity·游戏引擎·vr
向宇it12 小时前
【从零开始入门unity游戏开发之——C#篇10】循环结构——while、do-while、for、foreach的使用
java·开发语言·vscode·unity·c#·游戏引擎
浅陌sss12 小时前
Unity UI SafeArea适配
ui·unity·游戏引擎
Xiao_Chen____13 小时前
Unity 组件学习记录:Aspect Ratio Fitter
学习·unity·游戏引擎
冒泡P13 小时前
Unity中的委托和事件(UnityAction、UnityEvent)
unity·游戏引擎
林枫依依14 小时前
Unity超优质动态天气插件(含一年四季各种天气变化,可用于单机局域网VR)
unity·游戏引擎·vr·动态天气插件·一年四季
Unity官方开发者社区15 小时前
助力 Tuanjie OpenHarmony 开发:如何使用工具包 Hilog 和 SDK Kits Package?
unity
benben04417 小时前
Unity3D仿星露谷物语开发5之角色单例模式
unity·单例模式·游戏引擎