实现钢铁雄心那样的可点击地图(三)

发表于2017-12-07
评论7 2.7k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

一.再谈凹多边形三角化问题

上篇文章的多边形三角化已初显效果,但仍有部分凹点被错误三角化

SceneGizmosSlectionWire打开,显示网格

 

Debug后发现,现有代码仍无法完美处理此类多边形

 

对于点a,它是凸点,但若直接将“耳朵”割下,会有多边形外的部分被错误三角化,所以我们仍需要判断多边形与直线的关系,但由于精度问题,误差较大,所以改用插值+点与多边形的方式来进行判断

代码如下

private bool IsPolygonContainLine(List<PointF> nPolygon, Vector2 point1, Vector2 point2)
    {
        Vector2 vector;
        bool contain = true;
        float distance = Vector2.Distance(point1, point2);
        for (int i = 1; i < distance; i++)
        {
            vector = Vector2.Lerp(point1, point2, i / distance);
            if (!GeometryHelper.IsInPolygon(new PointF(vector.x, vector.y), nPolygon))
            {
                contain = false;
                break;
            }
        }
        return contain;
    }

点与凹多边形关系的判断,代码如下

public static bool IsInPolygon(PointF checkPoint, List<PointF> polygonPoints)
    {
        bool inside = false;
        int pointCount = polygonPoints.Count;
        PointF p1, p2;
        for (int i = 0, j = pointCount - 1; i < pointCount; j = i, i++)
        {
            p1 = polygonPoints[i];
            p2 = polygonPoints[j];
            if (checkPoint.Y < p2.Y)
            {//p2在射线之上 
                if (p1.Y <= checkPoint.Y)
                {//p1正好在射线中或者射线下方 
                    if ((checkPoint.Y - p1.Y) * (p2.X - p1.X) > (checkPoint.X - p1.X) * (p2.Y - p1.Y))//斜率判断,在P1和P2之间且在P1P2右侧 
                    {
                        //射线与多边形交点为奇数时则在多边形之内,若为偶数个交点时则在多边形之外。 
                        //由于inside初始值为false,即交点数为零。所以当有第一个交点时,则必为奇数,则在内部,此时为inside=(!inside) 
                        //所以当有第二个交点时,则必为偶数,则在外部,此时为inside=(!inside) 
                        inside = (!inside);
                    }
                }
            }
            else if (checkPoint.Y < p1.Y)
            {
                //p2正好在射线中或者在射线下方,p1在射线上 
                if ((checkPoint.Y - p1.Y) * (p2.X - p1.X) < (checkPoint.X - p1.X) * (p2.Y - p1.Y))//斜率判断,在P1和P2之间且在P1P2右侧 
                {
                    inside = (!inside);
                }
            }
        }
        return inside;
    }

当然,这么精妙的解法肯定不是我想的,方法来自计算机图形学几何工具算法详解一书,有兴趣的可以了解一下

然而还没有完,如果只是这样处理,最后会得到这样的mesh文件

没错,就是文章上面的第二张图。。。

 

二.解决点与多边形位置关系判断失败问题

Debug发现多边形只剩三个顶点,并且顶点均为凸点,但在判断点与多边形位置关系时仍有部分点不位于多边形内,所以这还是精度问题

先不考虑自定义数据结构用int存储小数,强行让其运行下去,发现在三角化吉林时出现了一个更奇怪的问题(并且此问题在后续三角化中多次出现)

多边形只有4个顶点,但凹点却有2个。世界上存在这么奇怪的多边形?答案当然是否定的。问题出在多边形的标准法向量,仔细研究代码,该多边形法向量其实是各个边界点法向量的平均值,并不完全标准。在3D数学基础:图形与游戏开发一书中也“明示”了这一问题

