六边形网格

原文地址:http://www.redblobgames.com/grids/hexagons/#line-drawing

六边形网格在一些游戏中被用到了,但并不像正方形网格那么直截了当的容易使用.我曾经手机了六边形网格相关的资源接近20年了,写这篇向导文章去探索那些最优美的方法,分析成最简洁风格的代码,主要是基于Charles Fu 和Clark Verbrugge向导.我将描述制作六边形网格的各种各样的方式,它们之间的关系,就像一些常见的算法一样.该文中许多部分都是互相有关联的;选择一个类型的网格去更新图标,代码,和文本去匹配,感受不同类型网格对应的代码变化,以及他们的原理.

该文中的样例代码是用伪代码(pseudo-code)写出来的;它们是容易阅读和被理解的,所以你可以参照它们写出自己使用语言对应的实现代码.

Geometry 几何学

六边形是六个边的多边形.规则的多边形的所有边都有着相同的长度.我将我们使用的多边形都假定是规则的.典型的六边形网格可以是纵向的 以及横向的.

纵向的(左)跟横向的(右)如下图:

六边形有六个边.每个边被两个六边形所共享.六边形有六个角.每个角被3个六边形共享.关于更多的关于中心,边缘,以及角相关的额内容,请看网格部分文章(矩形,六边形,三角形).

角度

在一个规则的六边形中,内角都是120度.里面可以容纳六个三角形楔形物,每个等边三角形内角都是60度.角i在坐标系中对应的弧度是(60*i),  size对应的是从中心点到六边形各个角顶点的距离.代码如下:

  
  
  1. function hex_corner(center, size, i):
  2.    var angle_deg = 60 * i  
  3.    var angle_rad = PI / 180 * angle_deg
  4.    return Point(center.x + size * cos(angle_rad),
  5.                 center.y + size * sin(angle_rad))

填充一个六边形,通过hex_corner函数进行六个顶点的计算.要画六边形,则使用计算出来的这些顶点进行画线操作即可.

横向与纵向模式下的区别是x跟y换掉了.横向模式下,角度是:0,60,120,180,240,300

纵向模式下角度是:30,90,150,210,270,330.

大小和跨距

下一步,我们想把几个六边形放在一起研究.在横向模式下,一个六边形的宽度=2*size.两个邻接的六边形在x轴方向上两个邻接六边形中心点间距离=size*3/2.

六边形的高度=size*sqrt(3),y轴上两个邻接六边形中心间的距离也是等于size*sqrt(3).

一些游戏中使用像素艺术去生成六边形,它们准确的说是不匹配咱们几何中所说的六边形的.我在这里描述的角度和跨度公式是没有办法为你的六边形进行计算.剩下的篇幅中,将描述在六边形网格中的算法,即使你的六边形是被拉伸或者收缩过的情况下也是可以工作的.

Coordinate Systems  坐标系统

现在让我们假定六边形放在网格中.四四方方的网格中,有一种很明显的方式可以去实现它.对于六边形,可以有多种实现方法.我推荐使用立体坐标系统作为主要的表现方式.使用坐标轴或者偏移坐标量来实现地图存储,展示坐标给使用者.

Offset coordinates 偏移量坐标

最常见的方法是偏移所有的行或者列.列 命名为col 或者q.  行 命名为row 或者r.你可以偏移奇数或者偶数列或者行,所以每个横向或者纵向的上的六边形都有两个变量来记录位置.

Cube coordinates立体坐标

另一种方式可以在三个主坐标轴下布置六边形,不像之前在方形网格中有两个轴那样.这种坐标下有个更优美的对称性.


我们采用一个立体网格,然后用x+y+x = 0 切割对角线平面.这是一个怪异的想法,但是它确实帮助我们使得六边形网格算法更简单一些.特别是,我们可以重用笛卡尔坐标系统中的标准操作:坐标相加,坐标相减,坐标乘除法缩放操作等.

