六邊形相關的數學知識

本文翻譯自Ruslan的博客,感謝分享相關知識。

均勻的六角形網格是計算機游戲和圖形應用程序中的重復模式。

有些操作可能需要在使用六邊形格子時實現:

  1. 通過其在網格中的索引找到六角形的位置;

  2. 用鼠標挑選一個六角形;

  3. 尋找相鄰的格子;

  4. 查找六角形的中心坐標等。

盡管這些東西看起來并不困難(有點像小學水平的幾何/算術運算法則 ),但它并不像矩形網格那樣簡單。

嘗試咨詢互聯網后,我在CodeProject上找到了一篇整潔的文章,該文章恰好解決了所提到的問題。它的評分很高,我想這確實是需要的東西。

但是,令我感到困惑的是,提出的解決方案(和代碼)似乎比人們預期的要復雜:

  1. 六邊形的位置通過迭代尋找,根據先前計算的位置迭代找到六邊形位置;

  2. 在此過程中需要處理了一些極端/邊界情況;

  3. 每個六邊形都存儲了大量狀態;

  4. 對于替代的六邊形定向模式(又稱“尖頭”定向),有一個單獨的代碼路徑,它與主模式相交;

  5. 要用鼠標選擇一個六邊形,代碼將在六邊形數組上進行迭代,并使用存儲的角點坐標對每個六邊形進行通用的“多邊形點”測試。

我們可以做得更簡單嗎? 根據六角格特性,讓我們檢查六角形的幾何特性并定義一些常數:

1.png

我們通過其半徑R定義六角形,并根據其半徑找到其他一些參數,例如W(“寬度”),S(“側面”)和H(“高度”)。

現在,讓我們看一下六角形網格本身:

2.png

通過數組索引查找六邊形位置

從該圖片中,可以得出通過其數組索引(i,j)查找六邊形單元的左上角位置的公式:

3.png

考慮到對于奇數列i%2(從i除以2所得的值)等于1,對于偶數列等于0,我們可以重寫它:


4.png

通過一個點找到六邊形索引

另一個操作是從屏幕上的點坐標中找到六邊形數組坐標,該坐標用于鼠標點擊。

根據觀察,六角形網格可以完全被一組矩形塊(寬度S,高度H)所覆蓋,在上圖中,它們被繪制成帶有紫色三角形的綠色矩形。每個瓦片有三個六邊形重疊。

找到我們的選擇點進入的那些六邊形并不太難:


5.png

在這里 (it,jt)被點選中的矩形圖形的列/行。這些花哨的括號是a 向下取整

從矩陣的行列轉換成六邊形的行列:

6.png

做完這些之后,現在我們可以找出三個可能的六邊形中的哪一個與我們的點相對應。為此,我們放大矩形單元格的坐標系統,并構建分離線(圖中的粗紅線)的繪圖。它的功能是 xt = R | 0.5-yt/H|,對于直線以上的所有點(它們都在綠色區域內),我們得到 xt > R |0.5t/ H|

對于矩形內的所有其他點,我們必須選擇左邊的上六邊形或下六邊形。這是根據它的值來決定的 yt

一般來說,我們必須注意奇數/偶數行索引的差異(因為六邊形數組中的行是鋸齒形的)。

考慮以上因素,選中的六邊形的最終數組下標是:

7.png

“尖”網格方向

到目前為止,我們只考慮了“平躺”網格方向(即六邊形“躺”在它們的兩側)。

如果我們想要有另一個方向,當六邊形的角向上時,我們應該重寫所有的公式嗎?

關于“尖”方向的一個簡單觀察是,你可以通過圍繞對角軸鏡像“平”方向來得到它。

這樣做的直接結果是,“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程序。

這是一個六邊形版本的游戲,叫做 “燈”(我借用了這個概念 在這里)。

游戲開始時,所有的“燈”都是亮著的(所有的六邊形都是黃色的),目標是關掉所有的燈(這樣所有的六邊形都變成灰色)。

8.png

每當用戶點擊一個六邊形時,它就會切換它的燈光,以及所有鄰近的六邊形。

<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上可用

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 雪花的形狀涉及到水在大氣中的結晶過程。大氣中的水分子在冷卻到冰點以下時,就開始凝華,而形成水的晶體,即冰晶。冰晶屬...
    蔓珠莎華_05e6閱讀 4,334評論 0 6
  • “六角形架構”已經存在很長時間了,的確相當長的時間了,這個玩意兒從主流架構中消失了很久,直到最近才慢慢的才回到大眾...
    water_lang閱讀 1,269評論 0 1
  • 六邊形地圖相較四方地圖的優勢:只有6個鄰居而且每個鄰居到中心的距離都是一樣的。而四方地圖有8個鄰居包含2種情況,一...
    Foo_d488閱讀 7,377評論 0 4
  • 這是一個正六邊形的棋盤,里面的格子都是由正三角形組成的棋子也是由正三角形組成的,棋子要落在棋盤上,就要知道棋子的位...
    瘦的時候的周超然閱讀 1,871評論 0 2
  • 給定地圖上的區域(用多邊形頂點的經緯度表示), 需要用正多邊形(三角形/正方形/六邊形)對地圖上的區域進行填充. ...
    胡拉哥閱讀 2,865評論 0 2