【问题标题】:Hexagonal Grids, how do you find which hexagon a point is in?六边形网格,你如何找到一个点在哪个六边形中?
【发布时间】:2011-12-04 01:11:11
【问题描述】:

我有一张由六边形行和列组成的地图

这不是我使用的十六进制地图的实际图像,而是使用相同大小和形状的六边形

当用户点击时,我需要能够分辨出鼠标在哪一个,

每个六边形都由一个“Tile”类的实例表示,但是它不包含任何特定于位置的数据,甚至不包含多边形,所以基本上判断特定六边形在哪里的唯一方法就是知道它是在二维数组中的位置。

我之前用过方形格子,比较容易判断选择了哪个方形,因为像素也是方形的,

// Example where each square is 10 by 10 pixels:
private void getClickedSquare(MouseEvent me)
{
    int mouseX = me.getX(); // e.g. 25
    int mouseY = me.getY(); // e.g. 70

    int squareX = (int)(mouseX / 10); // in this case 2
    int squareY = (int)(mouseY / 10); // in this case 7

    // Then to access the tile I would do
    map.squares[squareX][squareY].whatever();
}

但我什至不知道从哪里开始使用 Hexagons,有人有经验吗?

我不能使用多边形 (Java),因为当我开始在屏幕上移动地图并增加它的大小时,我会遇到每帧更新大量多边形的问题。虽然那时我可以检查一个点是否包含在地图的任何图块的多边形中!

目前显示的六边形只是 BufferedImages。

如果您想了解更多信息,请询问, 感谢您的时间:D

【问题讨论】:

标签: algorithm point hexagonal-tiles


【解决方案1】:

(更新:重构代码以使其更易于理解和更高效) (更新:缩短答案长度,修复代码错误,提高图像质量)

这张图片显示了一个六边形网格的左上角,并覆盖了一个蓝色方形网格。很容易找到一个点在哪个正方形内部,这也可以粗略估计哪个六边形。六边形的白色部分显示正方形和六边形网格共享相同坐标的位置,而六边形的灰色部分显示它们不共享的位置。

解决方案现在很简单,只需找到一个点在哪个框内,然后检查该点是否在任一三角形中,并在必要时更正答案。

private final Hexagon getSelectedHexagon(int x, int y)
{
    // Find the row and column of the box that the point falls in.
    int row = (int) (y / gridHeight);
    int column;

    boolean rowIsOdd = row % 2 == 1;

    // Is the row an odd number?
    if (rowIsOdd)// Yes: Offset x to match the indent of the row
        column = (int) ((x - halfWidth) / gridWidth);
    else// No: Calculate normally
        column = (int) (x / gridWidth);

此时我们有了我们的点所在的盒子的行和列,接下来我们需要根据六边形的两个顶部边缘来测试我们的点,看看我们的点是否在上面的任何一个六边形中:

    // Work out the position of the point relative to the box it is in
    double relY = y - (row * gridHeight);
    double relX;

    if (rowIsOdd)
        relX = (x - (column * gridWidth)) - halfWidth;
    else
        relX = x - (column * gridWidth);

拥有相对坐标使下一步更容易。

如上图所示,如果我们的点的 y> mx + c,我们知道我们的点位于直线上方,在我们的例子中,是六边形当前行和列的上方和左侧。 请注意,Java 中的坐标系 y 从屏幕左上角的 0 开始,而不是数学中通常的左下角,因此负梯度用于左边缘,正梯度用于右边缘.

    // Work out if the point is above either of the hexagon's top edges
    if (relY < (-m * relX) + c) // LEFT edge
        {
            row--;
            if (!rowIsOdd)
                column--;
        }
    else if (relY < (m * relX) - c) // RIGHT edge
        {
            row--;
            if (rowIsOdd)
                column++;
        }

    return hexagons[column][row];
}

上例中使用的变量的简要说明:

m 是渐变,所以 m = c / halfWidth

【讨论】:

  • 我什至无法解释这篇文章为我节省了多少时间。我真的不能为此感谢你。
  • 没问题 :) 如果您需要任何其他帮助,请查看我的博客,我的电子邮件和一些开源项目在我的 github 上,这些项目的数量只会增加 :) troygamedev。 blogspot.co.uk
  • 旧帖子,显然有用,但是当您引用的网格不是由正方形组成,而是由矩形组成时,您一直在说“蓝色正方形”。您是否意识到这一点,您的意思是矩形吗?几何图形未对齐以从高边的底部顶点开始绘制正方形,到尖角六边形的顶部。
  • @pstatix 是的,我相信我的意思是矩形。
  • 8 年后,这个答案仍在帮助人们。谢谢!
