最近做俯视角策略类原型的时候我在“地图走格子”这一步卡了整整一个晚上。正方形格子本身不难画难的是移动范围、攻击范围、寻路距离这些玩法逻辑一旦叠上去斜角距离的问题就会不断冒出来。后来把心一横把地图换成了六边形网格结果发现只要坐标系一次理顺代码量反而更小。这篇就把我整理出的一套Unity可直接运行实现完整放出来包含网格生成、坐标转换、邻居查找、鼠标点选和寻路距离计算每个公式我都会讲清楚它到底在干什么不是贴一段能跑就完事。文章内容适合已经熟悉Unity基础操作、想快速上手六边形网格的开发者。为了让读者少走弯路我会把坐标系的坑单独拎出来讲因为六边形网格几乎所有令人头疼的问题都出在坐标系混用上。整体走完你手里会有一份能直接扩展成战棋、模拟经营或SLG底层的网格框架。1. 为什么战棋和策略游戏偏爱六边形从“间距一致”这件事说起1.1 正方形网格的距离陷阱正方形网格看起来是最自然的方案但真正做玩法时它的几何特性非常别扭。一个格子有四个直线方向邻居还有四个斜角方向邻居。直线邻居的距离是1个边长斜角邻居的距离是根号2个边长约等于1.414。这带来什么问题当你打算让角色每回合移动2格时沿着边线走能到2格远沿着对角线走却到了1.414格远的地方。移动范围画出来不是圆而是个变形的菱形玩家一眼就能看出“斜着走好像比横着走更划算”但又说不清哪里不对劲。正方形网格的另一个麻烦是“贴角”问题。两个斜对角格子之间只共享一个顶点不共享边。真实世界里一个单位贴着墙角走到另一个格子通常需要绕过那面墙但如果你的寻路算法把斜对角也当成可通行路径单位就会鬼畜地穿墙角。折中方案是禁掉斜向移动但这又会让斜线方向的地图看起来非常割裂。1.2 六边形网格到底解决了什么六边形网格没有对角线问题它只有六个方向所有相邻格子的中心距离完全一致移动范围在视觉上是一个更接近圆形的六边形。战略游戏里“移动范围”“攻击范围”“影响力范围”这类玩法用六边形网格计算时不会出现任何距离奇点玩家能非常直觉地判断“我能走到哪里”。同时六边形只有边相邻、没有角相邻不存在穿墙角争议。从数学层面看六边形网格相当于把平面空间以最紧凑的方式铺满每个格子与六个邻居共享完整边线整个地图没有孤立的接触点。这也是为什么大量经典战棋和策略游戏最终都选了六边形比如《文明》系列后期作品和不少独立策略游戏核心原因就是六向对称在玩法判定上省掉了大量特例。我给个直观对比表维度正方形网格含斜角正方形网格禁斜角六边形网格邻居数量846特征距离是否一致不一致1 vs 1.414一致一致角点接触问题存在存在视觉上仍在不存在移动范围形状变形菱形十字六边形接近圆玩法规则复杂度高常有特例中低规则统一1.3 代价与准备六边形网格的代价是坐标系统比正方形复杂。正方形格子用二维数组就能搞定六边形则至少需要理解三种坐标偏移坐标、轴向坐标、立方体坐标。很多教程一上来就贴一堆公式读者照抄完还是糊里糊涂就是因为没先搞清楚这三种坐标各自解决什么问题。在动手写代码前还需要确定一个基础参数六边形是尖顶朝上还是平顶朝上。这会影响所有公式和邻居表。本文统一使用尖顶六边形pointy-top orientation也就是六边形有一个顶点朝上、一条边朝下的形态。2. 坐标系必须一次理顺偏移坐标、轴向坐标、立方体坐标2.1 偏移坐标人类最容易理解的存储方式偏移坐标就是我们平时说的“列和行”用(col, row)表示直接对应二维数组下标。六边形网格和正方形网格在存储上的区别在于奇数行需要整体向右错开半格这样六边形才能互相咬合。这个错开方式分两种一种是偶数行右移even-r一种是奇数行右移odd-r。本文用odd-r也就是奇数行右移半格// odd-r偏置奇数行右移半格 // 网格中共有 gridWidth 列gridHeight 行 for (int row 0; row gridHeight; row) { for (int col 0; col gridWidth; col) { // 这里 col 是列row 是行 // 创建格子数据 } }偏移坐标的好处是存储直观格子数据放在数组里序列化、存档、从服务端同步都非常方便。它的坏处是计算邻居时很别扭同一方向的邻居在奇数行和偶数行的偏移量不一样。如果只用偏移坐标硬算邻居代码里会出现大量if (row % 2 0)分支非常容易写错。2.2 轴向坐标算法真正喜欢的形式轴向坐标把二维的六边形网格表示成(q, r)两个值其中q沿一个轴方向递增r沿另一个轴方向递增。它与偏移坐标最大的区别是一个六边形的六个邻居在轴向坐标下的方向是固定的不随格子所在行数变化。轴向坐标和偏移坐标之间的转换非常规律。odd-r偏置下偏移转轴向q col - (row - (row 1)) / 2r row轴向转偏移col q (row - (row 1)) / 2row r注意这里(row 1)在C#中是对正偶数取0、对正奇数取1的位运算。用位运算而不是取模是因为位运算在热循环中稍快而且语义在这里更清晰它直接判断行号奇偶。我给这段转换写一个辅助函数/// summary偏移坐标转轴向坐标odd-r偏置/summary public static Vector2Int OffsetToAxial(int col, int row) { int q col - (row - (row 1)) / 2; int r row; return new Vector2Int(q, r); } /// summary轴向坐标转偏移坐标odd-r偏置/summary public static Vector2Int AxialToOffset(int q, int r) { int col q (r - (r 1)) / 2; int row r; return new Vector2Int(col, row); }2.3 立方体坐标取整问题的终极答案立方体坐标是六边形网格最早被发现好用的坐标系它用三个分量表示一个格子(q, r, s)其中s -q - r。也就是说所有合法的六边形坐标点都落在三维空间中的一个斜平面上满足q r s 0。为什么要引入第三个分量因为六边形网格的邻居计算在立方体坐标系下极其对称六个邻居就是固定六个方向没有奇偶行之分也没有类型分支。更重要的是从世界坐标反算格子坐标时四舍五入操作在立方体坐标系下可以做到完美无缺而在二维坐标下直接四舍五入会产生错误结果。轴向坐标其实就是立方体坐标去掉s分量的简化版。所以在实际代码中我们可以约定统一用轴向坐标参与计算只在需要取整时临时恢复s分量做一次修正。2.4 三种坐标之间的关系总结顺手梳理一下偏移坐标负责存储和显示轴向坐标负责算法逻辑立方体坐标负责取整修正。一个格子在生命周期里的完整路径是二维数组里存偏移坐标需要计算时转成轴向坐标做距离和邻居计算最后把结果再转回偏移坐标去操作数组。这条路径我在项目里走了无数遍它比“全程只用偏移坐标硬算”要省心得多。后者表面上看少了转换代码实际上一旦遇到取整、距离、邻居这些场景你会发现公式不统一之后还要为奇偶行分别写一套总量远超一次转换的代价。3. 从数学到Mesh六边形绘制公式与网格生成代码3.1 六边形的几何参数一个正六边形有两个关键半径。外半径R是中心到顶点的距离内半径r是中心到边中点的垂直距离两者的关系是r R * √3 / 2约等于0.866 * R。尖顶六边形的六个顶点从30度开始每隔60度一个。为什么从30度开始因为在尖顶形态下正右方的顶点位于水平方向偏上30度的位置。如果从0度开始得到的会是平顶六边形也就是有一条水平边朝上那不是我们要的形态。绘制Mesh时我用这六个顶点加一个中心顶点组成六个三角形。这是最直观的六边形Mesh构建方式/// summary /// 六边形几何常量。 /// 统一使用尖顶形态pointy-top中心到顶点距离为 outerRadius。 /// /summary public static class HexMetrics { public static float OuterRadius { get; private set; } public static float InnerRadius OuterRadius * 0.8660254f; // 六个顶点的局部坐标角度从30度开始 public static Vector3[] Corners new Vector3[6]; public static void Initialize(float outerRadius) { OuterRadius outerRadius; for (int i 0; i 6; i) { float angle 30f 60f * i; float rad Mathf.Deg2Rad * angle; Corners[i] new Vector3( Mathf.Cos(rad) * OuterRadius, 0f, Mathf.Sin(rad) * OuterRadius ); } } /// summary轴向坐标转世界坐标。适用于尖顶六边形、x-z平面。/summary public static Vector3 AxialToWorld(Vector2Int axial) { float x OuterRadius * (Mathf.Sqrt(3f) * axial.x Mathf.Sqrt(3f) * 0.5f * axial.y); float z OuterRadius * (1.5f * axial.y); return new Vector3(x, 0f, z); } }注意这里的坐标轴选择我把六边形放在xz平面上y轴留作高度方向。这样保留y轴给海拔扩展用后续如果有地形起伏需求直接在y分量上修改即可。如果习惯用xy平面做2D游戏把x和z换成x和y即可公式不变。3.2 格子数据类每个格子需要保存三类信息偏移坐标用于数组索引、轴向坐标用于算法计算、世界坐标用于渲染和交互。这三种坐标之间可以随时互转但缓存下来能省掉重复计算using UnityEngine; public class HexCell { public Vector2Int OffsetCoord; // 偏移坐标 (col, row) public Vector2Int AxialCoord; // 轴向坐标 (q, r) public Vector3 WorldPosition; // 格子中心世界坐标 public Color Color Color.white; public HexCell(int col, int row, Color color) { OffsetCoord new Vector2Int(col, row); // odd-r偏置偏移坐标转轴向坐标 int q col - (row - (row 1)) / 2; int r row; AxialCoord new Vector2Int(q, r); WorldPosition HexMetrics.AxialToWorld(AxialCoord); Color color; } }3.3 网格管理器与Mesh生成网格管理器是核心负责创建格子数据和渲染对象。生成时先填充数据再用代码动态创建每个六边形的Mesh不依赖任何美术资源using System.Collections.Generic; using UnityEngine; public class HexGrid : MonoBehaviour { [Header(网格参数)] public int gridWidth 8; public int gridHeight 8; public float outerRadius 1f; public Material cellMaterial; private HexCell[] cells; private DictionaryVector2Int, HexCell cellByOffset new DictionaryVector2Int, HexCell(); private void Awake() { HexMetrics.Initialize(outerRadius); GenerateGrid(); } private void Start() { CreateAllHexMeshes(); } private void GenerateGrid() { int count gridWidth * gridHeight; cells new HexCell[count]; cellByOffset.Clear(); int index 0; for (int row 0; row gridHeight; row) { for (int col 0; col gridWidth; col) { // 棋盘格配色方便观察网格结构 bool even (col row) % 2 0; Color color even ? new Color(0.72f, 0.72f, 0.72f) : new Color(0.52f, 0.52f, 0.52f); HexCell cell new HexCell(col, row, color); cells[index] cell; cellByOffset[new Vector2Int(col, row)] cell; index; } } } private void CreateAllHexMeshes() { for (int i 0; i cells.Length; i) { CreateHexMesh(cells[i]); } } private void CreateHexMesh(HexCell cell) { GameObject go new GameObject($Hex_{cell.OffsetCoord.x}_{cell.OffsetCoord.y}); go.transform.SetParent(transform, false); go.transform.position cell.WorldPosition; MeshFilter mf go.AddComponentMeshFilter(); MeshRenderer mr go.AddComponentMeshRenderer(); mr.shadowCastingMode UnityEngine.Rendering.ShadowCastingMode.Off; mr.receiveShadows false; // 演示环境使用独立材质实例便于点击变色。 // 正式项目若格子数量大不建议这样创建实例材质见第六章优化部分。 mr.material new Material(cellMaterial); Mesh mesh BuildHexMesh(cell.Color); mf.sharedMesh mesh; } private Mesh BuildHexMesh(Color color) { Mesh mesh new Mesh(); int vertexCount 7; Vector3[] vertices new Vector3[vertexCount]; Vector2[] uv new Vector2[vertexCount]; Color[] colors new Color[vertexCount]; int[] triangles new int[18]; // 六个顶点 中心点 for (int i 0; i 6; i) { vertices[i] HexMetrics.Corners[i]; colors[i] color; } vertices[6] Vector3.zero; colors[6] color; // 六个三角形中心索引为6 for (int i 0; i 6; i) { triangles[i * 3] 6; triangles[i * 3 1] i; triangles[i * 3 2] (i 1) % 6; } mesh.vertices vertices; mesh.triangles triangles; mesh.colors colors; mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } }在Unity场景里新建一个空物体挂上HexGrid创建一个纯色材质拖到cellMaterial上把相机摆成俯视角往下看运行后就能看到整片咬合正确的六边形网格。3.4 检验网格是否正确的方法一个很常见的错误是生成出来不是六边形网格而是歪歪扭扭的图形。这里给两个快速校验方法观察奇数行与偶数行是否互相咬合。如果奇数行的六边形没有嵌进偶数行的缝隙里而是上下对齐说明世界坐标公式里缺少了√3/2 * r这个轴向偏移量。从任意格子沿对角线方向走格子中心连线应当是一条直线。如果连出来是折线多半是坐标转换时奇偶行处理出了问题。我自己第一次实现时就是没把odd-r偏置转换成轴向坐标而是直接拿偏移坐标套世界坐标公式结果整个网格像梯形一样歪了。这个坑几乎每个人都会踩最好在代码注释里直接标清楚。4. 邻居方向表odd-r偏置下最容易写错的一环4.1 为什么方向表要区分奇偶行偏移坐标下一个格子的邻居位置取决于它所在行的奇偶性。原因很简单整个网格就是把奇数行向右错开了半格那么奇数行格子的斜上方邻居和偶数行格子的斜上方邻居偏移量必然不同。如果直接把正方形网格的“上下左右加上下斜角”套过来结果必然出错。比如在一个奇数行的格子里右上方的邻居并不在(col1, row-1)位置因为这个位置可能悬空。安全的做法是记住一张两张表偶数行用一张奇数行用另一张。方向顺序我固定为东、东北、西北、西、西南、南这样一个一个数过去不会漏方向偶数行偏移奇数行偏移东 E(1, 0)(1, 0)东北 NE(0, -1)(1, -1)西北 NW(-1, -1)(0, -1)西 W(-1, 0)(-1, 0)西南 SW(-1, 1)(0, 1)东南 SE(0, 1)(1, 1)这个方向表的推导过程很机械先在轴向坐标系里取六个固定方向(1,0),(1,-1),(0,-1),(-1,0),(-1,1),(0,1)再根据当前行奇偶性转回偏移坐标。只要转换公式没忘这张表可以随时推出来不需要死记。4.2 邻居查询的实现有了方向表邻居查询就变成一个二维查表操作// 偶数行和奇数行的邻居方向表顺序为东、东北、西北、西、西南、东南 private static readonly int[,] NeighborsEvenRow new int[,] { { 1, 0 }, { 0, -1 }, { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, 1 } }; private static readonly int[,] NeighborsOddRow new int[,] { { 1, 0 }, { 1, -1 }, { 0, -1 }, { -1, 0 }, { 0, 1 }, { 1, 1 } }; /// summary获取某个格子所有在网格范围内的邻居偏移坐标/summary public ListVector2Int GetNeighbors(Vector2Int offsetCoord) { ListVector2Int result new ListVector2Int(6); int[,] table (offsetCoord.y 1) 0 ? NeighborsEvenRow : NeighborsOddRow; for (int i 0; i 6; i) { int col offsetCoord.x table[i, 0]; int row offsetCoord.y table[i, 1]; if (IsInsideGrid(col, row)) { result.Add(new Vector2Int(col, row)); } } return result; } /// summary在指定偏移坐标处获取格子越界返回null/summary public HexCell GetCell(int col, int row) { if (!IsInsideGrid(col, row)) return null; cellByOffset.TryGetValue(new Vector2Int(col, row), out HexCell cell); return cell; } private bool IsInsideGrid(int col, int row) { return col 0 col gridWidth row 0 row gridHeight; }这段代码可以直接放在HexGrid类里。有了它移动范围计算、单位寻路、区域生长等玩法就能铺开写了。4.3 常见错误六方向套垂直坐标最容易犯的错误是把六方向错误地当成“上下左右加两条斜线”来写。比如有人会写出(col 1, row)、(col - 1, row)、(col, row 1)这组再随手补几个斜方向结果画出来完全不是六边形邻居关系。想验证自己的邻居表有没有写对可以在调试时做一个可视化选中一个格子把六个邻居格子高亮成红色用不同颜色标记每个方向。亲测这个调试方法能五分钟内暴露所有方向表问题远比盯着代码干看可靠。另一个容易忽略的坑很多文章和插件里用的是even-r偏置也就是偶数行右移半格方向表跟odd-r不一样。粘贴别人的方向表前必须先确认对方的偏置约定否则会得到镜像或错位的邻居关系。我曾经在一个开源项目里见过网友把odd-r和even-r的方向表混在一起结果网格看起来正常寻路却一直绕着远路。5. 屏幕点击定位格子世界坐标反查与cube round5.1 两种方案对比碰撞体还是数学反查在Unity里做格子点选通常有两条路。一是给每个六边形挂MeshCollider然后用Physics.Raycast命中格子。这个方法简单直观缺点是地图大了以后碰撞体数量巨大物理运算开销不低。另一个方案是向一个逻辑平面打射线拿到交点坐标后反算格子坐标再用数学方式拾取格子。我强烈推荐第二种。它不依赖物理系统没有碰撞体开销而且命中精度和相机角度无关只要数学公式正确一百万个格子也只需要一次反查。5.2 世界坐标反查格子的公式推导世界坐标转轴向坐标是前面正向公式的逆运算。尖顶六边形的正向公式是x R * (√3 * q √3/2 * r) z R * (3/2 * r)反解得到r (2/3) * z / R q (√3/3 * x - 1/3 * z) / R注意这里求出来的是浮点数几乎不可能恰好落在整数格子上。常见做法是四舍五入但直接对q和r分别四舍五入会出错因为有约束条件的格子空间在二维投影下是倾斜的直接取整可能会选到不存在的格子。正确姿势是立方体取整法。5.3 cube round为什么直接四舍五入会出错把轴向坐标恢复成立方体坐标(q, r, s)其中s -q - r。对三个浮点数分别四舍五入得到整数值。如果三个取整值恰好满足q r s 0说明取整成功如果不满足就需要找出偏差最大的那个分量并用另外两个分量的相反数覆盖它。这个修正保证最终结果一定落在合法的六边形网格点上。完整实现如下/// summary世界坐标转轴向坐标尖顶六边形x-z平面/summary public Vector2Int WorldToAxial(Vector3 worldPos) { float size HexMetrics.OuterRadius; float x worldPos.x; float z worldPos.z; // 反解正向公式 float q (Mathf.Sqrt(3f) / 3f * x - 1f / 3f * z) / size; float r (2f / 3f * z) / size; // 立方体取整修正 float s -q - r; int rq Mathf.RoundToInt(q); int rr Mathf.RoundToInt(r); int rs Mathf.RoundToInt(s); float dq Mathf.Abs(rq - q); float dr Mathf.Abs(rr - r); float ds Mathf.Abs(rs - s); if (dq dr dq ds) rq -rr - rs; else if (dr ds) rr -rq - rs; return new Vector2Int(rq, rr); } /// summary从世界坐标拾取格子越界返回null/summary public HexCell GetCellFromWorld(Vector3 worldPos) { Vector2Int axial WorldToAxial(worldPos); Vector2Int offset AxialToOffset(axial.x, axial.y); return GetCell(offset.x, offset.y); }代码里AxialToOffset就是第二节的转换函数。整套流程是世界坐标浮点数转轴向浮点数轴向浮点转立方体浮点立方体浮点取整修正轴向整数转偏移整数最后从字典里查格子。5.4 用射线打平面实现鼠标点选有了GetCellFromWorld鼠标点选就很简单了。在场景中往y 0平面打一条射线得到交点后直接反查格子using UnityEngine; public class HexGridInput : MonoBehaviour { public HexGrid grid; private Plane groundPlane new Plane(Vector3.up, Vector3.zero); private HexCell selectedCell; private MeshRenderer selectedRenderer; private void Update() { if (!Input.GetMouseButtonDown(0)) return; Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (!groundPlane.Raycast(ray, out float enter)) return; Vector3 hitPoint ray.GetPoint(enter); HexCell cell grid.GetCellFromWorld(hitPoint); if (cell null) return; // 恢复上一次选中格子的颜色 if (selectedCell ! null selectedRenderer ! null) { selectedRenderer.material.color selectedCell.Color; } // 高亮当前选中格子 selectedCell cell; selectedRenderer cell.SelfRenderer; selectedRenderer.material.color Color.yellow; } }这段代码里我用了cell.SelfRenderer字段需要在HexCell类里补一个渲染器引用。在CreateHexMesh时为HexCell存储MeshRenderer引用即可public class HexCell { // ... 原有字段 public MeshRenderer SelfRenderer; public HexCell(int col, int row, Color color) { // ... 原有初始化 SelfRenderer null; } }5.5 相机角度与射线命中用y 0平面反查格子前提是射线确实与平面相交。如果相机完全平视地面射线可能平行于平面或者交点非常远这时交互体验很差。一般策略类游戏相机会保持50到60度的俯视角这既能看清地图又不至于让射线失去交点。代码里的enter值表示射线起点到交点的距离。如果你的相机高度浮动比较大这个值可以作为UI射线检测或地块信息显示的参考距离但不需要刻意使用它。如果地图加入了海拔高度y 0平面反查就不再准确。一个格子海拔抬高了它对应的世界坐标y不再是0。最简单的处理是在格子边缘添加MeshCollider并改用物理射线命中或者把海拔数据存进HexCell拿到平面交点后用当前格子海拔做一次修正再反查。后者在性能上仍然优于给所有格子挂碰撞体但实现要复杂一些建议在真正需要地形起伏的项目里再做。6. 进阶与优化从Instancing到海拔、寻路6.1 性能瓶颈在哪每个格子对应一个独立GameObject、一个MeshFilter、一个MeshRenderer在原型阶段完全没问题。网格不超过15乘15时表现流畅但一旦扩大到50乘50就有2500个对象200乘200就是4万个对象Unity的场景管理、Transform同步、DrawCall都会成为瓶颈。给出一个简单的量化估算表网格规模六边形数量独立GameObject数独立DrawCall数是否适合示例方案10 x 10100100100适合30 x 30900900900勉强开始卡60 x 60360036003600明显卡顿100 x 100100001000010000不建议优化方向有两个Mesh合并和GPU Instancing。6.2 Mesh合并思路把所有六边形合并成一个大型Mesh一次性渲染DrawCall降到1。合并时需要把每个格子的顶点位置加上格子世界坐标三角形索引整体偏移。下面是思路性代码private Mesh BuildMergedMesh() { ListVector3 vertices new ListVector3(); Listint triangles new Listint(); ListColor colors new ListColor(); foreach (HexCell cell in cells) { int vertexStart vertices.Count; for (int i 0; i 6; i) { // 顶点位置要加上格子中心的世界偏移 Vector3 worldVertex cell.WorldPosition HexMetrics.Corners[i]; vertices.Add(worldVertex); colors.Add(cell.Color); } // 中心顶点 vertices.Add(cell.WorldPosition); colors.Add(cell.Color); int centerIndex vertexStart 6; for (int i 0; i 6; i) { triangles.Add(centerIndex); triangles.Add(vertexStart i); triangles.Add(vertexStart (i 1) % 6); } } Mesh mergedMesh new Mesh(); mergedMesh.vertices vertices.ToArray(); mergedMesh.colors colors.ToArray(); mergedMesh.triangles triangles.ToArray(); mergedMesh.RecalculateNormals(); mergedMesh.RecalculateBounds(); return mergedMesh; }合并后只有一个GameObject和一个Mesh渲染效率极高代价是不能再对单个格子做独立的GameObject级交互。调整格子颜色时直接改顶点色然后调用Mesh.vertices和Mesh.colors的更新接口。这正好满足策略类的常见需求格子本身不需要独立Transform只需要数据层面修改。6.3 GPU Instancing如果不希望做Mesh合并另一个思路是保留每个格子独立的MeshRenderer但使用MaterialPropertyBlock配合GPU Instancing一次性渲染大量同Mesh对象。这个方案比较适合每个格子颜色、缩放需要动态变化但又不希望合并Mesh的场景。对纯色六边形网格Mesh合并通常是最简单最直接的选择考虑到读者大多是做原型验证我建议先走合并路线等需要动态拆分地形时再考虑Instancing。6.4 距离函数为寻路和范围计算准备六边形网格距离在轴向坐标下极其优雅。给定两个轴向坐标距离是三个分量差绝对值的最大值/// summary六边形网格轴向坐标距离/summary public static int HexDistance(Vector2Int a, Vector2Int b) { int dq Mathf.Abs(a.x - b.x); int dr Mathf.Abs(a.y - b.y); int ds Mathf.Abs(-a.x - a.y b.x b.y); return Mathf.Max(dq, dr, ds); }这里传入的必须是轴向坐标不是偏移坐标。我见过很多人拿偏移坐标直接算距离得到的结果在奇偶行交替时莫名其妙增大或减小。其实只要转一次坐标就好。有了距离函数A寻路的启发式函数可以直接用它。在探索范围内目标格子与当前格子的估价距离可以直接用HexDistance因为六边形网格没有正方形网格那种斜角奇异性A的启发式天然满足一致性条件不需要额外调权。6.5 海拔与通行性扩展六边形网格的三维扩展非常自然。每个HexCell增加一个elevation字段渲染时把mesh顶点整体抬升即可。AxialToWorld返回的位置加上elevation * cellHeight就能得到带地形的格子位置。移动判定上常见规则是两个格子海拔差超过1时不可通行这只需要在邻居遍历时比较一下高度差。六边形网格切得比较均匀做地形分层时视觉效果比正方形网格好很多因为不会出现正方形地形中常见的“对角线棱角”。6.6 一整套可直接跑通的代码组织最后把代码文件组织一下便于直接放进Unity项目HexMetrics.cs几何常量、轴向坐标转世界坐标HexCell.cs格子数据包含偏移坐标、轴向坐标、世界坐标、颜色、渲染器引用HexGrid.cs网格生成、邻居查询、世界坐标反查、Mesh构建HexGridInput.cs鼠标点选演示场景搭建步骤新建场景删除默认平行光里的阴影提升性能新建空物体挂HexGrid组件创建标准材质拖到cellMaterial槽位新建空物体挂HexGridInput把HexGrid拖上去把相机摆到(6, 8, -6)附近旋转约55度俯视网格运行点击格子即可看到黄色高亮这套代码和结构是我自己项目中抽出来的基础版。最开始我也从抄网上的六边形教程开始但没理解坐标系的差异导致加了海拔和寻路后到处是特判。后来把坐标系彻底理顺才发现六边形网格的核心代码量比想象中少得多一个方向表、一个坐标转换函数、一个取整修正剩下的都是具体玩法逻辑。如果你正在做策略、模拟经营或任何需要走格子的项目建议先停下手头代码把坐标系这关过了后面会顺很多。