程序丨如何实现类似《钢铁雄心》的可点击地图(三)?
系列回顾:
一.再谈凹多边形三角化问题
上篇文章的多边形三角化已初显效果,但仍有部分凹点被错误三角化
将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
今日推荐
一键添加
加小编微信,享双重福利
1.加入GAD程序猿交流群,获取行业干货;
2.领取60G腾讯内部分享等独家程序资料。