1-5 绘制正六边形地图单元

  在上一章中,我们已经排列好了每个地图单元的位置,但是每个地图单元的外观还是正方形的;这一章中,会将正方形的外观更换成正六边形的外观。

  要将正方形地图元素外观替换为正六边形地图元素外观,这里首先删除Hex Cell预置上,除了Hex Cell脚本以外的所有组件,如下图:

  接下来,创建HexMesh脚本,内容如下:

HexMesh.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using UnityEngine;
using System.Collections.Generic;

//依赖MeshFilter和MeshRenderer组件
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]

public class HexMesh : MonoBehaviour
{
//存储计算生成后的mesh
private Mesh hexMesh;

//存储所有正六边形的顶点位置信息
private List<Vector3> vertices;

//索引,每个三角面片顶点的渲染顺序
private List<int> triangles;

void Awake()
{
//初始化MeshFilter组件的,实例化hexMesh,并给其命名
GetComponent<MeshFilter>().mesh = hexMesh = new Mesh();
hexMesh.name = "Hex Mesh";

//初始化vertices和triangles组件
vertices = new List<Vector3>();
triangles = new List<int>();
}
}

  脚本创建完成后,在Hex Grid物体下创建一个子物体,命名为Hex Mesh,并挂载HexMesh脚本。因为HexMesh脚本中有[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]语句,所以在Hex Mesh物体上会自动创建MeshFilter组件和MeshRenderer组件,但是MeshRenderer组件中没有默认的材质球,这里为其添加Unity自带的默认材质球,完成效果如下图:

  接下来,回到HexGrid脚本中,在其Awake方法中,与取得Canvas组件相类似,可以取得HexMesh组件。代码如下:

HexGrid.cs
1
2
3
4
5
6
7
8
9
10
//存储Hex Mesh物体上的hexMesh脚本组件
private HexMesh hexMesh;

private void Awake()
{
//获取Hex Mesh物体上的hexMesh脚本组件实例
hexMesh = GetComponentInChildren<HexMesh>();


}

  获取到HexMesh组件实例后,就可以调用其中的方法来构建正六边形的三角面片了,但是这里要注意,生成三角面片的方法调用,必须在HexMesh脚本初始化完成之后,所以这里在HexGrid的Start方法中调用构建三角面的方法,代码如下:

exGrid.cs
1
2
3
4
5
private void Start()
{
//调用绘制mesh的方法
hexMesh.Triangulate(cells);
}

  其实HexMesh.Triangulate方法在程序的任何阶段都可以被调用。在之后的一些步骤中,运行时对地图作出调整,我们还会调用这个方法,所以,在这个方法中,首先要清空旧的mesh、vertices、triangles这些变量的内容;接着读取存储所有HexCell实例的数组,依次录入其顶点Vector3信息和顶点顺序索引;然后将所有HexCell的这些信息全都保存在hexMesh的vertices和triangles数组中;最后,调用RecalculateNormals方法重新计算法线,使最后生成的三角面的视觉效果正确。HexMesh脚本修改代码如下:

HexMesh.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// <summary>
/// 根据数组长度创建cell的Mesh
/// </summary>
/// <param name="cells">存储所有Hex Cell实例的数组</param>
public void Triangulate(HexCell[] cells)
{
//清空原有的数据
hexMesh.Clear();
vertices.Clear();
triangles.Clear();

//依次读取数组中的Hex Cell实例,录入每个Hex Cell的顶点信息
for (int i = 0; i < cells.Length; i++)
{
Triangulate(cells[i]);
}

//将所有的顶点位置信息,顶点位置信息的索引存储到链表中
hexMesh.vertices = vertices.ToArray();
hexMesh.triangles = triangles.ToArray();

//重新计算法线方向,使得三角面片可以正确的显示出来
hexMesh.RecalculateNormals();
}

/// <summary>
/// 通过单个Hex Cell实例,计算其6个顶点位置,并创建三角形面片
/// </summary>
/// <param name="cell">单个Hex Cell的实例</param>
private void Triangulate(HexCell cell)
{
}

  由于正六边形是由多个三角面片构成的,所以需要创建AddTriangle方法,这个方法入参为3个顶点的Vector3信息。将3个入参信息添加到vertices链表中,并且与其对应的索引值添加到triangles链表中,以备Triangulate方法利用两个链表统一生成三角面片,修改代码如下:

HexMesh.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// 添加单个三角面片的顶点位置信息和索引
/// </summary>
/// <param name="v1">顺时针 第一个顶点的Vector3</param>
/// <param name="v2">顺时针 第二个顶点的Vector3</param>
/// <param name="v3">顺时针 第三个顶点的Vector3</param>
private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3)
{
//获取当前vertices链表中已经录入的数量
int vertexIndex = vertices.Count;

//在vertices链表中添加新增的顶点位置信息
vertices.Add(v1);
vertices.Add(v2);
vertices.Add(v3);

//在triangles链表中添加新增顶点的索引
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
}

  现在,生成每个地图单元中三角面片的方法基本完成了,在正式生成之前,我们需要先完善Triangulate(HexCell cell)方法,首先测试生成每个正六边形地图元素的第一个三角面片,即从顶部第一个顶点开始计算的两个顶点和中点共同构成的三角面。代码如下:

HexMesh.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 通过单个Hex Cell实例,计算其6个顶点位置,并创建三角形面片
/// </summary>
/// <param name="cell">单个Hex Cell的实例</param>
private void Triangulate(HexCell cell)
{
//获取单个cell的中点位置
Vector3 center = cell.transform.localPosition;

//根据中点位置计算出其余两个顶点的信息,将其传入添加单个三角面片信息的方法中
AddTriangle(
center,
center + HexMetrics.corners[0],
center + HexMetrics.corners[1]
);
}

  这样,就为每个正六边形地图元素,生成了第一个三角面片。效果如下图:

  通过观察上图可以得出,只要修改private void Triangulate(HexCell cell)方法内调用AddTriangle方法的部分,循环6次,即可生成全部的三角面片,修改代码如下:

HexMesh.cs
1
2
3
4
5
6
7
8
9
//根据中点位置计算出其余的顶点位置信息
for (int i = 0; i < 6; i++)
{
AddTriangle(
center,
center + HexMetrics.corners[i],
center + HexMetrics.corners[i + 1]
);
}

  在完成以上代码后,如果直接运行,Unity会弹出一个索引越界的错误,导致这个错误的原因是,当for循环中的i为6时,center + HexMetrics.corners[i + 1]中括号里的值为7,而HexMetrics.corners数组中只存储了6个顶点信息HexMetrics.corners[7]其实就是第一个顶点,即正六边形最上方的顶点,所以这里需要在corners数组的末尾添加一条数据,使HexMetrics.corners[7]指向第一个顶点的位置。代码如下:

HexMetrics.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//正六边形的六个顶点位置,其姿态为角朝上,从最上面一个顶点开始计算位置
//根据正六边形中点的位置,顺时针依次定义6个顶点的位置
public static Vector3[] corners =
{
new Vector3(0f, 0f, outerRadius),
new Vector3(innerRadius, 0f, 0.5f * outerRadius),
new Vector3(innerRadius, 0f, -0.5f * outerRadius),
new Vector3(0f, 0f, -outerRadius),
new Vector3(-innerRadius, 0f, -0.5f * outerRadius),
new Vector3(-innerRadius, 0f, 0.5f * outerRadius),
//正六边形其实只有6个顶点,但是当构建三角面片的时候,最后一个三角面片的顶点其实为:最后一个、第一个、中点,即corners[7]
//为了减少在循环中的判断,这里添加一条数据,防止索引越界即可
new Vector3(0f, 0f, outerRadius)
};

  在corners数组中添加完数据后,运行效果如下图:

  最后还有一点需要讨论,为什么我们不合并重叠的顶点?

  其实完全可以合并,并且还能将“面数”进行优化,比如只使用4个三角形面片就可以拼接成一个正六边形,而不是6个,但是在之后的步骤中,还会对正六边形地图元素作出一些其他的改动,如果现在优化顶点和面数,可能会导致之后的步骤变得更加复杂和难以处理。

  这一章我们已经生成了正确的正六边形地图元素,下一章将会重新排列这些地图元素的坐标,为之后的计算便捷做准备。