注意立体坐标中的三个主轴,观察它们是如何与六个六边形对角线方向上进行通信的;对角线网格轴如何跟六边形网格方向交互的.

因为我们已经在正方形以及立体网格中实现过的一些算法,立体坐标系统允许我们修改下算法就可以应用上六边形网格了(也就是转换一下坐标).我将打算在本页中的算法中都使用这个坐标系统.如果之前有算法使用其他的坐标系统的话,我将转换坐标们转换为立体坐标,运行那个算法.运行完了,再转换回去.

学习三维坐标是如何工作在六边形网格中.选择下图中的一个六边形 将高亮三维坐标系统中对应的坐标哟.具体看原文的下图:

http://www.redblobgames.com/grids/hexagons/

1.在立体网格中的每个方向都对应六边形网格中的一条线.尝试高亮z值为0,1,2,3的一个六边形,然后观察他们是怎么关联的.行row被标记为蓝色.同样尝试x(绿色)以及y(紫色).

2.六边形网格中的每个方向都跟立体网格中的两个方向有关联.例如,六边形网格北边在+y和-z之间的区域,所以往北每走一步都会让y+1,z-1.

立体坐标系统对于六边形问你哥哥坐标系统来说是个挺合理的选择.由于限制x+y+z=0 ,所以算法必须保持那样.这个限制可以确保对于每个六边形都有一个笛卡尔坐标可以对应.

有许多不同的有效的三维六边形坐标系统.有的会限制为x+y+z=0.我已经在许多系统中试过的唯一一种.你也可以去使用x-y,y-z,z-x去限制三位坐标系统,那样它将有自己独有的有趣的我之前没有探索过的属性集.

可能你会说,"Amit,但是我想去存储三个数字的坐标.我不知道那样的方式下该怎样去存储一个地图".

Axial coordinates 轴坐标

二维轴坐标系统,有时候也可称为梯度坐标,构建方法是从三维立体坐标系统中取出两个来进行组建的.因为我们在立体坐标中的那个限制x+y+z=0 中,第三个坐标是冗余的.二维轴坐标对于地图存储和显示坐标来说是足够有用了.像三维立体坐标系统,你可以用笛卡尔坐标系统中的标准的加,减,乘,除操作.

有许多的立体坐标系统,以及许多坐标轴系统.我可不打算在文章中去展示所有的关联.我打算挑两种去解释,q=x,r=z.你可以把q想成列,r想成行.

好处就是这样的坐标系统在偏移网格上算法分析运算都是非常干净的.坏处嘛,存储直角坐标系中的地图的化看起来会有那么一点点怪咯;细节请看地图存储处理这里.有一些算法甚至是比三维立体坐标上都表现的干净,但是对于那些,因为我们曾经限制x+y+z=0 ,所以我们可以计算出第三个隐式坐标并且使用它,只是为了那些必须要用到第三个坐标的算法.

Axes 轴

偏移坐标系统是人们最常向导的,因为它跟人们认知中二维方形网格中的笛卡尔坐标系统相似.不幸的是坐标轴的方向却和我们习惯的数学坐标轴方向有些不同,使得问题又有些复杂了.三为立体坐标系统和坐标轴偏移系统沿着轴的方向,就可以得到简单的算法实现,但是对于地图存储则会变得复杂了些.也有其他的坐标系统,也被称为交错的或者是重复的,我还没有探索这块;有的人说它工作起来会比三维立体坐标系统和轴坐标系统更容易.有机会看看去.

轴是坐标增长的方向.垂直于轴的是一条直线,在它上面都是一个常量.上面展示的那些直线就是存有等值的等高线.上图那些绿线紫线.

Coordinate conversion  坐标转换

你可能在你的工程中使用的是轴坐标或者偏移坐标,但是许多算法在三维立体坐标中表示出来才最简单.因此,你需要在各个坐标系统间进行转换.

轴坐标系统跟立体系统坐标是最接近的,所以转换起来也非常容易:

  
  
  1. # convert cube to axial
  2. q = x
  3. r = z
  4. # convert axial to cube
  5. x = q
  6. z = r
  7. y = -x-z

