先看效果,下面两张图,第一张是unity烘焙好的网格,第二张是导出的文件,第二张因为数字长和宽不相等,所以形状被压缩了下。

Unity UI 元素导航_System

Unity UI 元素导航_navmesh_02

 

一、实现

1、这个方法做的功能就是将navmesh网格的顶点和顶点索引导出并封装成方便操作的数据。

private void LoadNavMeshToArray()
{
    NavMeshTriangulation tmpNavMeshTriangulation = NavMesh.CalculateTriangulation();
    
     //将每个三角形的顶点索引分组放到二维数组里        
    int[,] convexs = new int[tmpNavMeshTriangulation.areas.Length,3];
    for (int i = 0, j = 0; i < tmpNavMeshTriangulation.indices.Length; j++)
    {
        convexs[j, 0] = tmpNavMeshTriangulation.indices[i];
        convexs[j, 1] = tmpNavMeshTriangulation.indices[i + 1];
        convexs[j, 2] = tmpNavMeshTriangulation.indices[i + 2];
        i = i + 3;
    }
    navPath = new int[mapSize.x, mapSize.y];
    //将地图二维数组、三角形索引、顶点传入方法进行绘图
    ConvexsTraverse(navPath, convexs, tmpNavMeshTriangulation.vertices);//用索引值遍历三角形

    state = 1;

    Repaint();
    SceneView.RepaintAll();
}

2、根据索引和顶点遍历所有三角形

public void ConvexsTraverse(int[,] navPath, int[,] convexs, Vector3[] vertexs)//用索引值遍历三角形
{
    Vector3[] vertex3 = new Vector3[3];
    for (int i = 0; i < (convexs.Length / 3); i++)//每次处理一个三角形
    {
        vertex3[0] = vertexs[convexs[i, 0]];
        vertex3[1] = vertexs[convexs[i, 1]];
        vertex3[2] = vertexs[convexs[i, 2]];
        VertexsTraverse(navPath, vertex3);
        FillDifference(navPath, vertex3);
    }
}

3、代码量有点多就大概说一下用了什么算法,然后直接把完整代码全部贴出来吧。

       用DDA直线生成算法画边框,三角型用扫描线填充法,三角形分上下三角型进行填充,不规则三角形就分割成上下三角形。

最后一个多边形扫描填充算法没用上哈也贴出来供参考。

       另外,其实用DDA直线生成算法锯齿较大哈,可以选用Bresenham直线算法锯齿会小很多,然后填充的画其实也可以选用边界填充或泛滥填充更好。

using NUnit.Framework.Internal;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.UI;

namespace Assets.Editor.SceneEditor
{
    public class EditorNavMesh : EditorWindow
    {
        #region Singleton
        private static EditorNavMesh m_Instance = null;

        public static EditorNavMesh Instance
        {
            get { return m_Instance; }
        }
        #endregion
        private const string CLIENT_DATA_PATH = "";//PathDefine.EnvDataPath + "Data/config/csv";
        private const string NAVMESH_MESHS_PATH = "";//PathDefine.EnvConfigPath + "navmesh/meshs";

        GameObject parent;//测试使用
        private int[,] navPath;
        private string sSavePath;
        private string sNavFileName;
        public int state = 0;
        public Vector2Int mapSize = new Vector2Int(50,50);
        public int _id = 1;

        public static void Init()
        {
            if (m_Instance == null)
            {
                m_Instance = EditorWindow.GetWindow<EditorNavMesh>();
                m_Instance.autoRepaintOnSceneChange = true;
            }
        }

        public void Awake()
        {
            title = "EditorNavMeshByArray";
            SceneView.onSceneGUIDelegate += OnSceneView;

        }

