Hex Grid 的坐标是怎么工作的?
从 War Chest 的棋盘出发,用 cube coordinates 的三维切面理解 axial coordinates、六个邻居以及 hex distance。
<!-- vault refs: degree of freedom vector space game tree web 可视化技术栈梳理 -->
我在 Knowledge Base 里实现 War Chest 时,棋盘上的一个位置被写成:
[q r]例如 [0 0]、[1 -1]、[-2 3]。
这看起来和普通的 [x y] 差不多。但一旦继续追问,就会遇到一串不那么直观的问题:
- q 和 r 分别指向哪里?
- 一个格子的六个邻居如何计算?
- 为什么介绍 axial coordinates 时,经常又冒出第三个坐标 s?
- 为什么三个坐标满足 q + r + s = 0?
- 明明棋盘是二维的,为什么要把它放进三维空间理解?
这篇文章从这些问题出发,建立一套能够直接用于 War Chest 规则的 hex grid 坐标模型。
Hex grid 的困难不是画六边形
方格棋盘天然适合 row/column 或 x/y coordinate。一个位置 [x y] 的四个邻居可以写成:
[x+1 y]
[x-1 y]
[x y+1]
[x y-1]Hex grid 有六个邻居。如果把它强行塞进二维数组,相邻位置会依赖当前行或列的奇偶性。以 offset coordinates 为例,同样是“右上”,在奇数行和偶数行可能要使用不同公式。
这并不是不能做,但它把棋盘的几何关系变成了奇偶分支。移动、攻击范围、距离和寻路都会反复遇到这种特殊情况。
War Chest 因此没有把棋盘保存为 row-major matrix,而是保存为一组合法的 axial coordinates:
{:board/cells #{[0 -3] [1 -3] [2 -3] ...}
:board/controls #{[3 -2] [1 -1] ...}
:board/home {:red #{[1 2] [-2 3]}
:blue #{[2 -3] [-1 -2]}}}棋盘可以是不规则形状。判断一个位置是否属于棋盘,只需检查它是否在 :board/cells 集合中。
先不要把 q 和 r 当成 x 和 y
War Chest 使用 flat-top hex。当前 SVG 投影是:
x=1.5qy=\sqrt{3}\left(r+\frac{q}{2}\right)因此固定 q 时,x 不变:相同 q 的格子在画面中组成一条垂直列。但这不意味着 +q 是垂直方向。
从 [0 0] 变成 [1 0] 时,x 和 y 都会增加,所以它在屏幕上向右下移动。真正垂直向下的是 [0 0] → [0 1],也就是 +r。
当前投影下,六个方向名、相邻变化和屏幕方向是:
这里有一个重要分层:
[q r]描述格子之间的逻辑关系;[x y]描述它最后画在屏幕上的位置。
改变六边形大小、棋盘旋转角度或 SVG viewBox,不应该改变移动和攻击规则。
为什么二维棋盘会出现三个坐标?
Hex grid 有三组彼此对称的网格方向。只用两个轴存储最简洁,但三个轴能让六个方向拥有完全对称的表达。
我们先允许 q、r、s 独立取整数。这样得到的不是 hex grid,而是一个三维整数点阵。然后只保留满足下面约束的点:
q+r+s=0这些点位于三维空间中的一张二维平面上。
q + r + s = 0 不是六边形从外部强加给我们的自然定律,而是一种对三个方向一视同仁的坐标约定。系数都取 1,让三条轴保持对称;常数取 0,是因为我们把原点定义成 [0 0 0]。q + r + s = c 也会得到一张平行的二维平面,但只有 c = 0 穿过我们选择的原点。
下面的交互图分四步展示这个过程。左侧 Three.js 场景可以拖动旋转;右侧同时显示平面投影和当前坐标。
{:title "把三维整数空间切成 Hex Grid"
:steps [{:mode :space
:title "三个独立坐标"
:body "q、r、s 都能独立变化时,整数点填满三维空间。此时还没有 hex grid。"}
{:mode :plane
:title "只保留 q + r + s = 0"
:body "约束去掉一个自由度:三个数字仍然存在,但只有两个能够独立选择。剩下的整数点组成二维 hex grid。"}
{:mode :illegal
:title "只让 q + 1"
:body "从 [0 0 0] 到 [1 0 0] 后,坐标和变成 1。红点离开约束平面,因此它不是一个合法 hex coordinate。"}
{:mode :neighbor
:title "让 s - 1 进行补偿"
:body "[1 0 -1] 的坐标和重新变成 0。它位于平面上,并且是距离原点最近的六个合法整数点之一。"}]}所谓“三维空间中的二维平面”,并不是说 War Chest 是一个三维游戏。它表达的是自由度:
- 三个独立数字有三个自由度,会填满三维空间;
- 加上 q + r + s = 0 后,任意两个坐标都会决定第三个;
- 因此只剩两个自由度,所有合法点都落在一张二维平面上。
这和在三维坐标中用 z = 0 表示普通二维平面类似。区别只是这里的平面是倾斜的:q + r + s = 0。
为什么移动到邻居时是“一加一减”?
从一个合法坐标移动到另一个合法坐标,移动前后都必须满足:
q+r+s=0把移动量写成 Δq、Δr、Δs,就能得到:
\Delta q+\Delta r+\Delta s=0如果只让 q + 1:
[0 0 0] → [1 0 0]变化量是 [1 0 0],它们的和是 1。新点离开了平面。
为了继续留在平面上,另外两个坐标必须提供 -1 的补偿。最小的整数补偿方式,是其中一个坐标 -1,另一个保持不变:
[0 0 0] → [1 -1 0]
[0 0 0] → [1 0 -1]把三个坐标轮流选择,就得到六种最小非零变化:
[+1 -1 0]
[+1 0 -1]
[ 0 +1 -1]
[-1 +1 0]
[-1 0 +1]
[ 0 -1 +1]这六个点正好是一个格子的六个邻居。
这里的“必须一加一减”需要说得严谨一些。只要变化量总和为 0,移动就仍然位于平面上,例如:
[0 0 0] → [2 -1 -1]也是合法 cube coordinate。但它不是相邻格。只有当目标是最近的另一个合法整数点时,变化才必然是一个 +1、一个 -1、一个 0。
所以六个邻居来自三个条件的交集:
1. 目标仍在 q + r + s = 0 平面上;
2. 三个坐标仍然是整数;
3. 目标是最近的非原点格点。
Axial coordinates 只是省略了可以推导的 s
由于:
s=-q-r存储时没有必要保存第三个数字。War Chest 只保存 [q r],需要计算距离时再恢复 s:
(defn axial-s [[q r]]
(- (+ q r)))例如:
Axial: [-2 3]
Cube: [-2 3 -1]因为:
-2+3-1=0因此 axial coordinates 和 cube coordinates 不是两套互不相关的系统。更准确的理解是:
Axial coordinates 是 cube coordinates 去掉冗余的第三个坐标后的存储形式。
规则层使用简洁的 [q r],推导距离和直线关系时仍然可以借用三个轴的对称性。
Hex distance 从哪里来?
一次相邻移动会让两个 cube coordinate 各变化 1,第三个不变。例如:
[0 0 0] → [1 0 -1]三个绝对变化量之和是 2,但只走了一步。因此两个位置之间的距离是:
d(a,b)=\frac{|\Delta q|+|\Delta r|+|\Delta s|}{2}War Chest 当前实现直接使用这个公式:
(defn axial-distance
[[q1 r1 :as a] [q2 r2 :as b]]
(let [dq (- q1 q2)
dr (- r1 r2)
ds (- (axial-s a) (axial-s b))]
(/ (+ (abs-num dq)
(abs-num dr)
(abs-num ds))
2)))这使游戏规则可以直接使用几何语言:
- 普通单位移动到 distance 1;
- Archer 攻击 exact distance 2;
- Light Cavalry 移动到 distance 1 或 2;
- Marshal 选择 distance 2 的友方单位。
不需要判断当前位于奇数行还是偶数行。
从坐标模型回到 War Chest
现在可以把整个数据流分成三层:
Cube coordinate [q r s]
用于理解三个对称方向与距离
↓ 省略 s = -q-r
Axial coordinate [q r]
用于规则、棋盘集合、棋子位置和存档
↓ axial->point
Screen point {x y}
用于 SVG polygon、文字和点击区域三层分别回答不同问题:
- Cube:为什么 hex grid 有六个对称邻居?
- Axial:游戏状态如何简洁、稳定地保存位置?
- Screen:这些位置最后画在哪里?
这也是为什么在 War Chest 中,UI 只在渲染边界把 [q r] 转成 SVG point。棋盘规则不应该知道像素,也不应该因为旋转了棋盘就改变移动结果。
下一步可以沿着同一个模型继续推导两件事:如何判断多个 hex 是否位于同一直线上,以及如何枚举距离中心不超过 n 的范围。这两件事会直接进入 Crossbow、Lancer、Archer 等单位的规则。