代码中,这两个函数可以写成这样:

  
  
  1. function cube_to_hex(h): # axial
  2.    var q = h.x
  3.    var r = h.z
  4.    return Hex(q, r)
  5. function hex_to_cube(h): # axial
  6.    var x = h.q
  7.    var z = h.r
  8.    var y = -x-z
  9.    return Cube(x, y, z)
偏移坐标系统是唯一有些复杂的:
  
  
  1. # convert cube to even-q offset
  2. col = x
  3. row = z + (x + (x&1)) / 2
  4. # convert even-q offset to cube
  5. x = col
  6. z = row - (col + (col&1)) / 2
  7. y = -x-z
  8. # convert cube to odd-q offset
  9. col = x
  10. row = z + (x - (x&1)) / 2
  11. # convert odd-q offset to cube
  12. x = col
  13. z = row - (col - (col&1)) / 2
  14. y = -x-z
  15. # convert cube to even-r offset
  16. col = x + (z + (z&1)) / 2
  17. row = z
  18. # convert even-r offset to cube
  19. x = col - (row + (row&1)) / 2
  20. z = row
  21. y = -x-z
  22. # convert cube to odd-r offset
  23. col = x + (z - (z&1)) / 2
  24. row = z
  25. # convert odd-r offset to cube
  26. x = col - (row - (row&1)) / 2
  27. z = row
  28. y = -x-z

实现笔记:我使用a&1而不是a%2来检测是否为奇数或者偶数.更细节的东西请看这里.

Neighbors   邻居们

给定一个六边形,哪六个六边形是它的邻居嘞?正如你想的那样,答案跟在三维立体坐标中是一样简单,一直都是跟轴坐标中那样非常简单的,只有偏移坐标系统中会有些复杂.你可能还想去计算6个对角线方向上对应的六边形(后面看到图就知道什么意思了,说不清楚).

三维立体坐标轴

在六边形坐标系统中移动,会导致三个坐标中的一个+1,一个-1(总和一定保持为0).有三种+1可能发生的场景,剩下的两个有一个可能会-1.总共6中可能的改变.每种都对应与六边形一个方向上.最简单最快的方法是预先计算出转换的这个表如下图,用的时候直接从这个表中取就好了:

  
  
  1. var directions = [
  2.   Cube(+1, -1,  0), Cube(+1,  0, -1), Cube( 0, +1, -1),
  3.   Cube(-1, +1,  0), Cube(-1,  0, +1), Cube( 0, -1, +1)
  4. ]
  5. function cube_direction(direction):
  6.    return directions[direction]
  7. function cube_neighbor(hex, direction):
  8.    return cube_add(hex, cube_direction(direction))

Axial coordinates 轴坐标系统

正如之前的,我们将使用立体坐标系统作为起始点.根据这个表将(dx,dy,dz)转换为(dq,dr).

  
  
  1. var directions = [
  2.   Hex(+1,  0), Hex(+1, -1), Hex( 0, -1),
  3.   Hex(-1,  0), Hex(-1, +1), Hex( 0, +1)
  4. ]
  5. function hex_direction(direction):
  6.    return directions[direction]
  7. function hex_neighbor(hex, direction):
  8.    var dir = hex_direction(direction)
  9.    return Hex(hex.q + dir.q, hex.r + dir.r)

Offset coordinates  偏移坐标

在偏移坐标系统中,这些改变决定于表格中我们已经给定的信息.如果当前正在偏移行或者列上时,规则是不同的,而不像在那些非偏移行列坐标系统中那样.