        private void OnDestroy()
        {
            SceneView.onSceneGUIDelegate -= OnSceneView;
            DeleteTest();
        }
        public int clones = 1;
        private void OnGUI()
        {

            EditorGUILayout.BeginHorizontal();
            mapSize = EditorGUILayout.Vector2IntField("MapSize:", mapSize);
            EditorGUILayout.EndHorizontal();

            
            if(state >= 0)//加载了地图才显示这个按钮
            {
                EditorGUILayout.BeginVertical();
                if (GUILayout.Button("LoadNavMeshToArray"))
                {
                    LoadNavMeshToArray();
                }
                EditorGUILayout.EndVertical();
            }
            if (state > 0)//加载了nav才显示保存按钮
            {
                EditorGUILayout.BeginVertical();
                EditorGUILayout.BeginHorizontal();
                _id = EditorGUILayout.IntField(_id);
                if (GUILayout.Button("Save"))
                {
                    Save(_id);
                }
                if (GUILayout.Button("Test"))
                {
                    TestNavArray();
                }
                if (GUILayout.Button("DeleteTest"))
                {
                    DeleteTest();
                }
                EditorGUILayout.EndHorizontal();
                GUILayout.Label("SavePath:" + sSavePath);
                
                EditorGUILayout.EndVertical();
            }

            Repaint();
            SceneView.RepaintAll();
        }

        private void OnSceneView(SceneView view)
        {
            Repaint();
        }
        private void DeleteTest()
        {
            if (parent != null)
                GameObject.DestroyImmediate(parent);
            parent = null;
        }
        private void TestNavArray()//用方块来测试
        {
            DeleteTest();

            parent = GameObject.CreatePrimitive(PrimitiveType.Cube);
            parent.transform.position = new Vector3(0, 0, 0);
            parent.name = "parent";
            for (int i = 0; i < mapSize.x ; i++)
            {
                for(int j = 0; j < mapSize.y; j++)
                {
                    if(navPath[i,j] == 1)
                    {
                        GameObject obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                        obj1.transform.position = new Vector3(j, 1, i);
                        obj1.transform.localScale = new Vector3(0.9f, 0.9f, 0.9f);
                        obj1.name = i.ToString() + j.ToString();
                        obj1.transform.parent = parent.transform;
                    }
                }
            }
        }
        private void Save(int id)
        {
            string path = EditorUtility.SaveFilePanel("Save nav", Application.dataPath, sNavFileName, "xml"); 
            sSavePath = path;
            WriteMapDataByXml(id, navPath, mapSize.x, mapSize.y);
        }

        private void LoadNavMeshToArray()
        {
            NavMeshTriangulation tmpNavMeshTriangulation = NavMesh.CalculateTriangulation();
            
            int[,] convexs = new int[tmpNavMeshTriangulation.areas.Length,3];
            for (int i = 0, j = 0; i < tmpNavMeshTriangulation.indices.Length; j++)
            {
                convexs[j, 0] = tmpNavMeshTriangulation.indices[i];
                convexs[j, 1] = tmpNavMeshTriangulation.indices[i + 1];
                convexs[j, 2] = tmpNavMeshTriangulation.indices[i + 2];
                i = i + 3;
            }
            navPath = new int[mapSize.x, mapSize.y];
            ConvexsTraverse(navPath, convexs, tmpNavMeshTriangulation.vertices);//用索引值遍历三角形

            state = 1;

            Repaint();
            SceneView.RepaintAll();
        }

        [MenuItem("NavmeshEditor/NavmeshToXmlByArr")]
        public static void ExportNavMeshToXml()
        {
            Init();
        }

        public void ConvexsTraverse(int[,] navPath, int[,] convexs, Vector3[] vertexs)//用索引值遍历三角形
        {
            Vector3[] vertex3 = new Vector3[3];
            for (int i = 0; i < (convexs.Length / 3); i++)//每次处理一个三角形
            {
                vertex3[0] = vertexs[convexs[i, 0]];
                vertex3[1] = vertexs[convexs[i, 1]];
                vertex3[2] = vertexs[convexs[i, 2]];
                VertexsTraverse(navPath, vertex3);
                FillDifference(navPath, vertex3);
            }
        }

