查看原文
其他

程序丨如何实现类似《钢铁雄心》的可点击地图​(三)?

2017-12-14 酸辣酸辣 Gad-腾讯游戏开发者平台

系列回顾:

如何实现类似《钢铁雄心》的可点击地图(一)?

如何实现类似《钢铁雄心》的可点击地图(二)?


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


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


将Scene中Gizmos的SlectionWire打开,显示网格



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


今日推荐

游戏开发者年终考试,你能得多少分?

魏韬:谈谈MOBA类游戏的客户端开发

经济实用的实现方案:对法线贴图做Mipmap

一键添加

加小编微信,享双重福利

1.加入GAD程序猿交流群,获取行业干货;

2.领取60G腾讯内部分享等独家程序资料。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存