正如上面所说的,我们将构建一个数字表去与给定的列号行号相加处理.然而这一次我们这里将有两个数组,一个是对于奇数列/行 使用的,一个是给偶数行/列用的.观察图表中(1,1)在你选择图中六个方向中的一个时它的变化情况.然后看(2,2).这个表格以及对应的代码对于四种表格类型是各不相同的,挑选一种类型观察一下吧.

  
  
  1. var directions = [
  2.   [ Hex(+1,  0), Hex( 0, -1), Hex(-1, -1),
  3.     Hex(-1,  0), Hex(-1, +1), Hex( 0, +1) ],
  4.   [ Hex(+1,  0), Hex(+1, -1), Hex( 0, -1),
  5.     Hex(-1,  0), Hex( 0, +1), Hex(+1, +1) ]
  6. ]
  7. function offset_neighbor(hex, direction):
  8.    var parity = hex.row & 1
  9.    var dir = directions[parity][direction]
  10.    return Hex(hex.col + dir.col, hex.row + dir.row)

用上面查找表是计算邻居最简单的方式了.对于那些你感觉奇怪的数字,可以在这里了解下.

Diagonals 对角线上的

在六边形坐标系统中,移动到一个对角线指定的位置上的六边形上时,三个坐标中有一个要或+2或-2,其他的两个则都-1或+1.

  
  
  1. var diagonals = [
  2.   Cube(+2, -1, -1), Cube(+1, +1, -2), Cube(-1, +2, -1),
  3.   Cube(-2, +1, +1), Cube(-1, -1, +2), Cube(+1, -2, +1)
  4. ]
  5. function cube_diagonal_neighbor(hex, direction):
  6.    return cube_add(hex, diagonals[direction])

跟之前的一样,你可以消去坐标中的某一维实现向轴坐标的转换,或者通过与计算出来的矩阵表转换为偏移坐标系统坐标.

Distances  距离

Cube coordinates 立体坐标系统

在立体坐标系统中,每一个六边形都是三维的立方体. 在六边形图标中距离为1,而在立体坐标系中距离为2.这使得求距离是简单的.在平面坐标系中,曼哈顿距离=abs(dx)+abs(dy).在立方体坐标系中,曼哈顿距离=abs(dx)+abs(dy)+abs(dz).在六面体表格中距离是下面函数计算结果的一半:

  
  
  1. function cube_distance(a, b):
  2.    return (abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z)) / 2

任何一个坐标中的三个维度中,其中一个必须是另外两个相加之和,然后票选出那个最大的和就是那个距离了.你可能喜欢分成两部分中的一份或者喜欢称为最大的那个(等于另外两个之和)的维度值作为距离,但是不论怎样,得出的结果都是相同的.

  
  
  1. function cube_distance(a, b):
  2.    return max(abs(a.x - b.x), abs(a.y - b.y), abs(a.z - b.z))

在图标中,最大值是被高亮出来了.也请注意每个颜色区域都指示除了6个对角方向中的一个方向.

Axial coordinates 轴坐标系统

在轴坐标系统中,第三个维度是被隐藏起来了.可以转换轴坐标为立体坐标然后进行距离计算:

  
  
  1. function hex_distance(a, b):
  2.    var ac = hex_to_cube(a)
  3.    var bc = hex_to_cube(b)
  4.    return cube_distance(ac, bc)

如果你编译了内联函数hex_to_cube 和 cube_distance 的话,它将生成下面这段代码:

  
  
  1. function hex_distance(a, b):
  2.    return (abs(a.q - b.q)
  3.          + abs(a.q + a.r - b.q - b.r)
  4.          + abs(a.r - b.r)) / 2

有大量的不同方法去计算六边形在轴坐标系统中的距离,但是不论你选用哪种,轴坐标中六边形距离计算都是源于立方体坐标系统中曼哈顿距离的计算方式.例如,

Offset coordinates  偏移坐标系统

同轴坐标系统中一样,我们将转换偏移坐标系统中的坐标为立体坐标系统中的立体坐标,然后使用立体坐标计算距离.

  
  
  1. function offset_distance(a, b):
  2.    var ac = offset_to_cube(a)
  3.    var bc = offset_to_cube(b)
  4.    return cube_distance(ac, bc)

