这里的六边形地图,指SLG游戏中的大世界地图,用六边形的网格进行平铺。例如文明,以及部分SLG手游。
这里介绍六边形地图格子的基本概念,以及具体数学实现代码。
1、基本概念
这里强制规定,所有的格子都是尖头朝上(Z轴正向)。
地图的起点在左下角(保证一个轴的值为正)。
1.1、 格子坐标
六边形的坐标,在逻辑计算中采用立体坐标,坐标的三个值之和为0 (X+Y+Z =0 )。坐标定义可以设置为 XYZ,也可以设置为 QRS ,如下图所示:
坐标的三个值分别代表六边形的三个朝向的偏移值。
之所以使用立体坐标,因为其对称性好,而且简便公式是最多的。其缺点是在视图上不够直观,不能直观地反应出平面位置。所以,在针对玩家进行显示时,建议不要直接使用立体坐标,而是转换成平面坐标(XY)。
1.2、 格子尺寸
关于格子有以下几个基本概念:
内径:中心到边的距离(OB);
外径:中心到角的距离(OA);
显然,这里边长就等于外径 (AC = OA)。
经过简单的三角函数计算,内径和外径的比例如下:
/// <summary>
/// 内径/外径 3^0.5 / 4
/// </summary>
public const float InnerToOutter = 0.8660254037844386f;
/// <summary>
/// 外径/内径 4 / 3^0.5
/// </summary>
public const float OutterToInner = 1.154700538379252f;
建议保证内径为整数,这样在做寻路计算时,走的格子距离就是整数。
1.3、 格子方向
对于六边形格子,是只能通过边跨越到另一个格子,且尖头朝上。因此方向就是:正左、正右、左上、右上、左下、右下六个方向。在坐标中的转换如下:
public static readonly int3[] HexDirOffset = new int3[]
{
new int3(0,0,0),//原点
new int3(1,-1,0),//正右
new int3(0,-1,1),//右上
new int3(-1,0,1),//左上
new int3(-1,1,0),//正左
new int3(0,1,-1),//左下
new int3(1,0,-1),//右下
};
2、 格子平铺
六边形的格子平铺也是尽量铺成矩形,这样方便设计地图。但这样的平铺,在计算边界时就要注意在X轴上要加上垂直偏移。
如上图所示,在平铺,左下为【0,0,0】的情况下,在X轴上会出现负数,但是Z轴确定是正数,Y轴是负数。
提供给玩家显示的时候,改为平面坐标(XY),转换方式下文会提到。
结果如下图所示:
平铺坐标的显示范围就能和配置的范围相同了,如上图水平、垂直都是10*10。但是视图坐标只用来显示用,逻辑计算还是用立体坐标计算。
3、 坐标转换
3.1、 六边形坐标转换为Unity坐标
ColumnLength : 列间距(内径的2倍);
HexTileInnerRadius : 内径 ;
RowLength : 行间距(外径的1.5f倍) ;
/// <summary>
/// 把六边形坐标转换成Unity坐标
/// (Y 轴为0);
/// </summary>
/// <param name="hexCoordinate">六边形地块</param>
/// <returns>Unity世界坐标,六边形的中心点</returns>
public static float3 HexCoordineToUnityPos_V3(this HexGrid hexCoordinate)
{
float x = (hexCoordinate.x * ColumnLength) + (hexCoordinate.z * HexTileInnerRadius);
float z = hexCoordinate.z * RowLength;
return new float3(x, 0, z);
}
3.2、 Unity坐标转换为六边形坐标
使用三角函数进行计算:
代码如下:
HexTileOuterRadius : 外径
/// <summary>
/// 将世界坐标(UnityPos) 转换到目标 HexGrid 下标
/// (这个方法不是很精确)
/// </summary>
/// <param name="unityX">世界坐标</param>
/// <param name="unityZ">世界坐标</param>
/// <returns>所属的 HexTile 坐标</returns>
public static HexGrid UnityPosToHexGrid(float unityX, float unityZ)
{
float q = (Sqrt_3 / 3.0f * unityX) - (1.0f / 3.0f * unityZ);
float r = 2.0f / 3.0f * unityZ;
return new HexGrid(HexRound(q / HexTileOuterRadius, r / HexTileOuterRadius));
}
/// <summary>
/// 坐标规整;
/// </summary>
private static Vector2Int HexRound(float q, float r)
{
float s = -q - r;
int rq = Mathf.RoundToInt(q);
int rr = Mathf.RoundToInt(r);
int rs = Mathf.RoundToInt(s);
float q_diff = Mathf.Abs(rq - q);
float r_diff = Mathf.Abs(rr - r);
float s_diff = Mathf.Abs(rs - s);
if (q_diff > r_diff && q_diff > s_diff)
rq = -rr - rs;
else if (r_diff > s_diff)
rr = -rq - rs;
return new Vector2Int(rq, rr);
}
关于坐标规整:
因为在计算时,通过三角函数会有小数的问题,将其四舍五入成整数,不总是能得到最近的格子坐标,且保证三个数之和为0 。
因此需要对坐标进行规整计算。
3.3、 立体坐标和视图坐标的转换
因为存在多种视图坐标和立体坐标的表示方法,因此坐标转换一定是要配套使用。
但,一般在一个游戏中,会限定同一种表示方法。因此可以直接用如下代码进行计算。
3.3.1、 立体坐标转换为视图坐标
x轴减去y轴的值,并出2即可:
/// <summary>
/// 转换成视图坐标
/// </summary>
public static Vector2Int ToViewCoordiante(this Vector3Int logicCoordiante)
{
int x = (logicCoordiante.x - logicCoordiante.y) / 2;
int y = logicCoordiante.z;
return new Vector2Int(x, y);
}
3.3.2、 视图坐标转换为立体坐标
/// <summary>
/// 转换成立体坐标
/// </summary>
public static HexGrid ToCubeCoordiante(this Vector2Int viewGrid)
{
int z = viewGrid.y;
int x = viewGrid.x - viewGrid.y / 2;
return new HexGrid(x, z);
}