36

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

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

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

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

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

// 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

4

9 回答 9

70

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

带有重叠方形网格的六边形网格

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

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

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

于 2011-10-10T14:20:18.747 回答
10

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

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

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

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

在此处输入图像描述

因此,第一步是在 UV 空间中表达您的点 (x,y),即 (x,y) = lambda U + mu V, 所以 = (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% 的时间进行两次距离检查(我猜通过查看每个检查的区域来计算数字):

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) 函数可以进一步整理。

于 2014-04-29T16:33:43.023 回答
6

我首先查看@pi 的答案https://stackoverflow.com/a/23370350/5776618并认为尝试在具有 UVW 空间的立方体坐标中进行类似的操作会很有趣(而不是 2D、轴向、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};

}
于 2016-05-13T09:02:13.807 回答
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) 下方。

于 2014-10-14T20:17:01.753 回答
3

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

然而,Sebastian 的方法似乎切入正题,用极少的几行代码完成了任务。

如果您阅读评论部分,您会发现有人在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
于 2014-05-01T21:44:13.507 回答
3

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

于 2015-06-17T19:57:13.430 回答
0

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

于 2020-01-05T06:15:04.843 回答
0

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

在我的示例中,我有一个自定义 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)));
}

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

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

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

于 2020-04-10T07:34:12.980 回答
0

这与其他答案类似,但我认为实现更简洁。它主要基于 Amit 的指南。

请注意,东北角确实给出了一个错误的结果,就像 P i 所描述的那样。

我使用立方体坐标。秘密的一部分是cube-round,它采用浮点结果并四舍五入到最接近的十六进制。

我发现使用矩阵更容易实现这些事情。首先,我们乘以一个倾斜和比例矩阵,得到浮动的轴向十六进制坐标,然后我们向下四舍五入找到实际的十六进制。size对应于单元半径。

这是在Parenscript中:

    (defmacro cube-round (coord)
      ;; round cube coordinates
      `(let* ((x (@ ,coord 0))
              (y (@ ,coord 1))
              (z (@ ,coord 2))
              ;; rounded components - used in calculations
              (rx (round x))
              (ry (round y))
              (rz (round z))
              ;; get the differential of each component
              (diffx (abs (- rx x)))
              (diffy (abs (- ry y)))
              (diffz (abs (- rz z))))
         ;; at this point coordinates might not add up to 1 (which is required by cube coordinates). Find the component that changed the most, and reset it to -1 * (ra + rb).
         (if (> diffx diffy diffz)
             ;; x was largest - reset it
             (setf rx (* -1 (+ ry rz)))
             (if (> diffy diffz)
                 ;; y was largest
                 (setf ry (* -1 (+ rx rz)))
                 ;; z was largest
                 (setf rz (* -1 (+ rx ry)))))
         ;; return final vector
         (make-vec3 (list rx ry rz))))

    (defmacro pixel-to-cube (coord size)
      (let ((sqrt3 (sqrt 3.0)))
        `(let* ((c ,coord)
                ;; skew+scale matrix for mapping pixel to axial coordinates [[sqrt(3)/3/size, -1/3/size], [0, 2/3/size]]
                (m (make-mat2 (list
                               (/ (/ ,sqrt3 3.0) ,size) (/ (/ -1 3.0) ,size)
                               0 (/ (/ 2 3.0) ,size))))
                (axial-coords (vec2-mat-mul m c))
                (q (@ axial-coords 0))
                (r (@ axial-coords 1))
                ;; make cube float coordinates from axial - make z = -1 * (x + y)
                (cube-float (make-vec3-float
                             (list q r (* -1 (+ q r))))))
           ;; finally, round coordinates to snap to a cell
           (cube-round cube-float))))
于 2022-01-30T22:07:59.183 回答