1. 项目概述当《文明》遇见Unity一套坐标转换代码的诞生几年前当我第一次尝试在Unity里复现《文明》系列那种标志性的六边形地图时我踩遍了所有能踩的坑。从鼠标点击屏幕到精准选中那个小小的六边形格子中间隔着一道看似简单、实则暗藏玄机的数学鸿沟。网上能找到的教程要么只讲理论一堆公式看得人头大要么给个代码片段但一放到自己的项目里就各种对不上不是点不准就是边缘判定诡异。经过几个项目的打磨和无数次调试我终于总结出了一套从立体坐标到屏幕点击的完整解决方案。这套方案的核心思想就是构建一个清晰、自洽的坐标转换流水线用一套代码把《文明》式SLG地图的底层交互逻辑彻底搞定。今天我就把这套实战经验毫无保留地分享出来无论你是刚接触六边形地图的新手还是正在为点击判定头疼的开发者相信都能从中找到直击痛点的答案。六边形地图的魅力在于其无与伦比的战略纵深和移动公平性相比正方形网格它每个方向的邻接关系都是等距的没有对角线移动带来的“作弊感”。但在实现上这份优雅带来了额外的复杂度我们熟悉的笛卡尔坐标系x, y不再直接适用需要引入一套专为六边形设计的坐标系统比如立方体坐标或轴向坐标并建立它与Unity世界坐标、屏幕像素坐标之间稳固的桥梁。这个过程涉及空间变换、几何计算和高效的算法查询是SLG游戏地图系统的基石。搞定了它你的游戏就拥有了一个坚实、可靠的世界舞台。2. 核心思路与坐标系统选型2.1 为什么是六边形坐标系统三剑客在深入代码之前我们必须先统一“语言”。处理六边形网格主要有三种流行的坐标系统偏移坐标、轴向坐标和立方体坐标。偏移坐标是最直观的它试图在传统的二维数组行、列上描述六边形。比如你可以把奇数行或偶数列的格子进行“偏移”。这种方法在存储地图数据时很直观想象一个二维数组map[row, col]就能对应一个格子。但是它的致命伤在于距离计算和邻居查找变得异常复杂且不统一。计算两个偏移坐标格子之间的距离公式会因行列的奇偶性而不同代码里会充满if (row % 2 0)这样的判断非常容易出错也不够优雅。轴向坐标是更专业的选择。它引入了一个倾斜的坐标系通常记为(q, r)。你可以把q轴想象成水平向右r轴则指向右下方或右上方取决于你的网格方向。每个六边形由一对(q, r)值唯一确定。这个系统的优点是很多计算如距离、邻居有了统一的公式比偏移坐标简洁得多。它是我早期常用的系统。立方体坐标则是我最终推荐并将在本文中详细使用的系统。它虽然听起来更“三维”但理解后会发现其数学之美和计算上的便利性无与伦比。其核心思想是将一个平面上的六边形网格映射到一个三维立方体的六个斜面上。我们使用三个坐标轴(x, y, z)来描述一个六边形并且强制约束x y z 0。这个约束条件意味着虽然有三个坐标但实际自由度只有两个和二维坐标是等价的。为什么选择立方体坐标因为它让所有六边形网格的向量运算变得像整数加减法一样简单。距离计算两个六边形A(x1, y1, z1)和B(x2, y2, z2)之间的距离等于它们各坐标轴差值绝对值的最大值再除以2不更简单(abs(x1-x2) abs(y1-y2) abs(z1-z2)) / 2。由于xyz0这个公式恒成立且非常对称。邻居查找一个六边形的六个直接邻居其坐标就是在原坐标(x, y, z)的基础上分别给x, y, z中的一个加1、另一个减1保持总和为0。这可以预先定义为一个常量方向数组查找邻居就是一次向量加法。线性插值与路径查找在立方体坐标空间中进行线性插值结果可以方便地取整回有效的六边形坐标这对于实现A*寻路等算法非常友好。实操心得不要被“立方体”和第三个坐标z吓到。记住z -x - y它只是一个由另外两个坐标导出的、用于简化计算的衍生值。在代码中我们通常只存储x和y或者q和rz在需要时实时计算。从轴向坐标(q, r)转换到立方体坐标(x, y, z)也非常简单取决于你设定的轴对应关系。2.2 整体转换流水线设计我们的目标是从一次鼠标点击最终得到一个具体的六边形格子对象。这个过程需要一条清晰的转换链。下图描绘了这条核心流水线flowchart TD A[屏幕像素坐标brInput.mousePosition] -- B{第一步屏幕到视口} B -- C[视口坐标br归一化 0-1] C -- D{第二步视口到世界射线} D -- E[摄像机射线brRay origin direction] E -- F{第三步射线与地图平面求交} F -- G[世界空间交点坐标brVector3] G -- H{第四步世界坐标到六边形立方体坐标} H -- I[浮点数立方体坐标brx, y, z] I -- J{第五步坐标取整与验证} J -- K[最终六边形格子索引brCubeCoord]这条流水线的每个环节都至关重要。屏幕到视口和视口到世界射线是Unity摄像机的基础操作决定了我们“看”世界的起点和方向。射线与平面求交是几何碰撞检测将屏幕上的一个点对应到游戏世界中的一个具体位置。最核心也最独特的是后两步世界坐标到六边形坐标的转换这需要根据你地图的布局平顶还是尖顶、六边形大小和朝向来计算坐标取整与验证则是将计算得到的浮点坐标“吸附”到最近的、有效的六边形格子上并处理边缘点击等边界情况。3. 核心模块实现与代码解析3.1 定义六边形数据与布局首先我们需要定义六边形的基本属性和整个地图的布局方式。创建一个HexMetrics静态类来存放所有常量是个好习惯。using UnityEngine; public static class HexMetrics { // 六边形外接圆半径从中心到顶点的距离 public const float outerRadius 10f; // 六边形内切圆半径从中心到边的垂直距离 public const float innerRadius outerRadius * 0.866025404f; // sqrt(3)/2 // 平顶六边形的六个顶点相对位置以中心为原点 public static readonly Vector3[] corners { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) // 重复第一个点方便闭合绘制 }; // 立方体坐标下的六个邻居方向向量 public static readonly CubeCoord[] directions { new CubeCoord(1, -1, 0), new CubeCoord(1, 0, -1), new CubeCoord(0, 1, -1), new CubeCoord(-1, 1, 0), new CubeCoord(-1, 0, 1), new CubeCoord(0, -1, 1) }; } // 立方体坐标结构体 [System.Serializable] public struct CubeCoord { public int x; public int y; public int z; // 由x, y推导但存储起来方便计算 public CubeCoord(int x, int y) { this.x x; this.y y; this.z -x - y; } public CubeCoord(int x, int y, int z) { this.x x; this.y y; this.z z; } // 静态的零坐标 public static CubeCoord zero new CubeCoord(0, 0, 0); // 运算符重载让坐标运算更直观 public static CubeCoord operator (CubeCoord a, CubeCoord b) new CubeCoord(a.x b.x, a.y b.y, a.z b.z); public static CubeCoord operator -(CubeCoord a, CubeCoord b) new CubeCoord(a.x - b.x, a.y - b.y, a.z - b.z); // 计算到另一个立方体坐标的距离 public int DistanceTo(CubeCoord other) { return (Mathf.Abs(x - other.x) Mathf.Abs(y - other.y) Mathf.Abs(z - other.z)) / 2; } // 获取指定方向的邻居 public CubeCoord GetNeighbor(int direction) { if (direction 0 || direction 6) return this; return this HexMetrics.directions[direction]; } }这里我们选择了平顶六边形布局corners数组的定义方式这是类似《文明》的常见布局。如果你需要尖顶六边形只需旋转这个顶点数组即可。CubeCoord结构体封装了立方体坐标的所有基本操作包括距离计算和邻居查找这会让后续的逻辑变得非常清晰。3.2 从世界坐标到立方体坐标的转换这是整个系统的核心算法。给定一个Unity世界空间中的点(worldPos)我们需要找出它落在哪个六边形内。using UnityEngine; public class HexGrid : MonoBehaviour { public float hexSize HexMetrics.outerRadius; // 关键转换函数世界坐标 - 立方体坐标 public CubeCoord WorldPosToCubeCoord(Vector3 worldPos) { // 1. 将世界坐标转换到以当前Grid原点为基准的局部平面坐标XZ平面 Vector3 localPos worldPos - transform.position; // 假设地图在XZ平面上Y轴为高度。忽略Y值。 float x localPos.x; float z localPos.z; // 2. 将局部平面坐标转换为六边形网格空间中的“分数坐标” // 对于平顶六边形每个六边形在X方向的宽度是 innerRadius * 2在Z方向的高度是 outerRadius * 1.5 float q (x * Mathf.Sqrt(3) / 3 - z / 3) / hexSize; float r (z * 2f / 3) / hexSize; // 3. 将轴向分数坐标(q, r)转换为立方体分数坐标(x, y, z) float cubeX q; float cubeY -q - r; float cubeZ r; // 4. 对立方体分数坐标进行取整得到候选的整数坐标 int rx Mathf.RoundToInt(cubeX); int ry Mathf.RoundToInt(cubeY); int rz Mathf.RoundToInt(cubeZ); // 5. 由于四舍五入可能导致 xyz ! 0需要进行修正 float xDiff Mathf.Abs(rx - cubeX); float yDiff Mathf.Abs(ry - cubeY); float zDiff Mathf.Abs(rz - cubeZ); if (xDiff yDiff xDiff zDiff) rx -ry - rz; else if (yDiff zDiff) ry -rx - rz; else rz -rx - ry; // 6. 返回修正后的立方体坐标 return new CubeCoord(rx, ry, rz); } }步骤解析与注意事项坐标归一化首先将输入的世界坐标转换到以地图网格原点为基准的局部坐标系。这是为了消除整个地图GameObject的位移、旋转带来的影响。平面坐标到轴向分数坐标这个公式是平顶六边形布局的推导结果。它根据六边形的几何关系将XZ平面上的点映射到轴向坐标(q, r)所在的连续空间中。hexSize用于缩放通常等于outerRadius。轴向到立方体转换根据定义进行转换。这里我们令x q,z r,y -x - z。四舍五入取整得到浮点数的立方体坐标后对三个分量分别四舍五入到最近的整数。约束修正这是最关键的一步由于四舍五入是独立进行的得到的整数坐标(rx, ry, rz)很可能不满足xyz0的约束。我们需要检查哪个分量的舍入误差最大然后根据约束条件重新计算该分量用另外两个分量推导确保最终得到一个有效的立方体坐标。这个算法被称为“立方体坐标取整”或“四舍五入约束修正”它能保证我们总是得到距离输入点最近的、有效的六边形格子坐标。踩坑实录我曾经尝试过先取整再修正或者只取整两个分量用公式算第三个结果在六边形边缘区域会出现跳变点击感觉“不跟手”。这个“比较误差并修正最大误差项”的方法是保证边缘点击判定稳定的黄金法则。3.3 屏幕点击到格子拾取的完整流程现在我们将摄像机射线与上述坐标转换结合起来实现完整的点击拾取功能。通常我会在一个管理类如HexMapManager或直接挂在摄像机上的脚本里处理这个逻辑。using UnityEngine; public class HexMapInput : MonoBehaviour { public Camera gameCamera; // 主摄像机 public HexGrid hexGrid; // 你的六边形网格管理器 public LayerMask groundLayer; // 地图所在层用于射线检测 private CubeCoord lastHoveredCoord; // 上次悬停的格子用于高亮等效果 void Update() { HandleMouseHoverAndClick(); } void HandleMouseHoverAndClick() { // 1. 从屏幕坐标生成射线 Ray ray gameCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 2. 射线与地图平面地面碰撞检测 // 这里使用物理射线检测简单可靠。确保你的地图有一个Collider如MeshCollider或Terrain Collider。 if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer)) { Vector3 worldHitPoint hit.point; // 3. 将碰撞点世界坐标转换为六边形立方体坐标 CubeCoord currentCoord hexGrid.WorldPosToCubeCoord(worldHitPoint); // 4. 处理悬停逻辑例如高亮格子 if (!currentCoord.Equals(lastHoveredCoord)) { OnHexHoverExit(lastHoveredCoord); // 取消上一个格子的高亮 OnHexHoverEnter(currentCoord); // 高亮当前格子 lastHoveredCoord currentCoord; } // 5. 处理点击逻辑 if (Input.GetMouseButtonDown(0)) // 左键点击 { OnHexClicked(currentCoord); } } else { // 射线未击中地面取消悬停 if (lastHoveredCoord ! null) { OnHexHoverExit(lastHoveredCoord); lastHoveredCoord CubeCoord?null; } } } void OnHexHoverEnter(CubeCoord coord) { // 在这里实现格子高亮效果例如改变材质、显示边框等。 // 可以通过hexGrid根据coord获取具体的HexCell对象。 HexCell cell hexGrid.GetCell(coord); if (cell ! null) cell.SetHovered(true); Debug.Log($悬停在六边形: {coord.x}, {coord.y}, {coord.z}); } void OnHexHoverExit(CubeCoord coord) { HexCell cell hexGrid.GetCell(coord); if (cell ! null) cell.SetHovered(false); } void OnHexClicked(CubeCoord coord) { // 在这里处理点击事件例如选中单位、移动、打开菜单等。 HexCell cell hexGrid.GetCell(coord); if (cell ! null) { Debug.Log($点击了六边形: {coord.x}, {coord.y}, {coord.z}。 格子信息: {cell}); // 触发游戏逻辑... } } }代码要点解析ScreenPointToRay这是Unity提供的标准方法将屏幕像素坐标鼠标位置转换为一条从摄像机出发、穿过该屏幕点的世界空间射线。物理射线检测使用Physics.Raycast与地图的碰撞体进行交互。这是最稳健的方法能自动处理地形高低起伏如果你的地图不是纯平面。确保你的地图GameObject附带了Collider并设置在正确的groundLayer上。hit.point射线与碰撞体相交点的精确世界坐标。这个点就是鼠标“指”在地面上的位置。事件分离将悬停Hover和点击Click逻辑分开。悬停通常用于实时反馈高亮点击用于触发主要动作。这能显著提升用户体验。性能考虑每帧都进行射线检测和坐标转换。在大多数情况下这对性能影响微乎其微。但如果地图极大或单位极多可以考虑在FixedUpdate中进行或添加一个距离阈值当鼠标移动超过一定像素再检测。4. 高级应用与性能优化实战4.1 地图存储、查询与高效索引当你的地图从几十个格子扩展到成千上万个时如何快速根据CubeCoord找到对应的HexCell对象就变得至关重要。一个高效的索引数据结构是必须的。using System.Collections.Generic; using UnityEngine; public class HexGrid : MonoBehaviour { private DictionaryCubeCoord, HexCell cellDictionary; private ListHexCell cellsList; void Awake() { cellDictionary new DictionaryCubeCoord, HexCell(); cellsList new ListHexCell(); // 假设在Start或某个初始化方法中生成所有格子并注册 // GenerateMap(); } public void RegisterCell(CubeCoord coord, HexCell cell) { if (!cellDictionary.ContainsKey(coord)) { cellDictionary.Add(coord, cell); cellsList.Add(cell); cell.Coord coord; // 让格子也知道自己的坐标 } else { Debug.LogError($重复的六边形坐标注册: {coord}); } } public HexCell GetCell(CubeCoord coord) { HexCell cell null; cellDictionary.TryGetValue(coord, out cell); return cell; // 如果不存在返回null } public HexCell GetCell(int x, int y) { return GetCell(new CubeCoord(x, y)); } // 根据世界坐标获取格子整合了转换和查询 public HexCell GetCellFromWorldPos(Vector3 worldPos) { CubeCoord coord WorldPosToCubeCoord(worldPos); return GetCell(coord); } // 获取一个坐标的所有邻居格子 public ListHexCell GetNeighbors(CubeCoord coord) { ListHexCell neighbors new ListHexCell(6); for (int i 0; i 6; i) { CubeCoord neighborCoord coord.GetNeighbor(i); HexCell neighborCell GetCell(neighborCoord); if (neighborCell ! null) { neighbors.Add(neighborCell); } } return neighbors; } }为什么用DictionaryCubeCoord, HexCellCubeCoord结构体需要正确实现GetHashCode和Equals方法才能作为字典的键。由于我们的CubeCoord是值类型struct且字段都是整数默认实现通常是可行的但为了绝对可靠可以重写这两个方法。查找效率字典的查找时间复杂度接近O(1)这意味着无论地图上有10个还是10000个格子根据坐标查找一个格子的速度都极快。ListHexCell作为辅助存储用于需要遍历所有格子的情况如保存/加载、全局更新等。性能优化技巧如果你的地图是固定的矩形或六边形区域也可以使用二维数组或交错数组来存储通过一个将CubeCoord映射到数组索引的公式来访问。这比字典的内存局部性更好访问可能更快。但对于不规则地图或动态生成/销毁格子的地图字典的灵活性无可替代。4.2 坐标转换的逆运算从格子到世界我们不仅需要从世界坐标找到格子也需要知道一个格子在Unity世界中的具体位置例如将单位移动到某个格子中心。这需要实现逆转换。public class HexGrid : MonoBehaviour { // ... 其他代码 ... // 关键转换函数立方体坐标 - 世界坐标格子中心点 public Vector3 CubeCoordToWorldPos(CubeCoord coord) { // 对于平顶六边形布局的转换公式 float x hexSize * (Mathf.Sqrt(3) * coord.x Mathf.Sqrt(3)/2 * coord.y); float z hexSize * (3f/2 * coord.y); // 注意这里的x, z对应Unity世界空间的X和Z轴。Y轴高度可以根据地形或逻辑单独设置。 Vector3 localPos new Vector3(x, 0f, z); // 加上网格整体的偏移 return transform.TransformPoint(localPos); } // 获取一个格子所有顶点的世界坐标用于绘制或碰撞体生成 public Vector3[] GetHexCornersWorldPos(CubeCoord coord) { Vector3 center CubeCoordToWorldPos(coord); Vector3[] corners new Vector3[7]; // 7个点最后一个与第一个重合方便绘制闭合图形 for (int i 0; i 7; i) { corners[i] center transform.rotation * HexMetrics.corners[i]; // 考虑网格的旋转 } return corners; } }公式推导说明CubeCoordToWorldPos中的公式是前面WorldPosToCubeCoord转换公式的逆运算。它根据立方体坐标(x, y, z)其中y -x - z推导回世界XZ平面坐标。这个公式保证了格子中心点的精确计算。应用场景单位移动当命令一个单位移动到HexCell A时调用CubeCoordToWorldPos(A.Coord)即可得到目标点世界坐标用于导航或插值移动。视觉表现生成网格线、高亮效果、建筑放置等都需要知道格子的精确形状和位置GetHexCornersWorldPos方法提供了顶点数据。预计算在游戏初始化时可以预先计算所有格子的世界坐标并缓存起来避免运行时频繁计算。4.3 处理地形高度与斜坡真实的SLG地图 rarely 是纯平的。如何将这套坐标系统扩展到有高度变化Y轴的地形上核心思路是将XZ平面上的坐标转换与Y轴的高度解耦。我们依然在XZ平面上进行六边形格子的归属判断WorldPosToCubeCoord只考虑X和Z。每个HexCell格子对象记录其地形高度、表面法线等信息。当需要获取一个格子的“表面位置”时使用CubeCoordToWorldPos得到XZ平面中心再根据该格子的高度数据叠加Y值。public class HexCell : MonoBehaviour { public CubeCoord Coord { get; set; } public int Elevation { get; private set; } // 海拔高度可以是整数层级 public float VisualHeightOffset { get; private set; } // 视觉高度偏移用于平滑过渡 // 获取格子表面的世界坐标考虑高度 public Vector3 SurfaceWorldPosition { get { Vector3 basePos HexGrid.Instance.CubeCoordToWorldPos(Coord); basePos.y Elevation * heightPerLevel VisualHeightOffset; return basePos; } } }在射线检测时Physics.Raycast返回的hit.point已经是考虑了碰撞体实际形状包括高度的交点。所以WorldPosToCubeCoord函数接收到的worldPos的Y值可能不为0但我们只取其X和Z分量进行计算这完全正确。格子的逻辑位置属于哪个六边形由其XZ投影决定而视觉和交互位置则由SurfaceWorldPosition决定。高级技巧斜坡上的移动与视野。在寻路算法如A*中成本Cost不仅要考虑距离还要考虑高度差。你可以将高度差作为一个惩罚因子加入移动成本计算中。同样视野系统Line of Sight也需要考虑高度可能需要在射线检测中同时检查地形高度和障碍物。5. 常见问题、调试技巧与避坑指南5.1 点击不准确或抖动这是最常见的问题现象是鼠标在格子边缘移动时选中的格子会频繁跳动。原因1射线碰撞体不匹配。你的视觉六边形模型和用于射线检测的碰撞体可能没有精确对齐。解决方案确保用于Physics.Raycast的碰撞体如一个覆盖地形的Plane、Terrain Collider或由许多六边形MeshCollider拼接的整体与视觉网格完美贴合。一个调试方法是在OnHexHoverEnter中用Debug.DrawLine画出从摄像机到hit.point的射线并在地面交点处画一个小球Debug.DrawSphere观察这个点是否精确落在视觉格子的中心。原因2坐标转换公式错误。平顶和尖顶六边形的转换公式不同hexSizeouterRadius的值必须与视觉模型的实际尺寸一致。解决方案单独测试坐标转换函数。创建一个测试脚本在场景中手动指定一些世界坐标点打印出转换后的CubeCoord并在这个坐标对应的格子中心可视化一个标记看是否匹配。原因3取整修正算法缺陷。确保你使用的是前面提到的“比较误差并修正最大误差项”的取整算法这是最稳定的方法。5.2 大规模地图的性能瓶颈问题当地图有上万个格子时每帧为每个格子更新逻辑即使只是检查鼠标悬停可能造成卡顿。优化方案1分帧处理。如果不是需要即时反馈的逻辑如非实时的路径计算可以分摊到多帧完成。优化方案2空间分区与视锥体剔除。只对摄像机视野内或附近的格子进行高频率的逻辑更新如悬停高亮。对于远处的格子可以降低更新频率或暂停更新。Unity的GeometryUtility.TestPlanesAABB可以帮助进行视锥体检测。优化方案3避免在Update中做昂贵的查找。例如GetCell调用字典查找很快但如果你在Update中遍历所有格子做某些计算就需要优化。使用缓存、脏标记等模式。5.3 坐标转换的数学验证如何确信你的转换公式是正确的编写单元测试或编辑器工具是极好的方法。#if UNITY_EDITOR using UnityEditor; using UnityEngine; [CustomEditor(typeof(HexGrid))] public class HexGridEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); HexGrid grid (HexGrid)target; if (GUILayout.Button(测试坐标转换往返)) { TestRoundTripConversion(grid); } } void TestRoundTripConversion(HexGrid grid) { int testCount 100; float maxError 0f; for (int i 0; i testCount; i) { // 随机生成一个世界坐标在地图范围内 Vector3 randomWorldPos grid.transform.position new Vector3( Random.Range(-50f, 50f), 0f, Random.Range(-50f, 50f) ); // 世界坐标 - 立方体坐标 CubeCoord coord grid.WorldPosToCubeCoord(randomWorldPos); // 立方体坐标 - 世界坐标格子中心 Vector3 centerWorldPos grid.CubeCoordToWorldPos(coord); // 计算原始点与格子中心点的距离在XZ平面上 float distance Vector2.Distance( new Vector2(randomWorldPos.x, randomWorldPos.z), new Vector2(centerWorldPos.x, centerWorldPos.z) ); maxError Mathf.Max(maxError, distance); // 这个距离应该小于六边形的内切圆半径innerRadius否则可能转换到了错误的邻居格子。 if (distance HexMetrics.innerRadius * 1.1f) // 留10%容错 { Debug.LogError($转换误差过大输入:{randomWorldPos}, 输出坐标:{coord}, 中心:{centerWorldPos}, 距离:{distance}); } } Debug.Log($坐标转换往返测试完成。最大误差: {maxError}. 内切圆半径(innerRadius)为: {HexMetrics.innerRadius}); } } #endif这个编辑器按钮可以快速验证你的转换流水线是否闭合。理想情况下一个世界坐标点转换到六边形坐标再转换回该格子中心的世界坐标两点在XZ平面上的距离不应超过innerRadius否则它可能属于另一个更近的格子。5.4 六边形方向与邻居索引务必明确你定义的六个邻居方向HexMetrics.directions与视觉模型的对应关系。平顶六边形的“方向0”通常是正东或正右方向。你可以在初始化时用不同颜色渲染每个方向的邻居来验证方向定义是否正确。void DebugDrawNeighborDirections(CubeCoord centerCoord) { Vector3 centerPos CubeCoordToWorldPos(centerCoord); for (int dir 0; dir 6; dir) { CubeCoord neighborCoord centerCoord.GetNeighbor(dir); Vector3 neighborPos CubeCoordToWorldPos(neighborCoord); Debug.DrawLine(centerPos, neighborPos, Color.red, 2f); // 可以在neighborPos处实例化一个带有方向编号的文字Mesh进行可视化 } }这套从立体坐标到屏幕点击的六边形地图解决方案经过多个项目的验证稳定且高效。它不仅仅是一套代码更是一种处理复杂空间关系的思维方式。一旦你理解了立方体坐标的优雅和转换流水线的清晰开发SLG游戏地图相关的任何功能——寻路、视野、区域划分、技能范围——都会变得有章可循。记住在游戏开发中坚实的基础层设计能省去后期无数的调试时间。希望这份详细的实战指南能成为你构建自己《文明》世界的一块坚实基石。