本文翻譯自Ruslan的博客,感謝分享相關知識。
均勻的六角形網格是計算機游戲和圖形應用程序中的重復模式。
有些操作可能需要在使用六邊形格子時實現:
通過其在網格中的索引找到六角形的位置;
用鼠標挑選一個六角形;
尋找相鄰的格子;
查找六角形的中心坐標等。
盡管這些東西看起來并不困難(有點像小學水平的幾何/算術運算法則 ),但它并不像矩形網格那樣簡單。
嘗試咨詢互聯網后,我在CodeProject上找到了一篇整潔的文章,該文章恰好解決了所提到的問題。它的評分很高,我想這確實是需要的東西。
但是,令我感到困惑的是,提出的解決方案(和代碼)似乎比人們預期的要復雜:
六邊形的位置通過迭代尋找,根據先前計算的位置迭代找到六邊形位置;
在此過程中需要處理了一些極端/邊界情況;
每個六邊形都存儲了大量狀態;
對于替代的六邊形定向模式(又稱“尖頭”定向),有一個單獨的代碼路徑,它與主模式相交;
要用鼠標選擇一個六邊形,代碼將在六邊形數組上進行迭代,并使用存儲的角點坐標對每個六邊形進行通用的“多邊形點”測試。
我們可以做得更簡單嗎? 根據六角格特性,讓我們檢查六角形的幾何特性并定義一些常數:
我們通過其半徑R定義六角形,并根據其半徑找到其他一些參數,例如W(“寬度”),S(“側面”)和H(“高度”)。
現在,讓我們看一下六角形網格本身:
通過數組索引查找六邊形位置
從該圖片中,可以得出通過其數組索引(i,j)查找六邊形單元的左上角位置的公式:
考慮到對于奇數列i%2(從i除以2所得的值)等于1,對于偶數列等于0,我們可以重寫它:
通過一個點找到六邊形索引
另一個操作是從屏幕上的點坐標中找到六邊形數組坐標,該坐標用于鼠標點擊。
根據觀察,六角形網格可以完全被一組矩形塊(寬度S,高度H)所覆蓋,在上圖中,它們被繪制成帶有紫色三角形的綠色矩形。每個瓦片有三個六邊形重疊。
找到我們的選擇點進入的那些六邊形并不太難:
在這里 (it,jt)被點選中的矩形圖形的列/行。這些花哨的括號是a 向下取整。
從矩陣的行列轉換成六邊形的行列:
做完這些之后,現在我們可以找出三個可能的六邊形中的哪一個與我們的點相對應。為此,我們放大矩形單元格的坐標系統,并構建分離線(圖中的粗紅線)的繪圖。它的功能是 xt = R | 0.5-yt/H|,對于直線以上的所有點(它們都在綠色區域內),我們得到 xt > R |0.5t/ H|。
對于矩形內的所有其他點,我們必須選擇左邊的上六邊形或下六邊形。這是根據它的值來決定的 yt。
一般來說,我們必須注意奇數/偶數行索引的差異(因為六邊形數組中的行是鋸齒形的)。
考慮以上因素,選中的六邊形的最終數組下標是:
“尖”網格方向
到目前為止,我們只考慮了“平躺”網格方向(即六邊形“躺”在它們的兩側)。
如果我們想要有另一個方向,當六邊形的角向上時,我們應該重寫所有的公式嗎?
關于“尖”方向的一個簡單觀察是,你可以通過圍繞對角軸鏡像“平”方向來得到它。
這樣做的直接結果是,“pointy”情況下的所有計算都可以通過以下方式執行:
交換輸入坐標(即x<->y或i<->j;
應用上述公式;
交換輸出坐標回來(“反鏡像”它們):i<->j, x<->y。
這個操作在代碼中是非常簡單的(而不是為每種情況都有一個單獨的代碼路徑),我把它留給讀者作為練習。
代碼
下面是一個Java代碼示例,它實現了公式:
<pre mdtype="fences" cid="n187" lang="" class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">?
package com.rush;
?
/**
* Uniform hexagonal grid cell's metrics utility class.
*/
public class HexGridCell {
private static final int[] NEIGHBORS_DI = { 0, 1, 1, 0, -1, -1 };
private static final int[][] NEIGHBORS_DJ = {
{ -1, -1, 0, 1, 0, -1 }, { -1, 0, 1, 1, 1, 0 } };
?
private final int[] CORNERS_DX; // array of horizontal offsets of the cell's corners
private final int[] CORNERS_DY; // array of vertical offsets of the cell's corners
private final int SIDE;
?
private int mX = 0; // cell's left coordinate
private int mY = 0; // cell's top coordinate
?
private int mI = 0; // cell's horizontal grid coordinate
private int mJ = 0; // cell's vertical grid coordinate
?
/**
* Cell radius (distance from center to one of the corners)
*/
public final int RADIUS;
/**
* Cell height
*/
public final int HEIGHT;
/**
* Cell width
*/
public final int WIDTH;
?
public static final int NUM_NEIGHBORS = 6;
?
/**
* @param radius Cell radius (distance from the center to one of the corners)
*/
public HexGridCell(int radius) {
RADIUS = radius;
WIDTH = radius * 2;
HEIGHT = (int) (((float) radius) * Math.sqrt(3));
SIDE = radius * 3 / 2;
?
int cdx[] = { RADIUS / 2, SIDE, WIDTH, SIDE, RADIUS / 2, 0 };
CORNERS_DX = cdx;
int cdy[] = { 0, 0, HEIGHT / 2, HEIGHT, HEIGHT, HEIGHT / 2 };
CORNERS_DY = cdy;
}
?
/**
* @return X coordinate of the cell's top left corner.
*/
public int getLeft() {
return mX;
}
?
/**
* @return Y coordinate of the cell's top left corner.
*/
public int getTop() {
return mY;
}
?
/**
* @return X coordinate of the cell's center
*/
public int getCenterX() {
return mX + RADIUS;
}
?
/**
* @return Y coordinate of the cell's center
*/
public int getCenterY() {
return mY + HEIGHT / 2;
}
?
/**
* @return Horizontal grid coordinate for the cell.
*/
public int getIndexI() {
return mI;
}
?
/**
* @return Vertical grid coordinate for the cell.
*/
public int getIndexJ() {
return mJ;
}
?
/**
* @return Horizontal grid coordinate for the given neighbor.
*/
public int getNeighborI(int neighborIdx) {
return mI + NEIGHBORS_DI[neighborIdx];
}
?
/**
* @return Vertical grid coordinate for the given neighbor.
*/
public int getNeighborJ(int neighborIdx) {
return mJ + NEIGHBORS_DJ[mI % 2][neighborIdx];
}
?
/**
* Computes X and Y coordinates for all of the cell's 6 corners, clockwise,
* starting from the top left.
*
* @param cornersX Array to fill in with X coordinates of the cell's corners
* @param cornersX Array to fill in with Y coordinates of the cell's corners
*/
public void computeCorners(int[] cornersX, int[] cornersY) {
for (int k = 0; k < NUM_NEIGHBORS; k++) {
cornersX[k] = mX + CORNERS_DX[k];
cornersY[k] = mY + CORNERS_DY[k];
}
}
?
/**
* Sets the cell's horizontal and vertical grid coordinates.
*/
public void setCellIndex(int i, int j) {
mI = i;
mJ = j;
mX = i * SIDE;
mY = HEIGHT * (2 * j + (i % 2)) / 2;
}
/**
* Sets the cell as corresponding to some point inside it (can be used for
* e.g. mouse picking).
*/
public void setCellByPoint(int x, int y) {
int ci = (int)Math.floor((float)x/(float)SIDE);
int cx = x - SIDE*ci;
?
int ty = y - (ci % 2) * HEIGHT / 2;
int cj = (int)Math.floor((float)ty/(float)HEIGHT);
int cy = ty - HEIGHT*cj;
?
if (cx > Math.abs(RADIUS / 2 - RADIUS * cy / HEIGHT)) {
setCellIndex(ci, cj);
} else {
setCellIndex(ci - 1, cj + (ci % 2) - ((cy < HEIGHT / 2) ? 1 : 0));
}
}
}</pre>
測試代碼
為了測試代碼,我編寫了一個小型Java applet程序。
這是一個六邊形版本的游戲,叫做 “燈”(我借用了這個概念 在這里)。
游戲開始時,所有的“燈”都是亮著的(所有的六邊形都是黃色的),目標是關掉所有的燈(這樣所有的六邊形都變成灰色)。
每當用戶點擊一個六邊形時,它就會切換它的燈光,以及所有鄰近的六邊形。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n202" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.rush;
?```
import java.applet.Applet;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
?
import javax.swing.JFrame;
import javax.swing.JOptionPane;
?
/**
* Example applet which uses hexagonal grid. It's a hexagonal version of the
* "lights out" puzzle game: http://en.wikipedia.org/wiki/Lights_Out_(game)
*/
public class HexLightsOut extends Applet implements MouseListener {
private static final long serialVersionUID = 1L;
?
private static final int BOARD_WIDTH = 5;
private static final int BOARD_HEIGHT = 4;
?
private static final int L_ON = 1;
private static final int L_OFF = 2;
?
private static final int NUM_HEX_CORNERS = 6;
private static final int CELL_RADIUS = 40;
?
// game board cells array
private int[][] mCells = { { 0, L_ON, L_ON, L_ON, 0 },
{ L_ON, L_ON, L_ON, L_ON, L_ON },
{ L_ON, L_ON, L_ON, L_ON, L_ON },
{ 0, 0, L_ON, 0, 0 } };
?
private int[] mCornersX = new int[NUM_HEX_CORNERS];
private int[] mCornersY = new int[NUM_HEX_CORNERS];
?
private static HexGridCell mCellMetrics = new HexGridCell(CELL_RADIUS);
?
@Override
public void init() {
addMouseListener(this);
}
?
@Override
public void paint(Graphics g) {
for (int j = 0; j < BOARD_HEIGHT; j++) {
for (int i = 0; i < BOARD_WIDTH; i++) {
mCellMetrics.setCellIndex(i, j);
if (mCells[j][i] != 0) {
mCellMetrics.computeCorners(mCornersX, mCornersY);
?
g.setColor((mCells[j][i] == L_ON) ? Color.ORANGE : Color.GRAY);
g.fillPolygon(mCornersX, mCornersY, NUM_HEX_CORNERS);
g.setColor(Color.BLACK);
g.drawPolygon(mCornersX, mCornersY, NUM_HEX_CORNERS);
}
}
}
}
?
@Override
public void update(Graphics g) {
paint(g);
}
?
/**
* Returns true if the cell is inside the game board.
*
* @param i cell's horizontal index
* @param j cell's vertical index
*/
private boolean isInsideBoard(int i, int j) {
return i >= 0 && i < BOARD_WIDTH && j >= 0 && j < BOARD_HEIGHT
&& mCells[j][i] != 0;
}
?
/**
* Toggles the cell's light ON<->OFF.
*/
private void toggleCell(int i, int j) {
mCells[j][i] = (mCells[j][i] == L_ON) ? L_OFF : L_ON;
}
?
/**
* Returns true if all lights have been switched off.
*/
private boolean isWinCondition() {
for (int j = 0; j < BOARD_HEIGHT; j++) {
for (int i = 0; i < BOARD_WIDTH; i++) {
if (mCells[j][i] == L_ON) {
return false;
}
}
}
return true;
}
?
/**
* Resets the game to the initial position (all lights are on).
*/
private void resetGame() {
for (int j = 0; j < BOARD_HEIGHT; j++) {
for (int i = 0; i < BOARD_WIDTH; i++) {
if (mCells[j][i] == L_OFF) {
mCells[j][i] = L_ON;
}
}
}
}
?
@Override
public void mouseReleased(MouseEvent arg0) {
mCellMetrics.setCellByPoint(arg0.getX(), arg0.getY());
int clickI = mCellMetrics.getIndexI();
int clickJ = mCellMetrics.getIndexJ();
?
if (isInsideBoard(clickI, clickJ)) {
// toggle the clicked cell together with the neighbors
toggleCell(clickI, clickJ);
for (int k = 0; k < 6; k++) {
int nI = mCellMetrics.getNeighborI(k);
int nJ = mCellMetrics.getNeighborJ(k);
if (isInsideBoard(nI, nJ)) {
toggleCell(nI, nJ);
}
}
}
repaint();
?
if (isWinCondition()) {
JOptionPane.showMessageDialog(new JFrame(), "Well done!");
resetGame();
repaint();
}
}
?
@Override
public void mouseClicked(MouseEvent arg0) {
}
?
@Override
public void mouseEntered(MouseEvent arg0) {
}
?
@Override
public void mouseExited(MouseEvent arg0) {
}
?
@Override
public void mousePressed(MouseEvent arg0) {
}
}</pre>
來源 github上可用。