        public void VertexsTraverse(int[,] navPath, Vector3[] vertexs)//顶点遍历
        {
            Vector3 v1, v2;
            int nSize = vertexs.Length;
            for (int i = 0; i < nSize; i++)
            {
                v1 = vertexs[i % nSize];
                v2 = vertexs[(i + 1) % nSize];
                ConstructLine(navPath, v1, v2);
            }
        }

        //将转换的地图导出xml
        public void WriteMapDataByXml(int id, int[,] navPath, int row, int col)
        {
            StreamWriter writer;
            StringBuilder message = new StringBuilder();
            FileInfo file = new FileInfo(sSavePath);
            writer = file.CreateText();

            String data = @"<root type='MapMask'>";
            message.Append(data);
            message.Append("\n");
            message.Append(string.Format("<id>{0}</id>\n", id));
            message.Append(string.Format("<width>{0}</width>\n", col));
            message.Append(string.Format("<height>{0}</height>\n", row));
            message.Append("<mask>\n");
            for (int i = col - 1; i >= 0; i--)
            {
                for (int j = 0; j < row; j++)
                {
                    message.Append(navPath[i, j]);
                }
                message.Append("\n");
            }
            message.Append("</mask>\n");
            message.Append("</root>");
            writer.Write(message);
            writer.Flush();
            writer.Dispose();
            writer.Close();
        }

        //根据顶点绘制边框算法,两点差值绘制法
        public void ConstructLine(int[,] navPath, Vector3 v1, Vector3 v2)
        {
            float nxd, nzd;//x和z轴相互间距离
            float mod, normalX, normalZ;
            float nx, nz;//用于存差值计算的点
            float nSafaX = 0, nSafaZ = 0;//维护xz,用于边界过线检测
            nxd = Mathf.Abs(v1.x - v2.x);
            nzd = Mathf.Abs(v1.z - v2.z);
            mod = Mathf.Sqrt(nxd * nxd + nzd * nzd);
            normalX = nxd / mod;
            normalZ = nzd / mod;

            nx = v1.x;
            nz = v1.z;
            navPath[(int)nz, (int)nx] = 1;
            if (v1.z == v2.z)//一横
            {
                if (v1.x < v2.x)
                {
                    while (nx <= v2.x)//末尾压线
                    {
                        navPath[(int)nz, (int)nx] = 1;
                        nx = nx + normalX;
                    }
                    nSafaX = (v2.x - (int)v2.x) > 0 ? v2.x + 1 : v2.x;//过线维护
                    nSafaZ = nz;
                }
                else
                {
                    while (nx >= v2.x)
                    {
                        navPath[(int)nz, (int)nx] = 1;
                        nx = nx - normalX;
                    }
                    nSafaX = v2.x;//过线维护
                    nSafaZ = nz;
                }
            }
            else if (v1.x == v2.x)//一竖
            {
                if (v1.z < v2.z)
                {
                    while (nz <= v2.z)
                    {
                        navPath[(int)nz, (int)nx] = 1;
                        nz = nz + normalZ;
                    }
                    nSafaX = nx;//过线维护
                    nSafaZ = (v2.z - (int)v2.z) > 0 ? v2.z + 1 : v2.z;
                }
                else
                {
                    while (nz >= v2.z)
                    {
                        navPath[(int)nz, (int)nx] = 1;
                        nz = nz - normalZ;
                    }
                    nSafaX = nx;//过线维护
                    nSafaZ = v2.z;
                }
            }
            else if (v1.x < v2.x && v1.z > v2.z)//右上撇,1x小于2x,1z大于2z情况
            {
                while (nx <= v2.x)
                {
                    navPath[(int)nz, (int)nx] = 1;
                    nx = nx + normalX;
                    nz = nz - normalZ;
                }
                nSafaX = (v2.x - (int)v2.x) > 0 ? v2.x + 1 : v2.x;//过线维护
                nSafaZ = v2.z;
            }
            else if (v1.x < v2.x && v1.z < v2.z)//右下奈,1xz小于2xz情况
            {
                while (nx <= v2.x)
                {
                    navPath[(int)nz, (int)nx] = 1;
                    nx = nx + normalX;
                    nz = nz + normalZ;
                }
                nSafaX = (v2.x - (int)v2.x) > 0 ? v2.x + 1 : v2.x;//过线维护
                nSafaZ = (v2.z - (int)v2.z) > 0 ? v2.z + 1 : v2.z;
            }
            else if (v1.x > v2.x && v1.z < v2.z)//左下撇,1x大于2x,1z小于2z情况
            {
                while (nx >= v2.x)
                {
                    navPath[(int)nz, (int)nx] = 1;
                    nx = nx - normalX;
                    nz = nz + normalZ;
                }
                nSafaX = v2.x;//过线维护
                nSafaZ = (v2.z - (int)v2.z) > 0 ? v2.z + 1 : v2.z;
            }
            else if (v1.x > v2.x && v1.z > v2.z)//左上奈,1xz大于于2xz情况
            {
                while (nx >= v2.x)
                {
                    navPath[(int)nz, (int)nx] = 1;
                    nx = nx - normalX;
                    nz = nz - normalZ;
                }
                nSafaX = v2.x;//过线维护
                nSafaZ = v2.z;
            }
            //navPath[(int)nSafaZ, (int)nSafaX] = 1;
        }