【解决方案2】:

编辑:这个问题比我最初想象的要难,我会用一些工作重写我的答案,但是我不确定解决方案路径是否对其他答案有任何改进。

问题可以改写:给定任何 x,y 找到中心最接近 x,y 的六边形

即在 n 上最小化 dist_squared( Hex[n].center, (x,y) )(平方意味着您无需担心平方根,这会节省一些 CPU)

但是,首先我们应该缩小要检查的六边形的数量——我们可以通过以下方法将其缩小到最多 5 个:

所以,第一步是在 UV 空间中表达您的点 (x,y) 即 (x,y) = lambdaU + muV, 所以 = (lambda, mu) 在 UV 空间中

这只是一个 2D 矩阵变换(如果您不了解线性变换,http://playtechs.blogspot.co.uk/2007/04/hex-grids.html 可能会有所帮助)。

现在给定一个点 (lambda, mu),如果我们将两者都四舍五入到最接近的整数,那么我们有:

绿色广场内的所有地方都映射回 (2,1)

所以绿色正方形内的大多数点都是正确的,即它们是六边形 (2,1)。

但有些点应该返回六边形#(2,2),即:

同样,有些应该返回六边形 # (3,1)。然后在那个绿色平行四边形的对角,还有另外两个区域。

总而言之,如果 int(lambda,mu) = (p,q) 那么我们可能在六边形 (p,q) 内,但我们也可能在六边形 (p+1,q) 内,(p,q +1), (p-1,q) 或 (p,q-1)

有几种方法可以确定是哪种情况。最简单的方法是将所有这 5 个六边形的中心转换回原始坐标系,然后找到最接近我们的点。

但事实证明,您可以将范围缩小到约 50% 的时间不进行距离检查,约 25% 的时间进行一次距离检查,其余约 25% 的时间进行 2 次距离检查(我'我通过查看每个检查的区域来猜测数字):

p,q = int(lambda,mu)

if lambda * mu < 0.0:
    // opposite signs, so we are guaranteed to be inside hexagon (p,q)
    // look at the picture to understand why; we will be in the green regions
    outPQ = p,q

else:
    // circle check
    distSquared = dist2( Hex2Rect(p,q), Hex2Rect(lambda, mu) )

    if distSquared < .5^2:
        // inside circle, so guaranteed inside hexagon (p,q)
        outPQ = p,q

    else:
        if lambda > 0.0:
            candHex = (lambda>mu) ? (p+1,q): (p,q+1)
        else:
            candHex = (lambda<mu) ? (p-1,q) : (p,q-1)

最后的测试可以整理了:

     else:
        // same sign, but which end of the parallelogram are we?
        sign = (lambda<0) ? -1 : +1
        candHex = ( abs(lambda) > abs(mu) ) ? (p+sign,q) : (p,q+sign)

现在我们已经把它缩小到另一个可能的六边形,我们只需要找到更接近的一个:

        dist2_cand = dist2( Hex2Rect(lambda, mu), Hex2Rect(candHex) )

        outPQ = ( distSquared < dist2_cand ) ? (p,q) : candHex

Dist2_hexSpace(A,B) 函数可以进一步整理。

【讨论】:

  • Cos 和 Sin 的计算量不是很大吗?
  • 您可以预先计算它们,因为您知道它是 60°。如果我没记错的话 (cos60,sin60) 是 (1/2, root(3)/2)
  • 听起来像是一个完全有效的解决方案,但我不确定它是否会比上述方法更快,您认为您可以提供一些伪代码吗?
  • 我改了答案,放了一些图片。
  • 它仍然适用于“扁平”六边形。 U 和 V 只是不同而已。我仍然觉得我们缺少一些非常简单聪明的方法......以某种方式使用等距网格的三向对称性,也许得到3个解决方案集并找到交点。但我看不太清楚。
【解决方案3】:

我从查看@pi 的答案https://stackoverflow.com/a/23370350/5776618 开始,并认为在具有UVW 空间(而不是二维、轴向、UV 空间)的立方体坐标中尝试类似的东西会很有趣。

以下方程映射(x,y) => (u,v,w)

u = (2/3)*x;
v = -(1/3)*x + (1/2)*y;
w = -(1/3)*x - (1/2)*y;

那么就很简单,将u、v、w四舍五入到最接近的整数,再转换回x,y。但是有一个主要的障碍......

在上面的答案中,注意到 UV 空间中的舍入将有一些区域映射不正确:

在使用立方体坐标时仍然会发生这种情况:

橙色三角形中的任何区域距离六边形中心 >0.5 个单位,并且在四舍五入时会远离中心四舍五入。如上所示,红色三角形中的任何内容(u=1.5 线的左侧)都会将 u 错误地四舍五入为 u=1 而不是 u=2。

这里有一些关键的观察结果......

1.橙色/红色问题区域不重叠

2。在立方体坐标中,有效的十六进制中心有 u + v + w =​​ 0

在下面的代码中,u、v 和 w 都从一开始就进行四舍五入,因为只有四舍五入的坐标之和不为零时才会出现舍入问题。

uR = Math.round(u);
vR = Math.round(v);
wR = Math.round(w);

如果这些总和不为零,因为问题区域不重叠,所以只有 1 个坐标舍入不正确。这个坐标也是被四舍五入最多的坐标。

arr = [ Math.abs(u-uR), Math.abs(v-vR), Math.abs(w-wR) ];
var i = arr.indexOf(Math.max(...arr));

找到问题坐标后,在另一个方向进行四舍五入。然后根据舍入/校正后的 (u,v,w) 计算最终的 (x,y)。

nearestHex = function(x,y){

  u = (2/3)*x;
  v = -(1/3)*x + (1/2)*y;
  w = -(1/3)*x - (1/2)*y;

  uR = Math.round(u);
  vR = Math.round(v);
  wR = Math.round(w);

  if(uR+vR+wR !== 0){
    arr = [ Math.abs(u-uR), Math.abs(v-vR), Math.abs(w-wR) ];
    var i = arr.indexOf(Math.max(...arr));

    switch(i){
      case 0:
        Math.round(u)===Math.floor(u) ? u = Math.ceil(u) : u = Math.floor(u);
        v = vR; w = wR;
        break;

      case 1:
        Math.round(v)===Math.floor(v) ? v = Math.ceil(v) : v = Math.floor(v);
        u = uR; w = wR;
        break;

      case 2:
        Math.round(w)===Math.floor(w) ? w = Math.ceil(w) : w = Math.floor(w);
        u = uR; v = vR;
        break;
    }
  }

  return {x: (3/2)*u, y: v-w};

}

【讨论】:

    【解决方案4】:

    这是对 SebastianTroy 答案的补充。我会把它作为评论留下,但我还没有足够的声誉。

    如果您想实现此处所述的轴坐标系: http://www.redblobgames.com/grids/hexagons/

    您可以对代码稍作修改。

    代替

    // Is the row an odd number?
    if (rowIsOdd)// Yes: Offset x to match the indent of the row
        column = (int) ((x - halfWidth) / gridWidth);
    else// No: Calculate normally
        column = (int) (x / gridWidth);
    

    使用这个

    float columnOffset = row * halfWidth;
    column = (int)(x + columnOffset)/gridWidth; //switch + to - to align the grid the other way
    

    这将使坐标 (0, 2) 与 (0, 0) 和 (0, 1) 位于同一对角列上,而不是直接位于 (0, 0) 下方。

    【讨论】:

    • 很好,我没有考虑轴向坐标系,我会修改我的答案以包含你的观点,但我不想抢你的声誉!
    • 该链接对于任何学习如何实现十六进制网格的人来说都是一个极好的资源。 :-)
    【解决方案5】:

    我又看了一下http://playtechs.blogspot.co.uk/2007/04/hex-grids.html,它在数学上非常整洁。

    不过,Sebastian 的方法似乎切入正题,只需几行代码即可完成任务。

    如果您通读 cmets 部分,您会发现有人在 http://gist.github.com/583180 处编写了 Python 实现

    我将在此处重新粘贴以供后代使用:

    # copyright 2010 Eric Gradman
    # free to use for any purpose, with or without attribution
    # from an algorithm by James McNeill at
    # http://playtechs.blogspot.com/2007/04/hex-grids.html
    
    # the center of hex (0,0) is located at cartesian coordinates (0,0)
    
    import numpy as np
    
    # R ~ center of hex to edge
    # S ~ edge length, also center to vertex
    # T ~ "height of triangle"
    
    real_R = 75. # in my application, a hex is 2*75 pixels wide
    R = 2.
    S = 2.*R/np.sqrt(3.)
    T = S/2.
    SCALE = real_R/R
    
    # XM*X = I
    # XM = Xinv
    X = np.array([
        [ 0, R],
        [-S, S/2.]
    ])
    XM = np.array([
        [1./(2.*R),  -1./S],
        [1./R,        0.  ]
    ])
    # YM*Y = I
    # YM = Yinv
    Y = np.array([
        [R,    -R],
        [S/2.,  S/2.]
    ])
    YM = np.array([
        [ 1./(2.*R), 1./S],
        [-1./(2.*R), 1./S],
    ])
    
    def cartesian2hex(cp):
        """convert cartesian point cp to hex coord hp"""
        cp = np.multiply(cp, 1./SCALE)
        Mi = np.floor(np.dot(XM, cp))
        xi, yi = Mi
        i = np.floor((xi+yi+2.)/3.)
    
        Mj = np.floor(np.dot(YM, cp))
        xj, yj = Mj
        j = np.floor((xj+yj+2.)/3.)
    
        hp = i,j
        return hp
    
    def hex2cartesian(hp):
        """convert hex center coordinate hp to cartesian centerpoint cp"""
        i,j = hp
        cp = np.array([
            i*(2*R) + j*R,
            j*(S+T)
        ])
        cp = np.multiply(cp, SCALE)
    
    return cp
    

    【讨论】:

      【解决方案6】:

      我不知道它是否会帮助任何人,但我想出了一个更简单的解决方案。当我创建我的六边形时,我只是给他们一个中间点,然后用鼠标坐标找到最近的中间点,我可以找到一个我在上面!

      【讨论】:

      • 或许你可以举个例子。
      • 你怎么知道哪个是最接近的 4 个六边形来测试你的鼠标点?
      【解决方案7】:

      我找到了一种不同的方法来查看鼠标是否在六边形中。使用一点三角函数,您可以找到鼠标和六边形中心之间的线的角度,使用这个角度您可以计算出从六边形中心到六边形边缘的线有多长角度。然后只需检查鼠标之间的线长度是否小于六边形边缘的预期长度。如果有人想要示例代码,我可以分享。

      【讨论】:

      • 那么如何选择六边形来进行初始三角计算?或者您是否遍历每个六边形并检查直到找到正确的?当您检查线长时,您是否将六边形近似为一个圆圈?如果不是,我会对计算给定角度的六边形“radius”的代码非常感兴趣!
      【解决方案8】:

      我知道这已经很晚了,但我目前正在使用六边形网格并试图找到解决这个问题的方法。繁重的数学方法对我来说似乎有点矫枉过正,但我​​明白它们为什么以及如何工作。几乎是偶然的,我找到了一个超级简单的解决方案,只需几行代码即可完成。

      在我的示例中,我有一个自定义的 Hexagon 类,其中包含一个成员 Point 变量,该变量存储六边形中心的 (x, y)。然后我根据这个中心值计算并绘制六边形。

      每个 Hexagon 类还附加到一个 Tile 类,该类存储一行和 col 变量(在绘制网格时给出)。

      需要的变量: - 半径 - 网格行 - 网格颜色 - 六边形中心点 - 鼠标点击点(或其他给定点) - 瓷砖/六边形列表

      我的鼠标监听器:

      addMouseListener(new MouseAdapter() {
              @Override
              public void mouseClicked(MouseEvent e) {
                  super.mouseClicked(e);
                  System.out.println("Mouse Click Registered");
                  double closestDistance = Double.MAX_VALUE;
                  int closestIndex = -1;
                  for (int i = 0; i < tiles.size(); i++) {
                      double distance = tiles.get(i).getDistance(new myPoint(e.getX(), e.getY()));
                      if (distance < closestDistance) {
                          closestDistance = distance;
                          if (closestDistance <= radius) {
                              closestIndex = i;
                          }
                      }
                  }
                  if (closestIndex > -1) {
                      Tile t = tiles.get(closestIndex);
                      System.out.println("Selected tile: " + t.getCol() + ", " + t.getRow());
                  }
              }
          });
      

      我从 Tile 类执行的计算:

      public double getDistance(myPoint p) {
          myPoint center = this.hexagon.getCenter();
          double xd = center.x - p.x;
          double yd = center.y - p.y;
          return Math.abs(Math.sqrt((xd * xd) + (yd * yd)));
      }
      

      这是做什么的。遍历地图上的六边形列表,计算指定点到六边形中心点的距离的绝对值。如果距离小于先前计算的距离,则将该值设置为最小值。如果该数字小于半径,则将最接近索引设置为该索引#。一直持续到图块循环结束。

      循环后,验证值索引是否已保存,如果是,则选择该索引。

      注意:这可能会通过从指定点计算行/列来进一步优化。有了这些信息,您可以限制循环到发出该点的瓷砖的数量。

      【讨论】:

      • 感谢您花时间回答,如果您查看我的回答,您会发现它只是“找到行和列,然后做一些额外的检查”而不是“高数学”!您的方法非常繁琐,适用于少量的十六进制和不频繁的检查,但是对于数以万计的十六进制和每次鼠标移动检查来说,它有点太重了。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多