1-6 修改正六边形地图单元的坐标

  地图系统是一个回合制策略游戏的基础,一个灵活、稳固、拓展性强的地图系统会给游戏带来更多的可能。

  上一章中,我们正确的生成了所有的正六边形地图单元,现在让我们来重新观察一下这些地图单元的坐标,如下图:

  通过观察上图我们可以发现,在水平的Z轴方向上,每个地图单元的排列都很正常,但是在垂直的X轴方向上,地图单元排列成了锯齿状。导致这个问题的原因是之前我们取消偶数行偏移造成的。相比于正方形的地图元素排列,正六边形的地图元素排列在处理坐标时并没有那么容易。为了方便之后的一些操作,首先创建一个HexCoordinates结构体,我们可以用它来转换现有的坐标系。转换后的X与Z坐标,只公开get属性,确保其不被修改。使用System.Serializable标记这个结构体,使其可以序列化,以便Unity在runtime模式下也可以识别它。

HexCoordinates.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;

[System.Serializable]
public struct HexCoordinates
{
//存储重新计算后的X坐标值
public int X { get; private set; }

//存储重新计算后的Z坐标值
public int Z { get; private set; }

/// <summary>
/// 重载默认的构造函数
/// </summary>
/// <param name="x">为转换后的X坐标赋初始值</param>
/// <param name="z">为转换后的Z坐标赋初始值</param>
public HexCoordinates(int x, int z)
{
X = x;
Z = z;
}
}

  接下来,创建一个计算坐标偏移的静态方法,我们稍后会完成这个方法,现在只返回传入的参数值即可。代码如下:

HexCoordinates.cs
1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 进行X与Z的坐标转换,将X方向锯齿状的排列,改为斜向的排列
/// </summary>
/// <param name="x">原始cell的x轴坐标</param>
/// <param name="z">原始cell的z轴坐标</param>
/// <returns>目前返回传入的参数值</returns>
public static HexCoordinates FromOffsetCoordinates(int x, int z)
{
return new HexCoordinates(x, z);
}

  然后,为了方便之后的观察和调试方便,我们需要重载ToString()方法。如果使用原始的ToString()方法,只会返回struct的名称,这里我们需要返回X和Z的坐标值,修改代码如下:

HexCoordinates.cs
1
2
3
4
5
6
7
8
/// <summary>
/// 重载默认的ToString方法,使其返回的是X和Z的坐标值
/// </summary>
/// <returns>X和Z的坐标值</returns>
public override string ToString()
{
return "(" + X.ToString() + ", " + Z.ToString() + ")";
}

  最后,我们还需要声明ToStringOnSeparateLines()方法,用来将X和Z的值输出到之前的UI元素上,这个方法与重载的ToString()方法很类似,只是添加了\n进行换行。代码如下:

HexCoordinates.cs
1
2
3
4
5
6
7
8
/// <summary>
/// 将转换后的X和Z的坐标值添加换行符,以便显示在UGUI的每个cell上
/// </summary>
/// <returns>添加换行符后的X和Z,符合Text组件的富文本格式</returns>
public string ToStringOnSeparateLines()
{
return X.ToString() + "\n" + Z.ToString();
}

  完成了这些步骤后,让我们回到FromOffsetCoordinates(int x, int z)方法中。

  这里有个很重要的一点需要注意,在之前的所有步骤中,每个地图单元和其坐标显示,是完全一一对应的,我们在修改地图单元排列方式的同时,其坐标值也会跟着改变。但是在接下来的步骤中,我们会脱离开这种相互影响的关系,只专注于修改坐标。即通过FromOffsetCoordinates(int x, int z)方法将地图单元网格和坐标值两者排列分开。也可以理解为在保持所有地图单元网格排列为矩形不变的情况下,只重新排列每个网格对应的坐标值,这样我们就将坐标与网格分开看待了。在之后的一些步骤中,做到只修改网格或坐标其中之一,而不影响另一个的排列方式的效果。

  所以,这里我们就要取消坐标值的偶数行的偏移,让X轴的坐标依然是斜向排列的。代码如下:

HexCoordinates.cs
1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// 进行X与Z的坐标转换,将X方向锯齿状的排列,改为斜向的排列
/// 这个方法将mesh和坐标值分开处理了
/// 这里的入参只是处理X和Z的坐标,与mesh的排列和位置无关
/// </summary>
/// <param name="x">原始cell的x轴坐标</param>
/// <param name="z">原始cell的z轴坐标</param>
/// <returns>目前返回传入的参数值</returns>
public static HexCoordinates FromOffsetCoordinates(int x, int z)
{
return new HexCoordinates(x - z / 2, z);
}

  完成这个方法后,我们回到HexGrid.CreateCell(int x, int z, int i)方法中。这个方法是负责排列每个地图元素、计算每个地图元素的坐标值、将坐标值传递到Text组件上并显示出来。所以要在此方法中调用struct HexCoordinates修改坐标的FromOffsetCoordinates方法,和为Text输出富文本格式坐标值的ToStringOnSeparateLines方法。代码如下:

HexGrid.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void CreateCell(int x, int z, int i)
{


//设置被实例化地图单元的父级和位置
cell.transform.SetParent(transform, false);
cell.transform.localPosition = position;

//在不改变cell排列的情况下,重新计算每个cell的坐标位置
cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z);



//设置label的文字,就是cell在数组中的位置
//label.text = x.ToString() + "\n" + z.ToString();

//将转换后的坐标值复制给UGUI的Text组件,将它显示出来
label.text = cell.coordinates.ToStringOnSeparateLines();
}

  最后,在HexCell组件中创建一个HexCoordinates的实例,这样,在HexGrid实例化地图元素的时候,会自动调用每个地图元素HexCell组件上的HexCoordinates实例进行坐标的转换。代码如下:

HexCell .cs
1
2
3
4
5
6
public class HexCell : MonoBehaviour
{
//在实例化每个cell的时候会调用该实例
//针对每个cell,重新计算它的坐标值
public HexCoordinates coordinates;
}

  经过以上步骤的操作,我们的坐标转换就完成了。最终在不改变地图单元mesh排列的情况下,达到了下图的坐标排列效果:

  将修改坐标前后的效果对比一下,更加方便理解和找出其中的规律:

  经过观察转换后的效果,我们发现这样一个问题:如果一个人物站在某个正六边形地图单元上,那他可以朝6个方向移动,分别为右上、右、右下、左下、左、左上。目前我们在这里只有X和Z维度,Z可以描述左右移动和偏移量,X可以描述右上和左下移动和偏移量。这里缺少了描述左上和右下移动和偏移量的坐标。所以,我们需要在这个平面中再添加一个维度,用来描述这两个对称方向的移动和偏移量。如下图所示:

  通过观察上图可以发现,其实只需要将X轴镜像翻转一下,便可以得到Y轴。并且在坐标系的任意一个位置,X+Y+Z的值适中为0。也就是说,一个轴上的坐标值增大,另一个轴上就会减少,这样就产生了6个移动方向。这些坐标通常称为立方体坐标,因为它是三维的,其拓扑结构类似于立方体。

  通过以上观察和总结,添加Y维度的坐标就变得很容易了。只需要利用X+Y+Z坐标始终为0这个特性即可。我们可以在HexCoordinates中创建一个方法用来计算Y的坐标值,然后在ToStringToStringOnSeparateLines方法中调用它。代码如下:

HexCoordinates.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
33
34
35
36
37
38
39
40
41
42
43
44
45
//计算Y的坐标值并存储下来
public int Y
{
get
{
return -X - Z;
}
}

/// <summary>
/// 进行X与Z的坐标转换,将X方向锯齿状的排列,改为斜向的排列
/// 这个方法将mesh和坐标值分开处理了
/// 这里的入参只是处理X和Z的坐标,与mesh的排列和位置无关
/// </summary>
/// <param name="x">原始cell的x轴坐标</param>
/// <param name="z">原始cell的z轴坐标</param>
/// <returns>目前返回传入的参数值</returns>
public static HexCoordinates FromOffsetCoordinates(int x, int z)
{
return new HexCoordinates(x - z / 2, z);
}

/// <summary>
/// 重载默认的ToString方法,使其返回的是转换后的X和Z的坐标值
/// </summary>
/// <returns>X和Z的坐标值</returns>
public override string ToString()
{
//return "(" + X.ToString() + ", " + Z.ToString() + ")";

//加入了Y坐标值的表示
return "(" + X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")";
}

/// <summary>
/// 将转换后的X和Z的坐标值添加换行符,以便显示在UGUI的每个cell上
/// </summary>
/// <returns>添加换行符后的X和Z,符合Text组件的富文本格式</returns>
public string ToStringOnSeparateLines()
{
//return X.ToString() + "\n" + Z.ToString();

//加入了Y坐标值的输出
return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString();
}

  完成以上代码后,回到Unity,点击Play按钮查看效果如下图。这样我们在不改变mesh排列的情况下,重新排列了每个地图单元的坐标。并且添加了Y维度的坐标轴,完善了整个地图的坐标系统。

  下一章,我们会使用脚本将计算好的坐标显示在Inspector上,这样将更加便于我们调试和观察坐标的变化。