        //差值填充三角形算法
        public void FillDifference(int[,] navPath, Vector3[] vertexs)
        {
            int i;
            int len = vertexs.Length;
            Vector3 v1, v2, v3;
            for (i = 0; i < len; i++)//处理简单的三角形
            {
                v1 = vertexs[i % len];
                v2 = vertexs[(i + 1) % len];
                if (v1.z == v2.z)
                {
                    if (v1.x <= v2.x)
                        FillSimpleTriangle(navPath, v1, v2, vertexs[(i + 2) % len]);
                    else
                        FillSimpleTriangle(navPath, v2, v1, vertexs[(i + 2) % len]);
                    return;
                }
            }
            //不是简单三角形就分上下两部分进行处理
            for (i = 0; i < len; i++)//顺时针旋转判断
            {
                v1 = vertexs[i % len];
                v2 = vertexs[(i + 1) % len];
                v3 = vertexs[(i + 2) % len];
                if (v2.z < v1.z && v1.z < v3.z)
                {
                    FillComplexTriangle(navPath, v1, v2, v3);
                    return;
                }
            }
            for (i = 0; i < len; i++)//逆时针旋转判断
            {
                v1 = vertexs[i % len];
                v2 = vertexs[(i + 2) % len];
                v3 = vertexs[(i + 1) % len];
                if (v2.z < v1.z && v1.z < v3.z)
                {
                    FillComplexTriangle(navPath, v1, v2, v3);
                    return;
                }
            }
        }

        //填充简单三角形,左右顶点,上边或下边顶点
        public void FillSimpleTriangle(int[,] navPath, Vector3 vl, Vector3 vr, Vector3 vc)
        {
            float nLxd, nRxd;//两个与不平行的点x距离
            float nzd;//高度的平方
            float lMod, rMod, lNormalX, rNormalX, normalZ;
            float nLx, nRx;//用于存差值计算的点
            nLxd = vc.x - vl.x;
            nRxd = vc.x - vr.x;
            nzd = Mathf.Abs(vc.z - vl.z);

            lMod = Mathf.Sqrt(nLxd * nLxd + nzd * nzd);
            rMod = Mathf.Sqrt(nRxd * nRxd + nzd * nzd);
            //lNormalX = ((nLxd / lMod) * (lMod / nzd));
            //rNormalX = ((nRxd / rMod) * (rMod / nzd));
            lNormalX = nLxd / nzd;
            rNormalX = nRxd / nzd;
            //normalZ = 1;
            int rowb, rowe;
            int ndir = 0;//方向,1表示下,-1表示上
            if (vl.z > vc.z)//上三角情况
            {
                rowb = (int)vl.z;
                rowe = (int)vc.z;
                ndir = -1;
            }
            else//下三角情况
            {
                rowb = (int)vl.z;
                rowe = (int)vc.z;
                ndir = 1;
            }
            nLx = vl.x;
            nRx = vr.x;
            for (rowb = rowb; rowb != rowe; rowb += ndir)
            {
                for (int col = (int)nLx; col <= nRx; col++)
                {
                    //rowb = rowb > 0 ? rowb : 0;
                    //col = col > 0 ? col : 0;
                    navPath[rowb, col] = 1;
                }
                if ((nRx - (int)nRx) > 0)//过线维护
                {
                    navPath[rowb, (int)nRx + 1] = 1;
                }
                nLx += lNormalX;
                nRx += rNormalX;
            }
        }