我们将使用相同的模型到许多用到的算法中:转换六边形网格系统中的坐标为立体坐标系统中的坐标,然后在立体坐标系统中对应的算法,然后把计算出来的结果坐标再转换回去.

Line drawing  六边形间连线

我们该怎样去画一条从一个六边形到另一个六边形的线呢?我使用了直线插补法去实现绘画.均匀平滑的画出了N+1个点间的线,并且计算出了当前位置是在哪个六边形中.

1.首先我们需要计算两个终端节点之间的距离N.

2.然后平滑下两个终端节点A和B之间的N+1个节点.使用线性插补法,从0到N个节点中,第i个点都对应的坐标都可以通过 A + (B - A) * 1.0/N * i 来计算得出.在图标中这些点用深色以及点描出.计算结果如图所示.

3.转换每个浮点型样例点坐标为整形的六边形坐标系统中的坐标.这个算法叫做cube_round.

将这些放在一起就可以画出A到B的线了:

  
  
  1. function cube_lerp(a, b, t):
  2.    return Cube(a.x + (b.x - a.x) * t,
  3.                a.y + (b.y - a.y) * t,
  4.                a.z + (b.z - a.z) * t)
  5. function cube_linedraw(a, b):
  6.    var N = cube_distance(a, b)
  7.    var results = []
  8.    for each 0 i N:
  9.        results.append(cube_round(cube_lerp(a, b, 1.0/N * i)))
  10.    return results

更多注意:

  • DDA算法在平面网格集中坐标轴方向上最大的距离就是N.

  • 有时候如果你添加一个很小的值(1e-6,-2e-6级别的增量)到起始点到终点中的某一点的坐标上时,画出来的线将会看起来更加平滑连续。这样将在某个方向上推进线的延伸而且可以避免锐化的边缘出现。
  • cube_lerp函数需要通过浮点坐标返回一个立体坐标。如果你使用的静态类型语言的化,你不可以使用例题类型坐标但是你可以定义FloatCube浮点立体坐标,或者如果你想避免定义其他类型的话,你可以将画线函数中的函数内联。

  • 你可以通过内联cube_lerp函数去优化这段代码,然后再循环外计算B.x-A.x,B.x-A.y,以及1.0/N。乘法操作可以转化为重复的加法运算。你可能在最后发现算法很像DDA算法了。

  • 我使用的是轴坐标系统或者立体坐标系统中画线的,但是如果你想用其他的坐标系统的话,看一下这篇文章

  • 有许多画线的方法。有时候你将想用“super cover”算法。有人给我发送过super cover画线的代码,可惜我还没有学会。

  • Movement Range  移动范围

Coordinate range 坐标范围

给定一个中心六边形以及一个距离范围N,哪些六边形是跟中心六边形距离为N的范围之内呢?



我们可以回顾下六边形距离计算公式,distance = max(abs(dx),abs(dy),abs(dz))。找寻所有的距离在N范围内的六边形,我们需要使用 max(abs(dx),abs(dy),abs(dz)) <= N.意思是 我们需要三个条件都成立abs(dx) <=N  ,abs(dy)<=N ,abs(dz) <=N。移除绝对值符号,我们就得出了 -N<= dx <= N,-N<=dy<=N,-N<=dz<=N .在代码中,它表现为一个嵌套循环:

    
    
  1. var results = []
  2. for each -N dx N:
  3.    for each -N dy N:
  4.        for each -N dz N:
  5.            if dx + dy + dz = 0:
  6.                results.append(cube_add(center, Cube(dx, dy, dz)))

虽然这个循环可以计算出结果来,但是却不高效.dz对应的所有值都循环了一遍,但事实上他们中却只有iyige满足立体坐标系统中的限制表达式dx+dy+dz=0 的条件.因此,我们可以直接根据限制表达式直接计算得出dz,而不必再去循环求出.

    
    
  1. var results = []
  2. for each -N dx N:
  3. for each max(-N, -dx-N) dy min(N, -dx+N):
  4. var dz = -dx-dy
  5. results.append(cube_add(center, Cube(dx, dy, dz)))

