【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;
    }
}
相关推荐
ttod_qzstudio17 小时前
Unity中使用EzySlice实现模型切割与UV控制完全指南
unity
南無忘码至尊17 小时前
Unity 实现与 Ollama API 交互的实时流式响应处理
unity·游戏引擎·交互
平行云20 小时前
如何实现UE程序大并发多集群的像素流部署
unity·ue5·图形渲染
向宇it2 天前
【unity小技巧】在 Unity 中将 2D 精灵添加到 3D 游戏中,并实现阴影投射效果,实现类《八分旅人》《饥荒》等等的2.5D游戏效果
游戏·3d·unity·编辑器·游戏引擎·材质
向宇it2 天前
Unity Universal Render Pipeline/Lit光照材质介绍
游戏·unity·c#·游戏引擎·材质
__water2 天前
RHA《Unity兼容AndroidStudio打Apk包》
android·unity·jdk·游戏引擎·sdk·打包·androidstudio
两水先木示3 天前
【Unity3D】微信小游戏适配安全区域或胶囊控件(圆圈按钮)水平高度一致方案
unity·微信小游戏·安全区域·ui适配·胶囊控件·safearea
枯萎穿心攻击3 天前
ECS由浅入深第三节:进阶?System 的行为与复杂交互模式
开发语言·unity·c#·游戏引擎
不绝1913 天前
怪物机制分析(有限状态机、编辑器可视化、巡逻机制)
网络·游戏·unity·游戏引擎
unicrom_深圳市由你创科技3 天前
Unity开发如何解决iOS闪退问题
unity·ios·蓝桥杯