        //填充复杂三角形
        public void FillComplexTriangle(int[,] navPath, Vector3 vl, Vector3 vt, Vector3 vb)
        {
            float nRxd, nRzd;
            float rMod, rNormalX;
            float nzd;//高度的平方
            nRxd = vt.x - vb.x;
            nRzd = vb.z - vt.z;
            nzd = Mathf.Abs(vt.z - vb.z);
            rMod = Mathf.Sqrt(nRxd * nRxd + nRzd * nRzd);
            //rNormalX = ((nRxd / rMod) * (rMod / nzd));
            rNormalX = nRxd / nzd;

            float rowb, rowe;
            float nBx = vb.x;//用于存差值计算的点
            rowb = vb.z;
            rowe = vl.z;
            for (rowb = rowb; rowb >= rowe; rowb--)//找到分割点
            {
                nBx += rNormalX;
            }
            Vector3 vSplitPoint = new Vector3() { x = nBx, y = 0, z = rowe };//分割点
            Vector3 vParallelPoint = vl;//与分割点平行的点
            Vector3 vTopPoint = vt;//最上面的点
            Vector3 vBottomPoint = vb;//最下面的点

            if (vParallelPoint.x <= vSplitPoint.x)
            {
                FillSimpleTriangle(navPath, vParallelPoint, vSplitPoint, vTopPoint);//填充上半部分
                FillSimpleTriangle(navPath, vParallelPoint, vSplitPoint, vBottomPoint);//填充下半部分
            }
            else
            {
                FillSimpleTriangle(navPath, vSplitPoint, vParallelPoint, vTopPoint);
                FillSimpleTriangle(navPath, vSplitPoint, vParallelPoint, vBottomPoint);
            }


        }

        //根据多边形填充算法,扫描线法(只对一个多边形正常)
        public void FillPolygon(int[,] navPath, int row, int col)
        {
            int state = 0;//0表示离开,1填充,2进入
            for (int i = 0; i < row; i++)
            {
                state = 0;
                for (int j = 0; j < col; j++)//检测是否只有入口没有出口,是本行不填充
                {
                    var cell = navPath[i, j];
                    if (cell == 1 && state == 0)
                    {
                        state = 2;
                    }
                    else if (cell == 0 && state == 2)
                    {
                        state = 1;
                    }
                    else if (cell == 1 && state == 1)
                    {
                        state = 0;
                        break;
                    }
                }
                if (state != 0)//如果不等于0就等于没有出口所以本行不用填充
                    continue;

                for (int j = 0; j < col - 1; j++)//开始填充本行
                {
                    if (navPath[i, j] == 1 && state == 0)
                    {
                        state = 2;
                    }
                    else if (navPath[i, j] == 0 && state == 2)
                    {
                        state = 1;
                    }
                    else if (navPath[i, j] == 1 && state == 1)
                    {
                        if (navPath[i, (j + 1)] == 0)
                            state = 0;
                    }
                    if (state == 1)
                    {
                        navPath[i, j] = 1;
                    }
                }
            }
        }
    }
}

二、使用

1、赋值上面代码创建脚本粘贴进去

2、MapSize是地图大小,大小必须超过网格的最大值

Unity UI 元素导航_算法_03

2、Test按钮可以帮助你测试看看生成的数据怎样的,如下图

Unity UI 元素导航_Unity UI 元素导航_04