循环迭代了所有坐标中符合条件的坐标点。在图表中,每个范围都对应于一组线。每条线都对应一个不等式。我们挑选出所有满足六个不等式条件的额六边形。

Intersecting ranges 交叉区域


如果你需要查看六边形们所在的区域是不是不止一个区域内,你可以在生成六边形之前交叉这些区域看看.

你可能想用代数方法或者几何方法去解决这些问题.代数方法,每一个区域都应满足不等式-N<= dx <= N所限制的表达式.几何方法解决的话,每一片驱雨都是在3D空间的立体几何,我们打算在3D空间中交叉两个立方体,在3D空间中生成一个新的立方体,然后映射 x+y+z=0 平面中得到六边形.我打算使用代数方法解决这个问题:


首先,我们重写了限制表达式 -N<= dx <= N为更通用化的形式 Xmin <= x <= Xmax,并且设置Xmin = center.x-N,Xmax=center.x+N.对于y,z 轴做同样的处理,得出之前这个选区问题的更通用化的代码:

   
   
  1. var results = []
  2. for each xmin x xmax:
  3. for each max(ymin, -x-zmax) y min(ymax, -x-zmin):
  4. var z = -x-y
  5. results.append(Cube(x, y, z))

数学上两个区间的交叉区域a <= x <= b以及 c <= x <= d,可以合并为max(a,c) <= x <= min(b,d). 因为六边形区域是表示为x,y,z上,我们可以分别计算x,y,z上的交叉区域,然后用嵌套循环去生成在交叉区域中的六边形.对于一个六边形,我们设定Xmin = H.x-N 以及 Xmax = H.x+N ,同样的方法也给y,z轴上设置了.对于两个六边形的交叉区域,我们设置Xmin = max(H1.x - N,H2.x - N)以及Xmax = min(H1.x+N,H2.x+N),同样的方法设置了y,z轴上.这个模型可以在三个或者更多的交叉区域上的工作.

Obstacles  障碍

 如果存在障碍物的话,限制距离后的泛洪法(广度优先搜索)是最简单不过的事情了.在下图图表中,限制步数为7的情况.在代码中,fringes[k]是一个存储了在限制为k步内可以到达的所有六边形的数组.每一次主循环,都会向外扩张一层.


   
   
  1. function cube_reachable(start, movement):
  2. var visited = set()
  3. add start to visited
  4. var fringes = []
  5. fringes.append([start])
  6. for each 1 < k movement:
  7. fringes.append([])
  8. for each cube in fringes[k-1]:
  9. for each 0 dir < 6:
  10. var neighbor = cube_neighbor(cube, dir)
  11. if neighbor not in visited, not blocked:
  12. add neighbor to visited
  13. fringes[k].append(neighbor)
  14. return visited

Rotation 旋转

不再是考虑一个六边形到另一个六边形了,现在假定给了一个六边形数组(6个刚好组成宏观上的六边形),你可能想要将其旋转一下.


仔细观察应该可以发现,当前这个宏观六边形中的六个坐标点,右旋转60度后刚好如下面代码中的旋转映射情况:

   
   
  1. [ x, y, z]
  2. to [-z, -x, -y]

同理,左旋60度对应的坐标点跟上一个坐标点的映射关系也是有规律的,如下所述:

   
   
  1. [ x, y, z]
  2. to [-y, -z, -x]

当你在图表中挪动鼠标时,可以注意到每60度旋转,正负号和坐标位置都翻转了下.120度旋转时坐标上对应的数值对应的正负号又回来了,只是位置还不对;180度旋转时,坐标上对应的数字跟执勤啊的一样了,只是符号变化了.(看图找规律找规律)

你可以转换这些为轴坐标或者偏移坐标系统.在这里看其他方式的旋转计算.

Rings



猜你喜欢

转载自blog.csdn.net/sky_person/article/details/50173527
今日推荐