【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;
    }
}
相关推荐
Allen747412 小时前
ComfyUI 自动化生产 3D资产 工作流笔记
图像处理·opencv·unity·自然语言处理·3d模型生成·confyui
nnsix15 小时前
Unity Windows11 打字中文显示不出来输入法的候选框
unity
adogai1 天前
unity mcp接入 实现一句话生成游戏!
游戏·unity·游戏引擎
mxwin1 天前
Unity Shader 逐像素光照 vs 逐顶点光照性能与画质的权衡策略
unity·游戏引擎·shader·着色器
CDN3601 天前
游戏盾导致 Unity/UE 引擎崩溃的主要原因排查?
游戏·unity·游戏引擎
mxwin1 天前
Unity URP 全局光照 (GI) 完全指南 Lightmap 采样与实时 GI(光照探针、反射探针)的 Shader 集成
unity·游戏引擎·shader·着色器
mxwin1 天前
Unity URP 溶解效果基于噪声纹理与 clip 函数实现物体渐隐渐显
unity·游戏引擎·shader
CheerWWW1 天前
GameFramework——Download篇
笔记·学习·unity·c#
mxwin1 天前
Unity URP 下的 Early-Z / Depth Prepass 解决复杂片元着色器造成的 Overdraw 问题
unity·游戏引擎·着色器
mxwin1 天前
Unity Shader 顶点色:利用模型顶点颜色传递渲染数据
unity·游戏引擎·shader