这引号颇有我当年的风范(:逃

所以不管是点与多边形的判定,还是多边形凹点的判定都并非100%成功,也难怪此项目推进如此困难。。。。。。

于是我决定采取一些其他“技术手段”来绕过这些问题,最终得到我们需要的mesh

三.序列化mesh文件

网上查了一圈,都没有找到直接生成unity可读取模型的方法,退而求其次,将这些mesh文件的顶点与三角面保存到本地,在每次项目start时通过代码生成

东西很简单,只提供代码,就详细不展开了

序列化mesh

public static string MeshToString(MeshFilter mf, float scale)
    {
        Mesh mesh = mf.mesh;
        Material[] sharedMaterials = mf.GetComponent<Renderer>().sharedMaterials;
        Vector2 textureOffset = mf.GetComponent<Renderer>().material.GetTextureOffset("_MainTex");
        Vector2 textureScale = mf.GetComponent<Renderer>().material.GetTextureScale("_MainTex");
        StringBuilder stringBuilder = new StringBuilder();
        Vector3[] vertices = mesh.vertices;
        for (int i = 0; i < vertices.Length; i++)
        {
            Vector3 vector = vertices[i];
            stringBuilder.Append(string.Format("v {0} {1} {2}\n", vector.x * scale, vector.z * scale, vector.y * scale));
        }
        for (int k = 0; k < mesh.subMeshCount; k++)
        {
            int[] triangles = mesh.GetTriangles(k);
            for (int l = 0; l < triangles.Length; l += 3)
            {
                stringBuilder.Append(string.Format("t {0} {1} {2}\n", triangles[l], triangles[l + 1], triangles[l + 2]));
            }
        }
        stringBuilder.Append("\n");
        return stringBuilder.ToString();
    }

反序列化mesh

public static void GetMeshByName(Mesh mesh, string meshName)
    {
        using (StreamReader streamReader = new StreamReader(_meshSrc + meshName))
        {
            string str = null;
            string []strs = null;
            List<Vector3> vertices = new List<Vector3>();
            List<int> triangles = new List<int>();
            str = streamReader.ReadLine();
            while (str != null)
            {
                strs = str.Split(' ');
                if (strs[0] == 'v'.ToString())
                {
                    vertices.Add(new Vector3(float.Parse(strs[1]), float.Parse(strs[2]), float.Parse(strs[3])));
                }
                if(strs[0] == 't'.ToString())
                {
                    triangles.Add(int.Parse(strs[1]));
                    triangles.Add(int.Parse(strs[2]));
                    triangles.Add(int.Parse(strs[3]));
                }
                str = streamReader.ReadLine();
            }
            mesh.Clear();
            mesh.vertices = vertices.ToArray();
            mesh.triangles = triangles.ToArray();
            mesh.RecalculateNormals();
        }
    }

四.制作可点击地图按钮

有了mesh文件,还需要一份文件存储这些mesh文件的名字,路径,以及相关介绍,这部分使用xml来实现。

载入所有的xml节点

public static List<ProvinceNode> GetAllProvinceNode()
    {
        if (provinceNodeList.Count == 0)
        {
            XmlDocument xmlDoc = ReadAndLoadXml();
            XmlNode provinces = xmlDoc.SelectSingleNode("provinces");
            foreach (XmlNode province in provinces.ChildNodes)
            {
                XmlElement _province = (XmlElement)province;
                ProvinceNode provinceNode = new ProvinceNode(_province.GetAttribute("name"), _province.GetAttribute("describe"), _province.GetAttribute("meshSrc"), int.Parse(_province.GetAttribute("offset")));
                provinceNodeList.Add(provinceNode);
            }
        }
        return provinceNodeList;
    }

Xml文件格式示意

Mesh载入场景后,只需在gameobject上添加MeshCollider组件便能检测到碰撞,再通过射线检测完成从点击到相应

射线检测

if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            if (Physics.Raycast(ray, out hitInfo))
            {
                GameObject gameObj = hitInfo.collider.gameObject;
                if (gameObj.tag == "map")//当射线碰撞目标为boot类型的物品 ,执行拾取操作
                {
                    Debug.Log(XmlLoader.GetDescribeByName(gameObj.name));
                }
            }
        }
    }

最终图像效果

点击后打印

 

项目到这里就基本结束了,当然,如果有更好的解决方案,或是给项目加其他有趣的功能,请务必联系我

最后附上完整的项目链接

https://github.com/shangguanhun/MapGenerate

 

PS:来公司实习一个多月,终于摸完鱼了,开心^_^

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台