Unity六边形网格坐标转换工具库:从原理到工程实践
1. 项目概述告别坐标计算的“苦力活”如果你正在开发一款策略游戏、模拟经营游戏或者任何需要六边形网格Hex Grid地图的项目那么“坐标转换”这个坎儿你肯定绕不过去。从逻辑上的立体坐标Cube Coordinates或轴向坐标Axial Coordinates到屏幕上实际的像素或世界坐标这中间的计算公式网上教程一搜一大把。但每次新开一个项目或者地图参数稍有变动就得把这些公式重新推导、复制粘贴、调试一遍是不是感觉像在重复造轮子而且还是个容易出错的轮子更别提还要处理六边形网格的奇偶偏移、不同朝向平顶还是尖顶带来的差异了。这个项目的核心就是帮你把这些繁琐、易错但又至关重要的数学计算封装成一个干净、高效、即拿即用的C#工具库。让你能把精力真正放在游戏逻辑和玩法设计上而不是一遍又一遍地跟三角函数和偏移量较劲。2. 六边形地图坐标系统深度解析在动手封装之前我们必须彻底理解六边形地图常用的几种坐标系统。这是所有转换逻辑的基石理解透了后面的封装才能知其然更知其所以然。2.1 为什么是立体坐标Cube Coordinates你可能见过轴向坐标Axial 即q, r、偏移坐标Offset等。但在这个封装里我选择以立体坐标x, y, z作为核心的内部逻辑坐标。原因有三计算对称优雅在立体坐标系中一个六边形的坐标满足x y z 0这个约束条件。这个特性使得距离计算、邻居查找等操作变得极其简单。例如两个六边形a和b之间的“网格距离”就是它们各坐标分量差值的绝对值之和除以2(abs(dx) abs(dy) abs(dz)) / 2。这种简洁性是其他坐标系难以比拟的。方向定义清晰从中心六边形出发到其六个邻居的方向向量可以整齐地定义为一个数组CubeDirectionVectors [ (1, -1, 0), (1, 0, -1), (0, 1, -1), (-1, 1, 0), (-1, 0, 1), (0, -1, 1) ]。遍历邻居只需简单向量加法。易于与其他系统互转立体坐标与轴向坐标q, r的转换是线性的通常令x q, z r, y -x-z与屏幕坐标的转换也有一套成熟的几何公式。以它为中枢可以无歧义地转换到其他任何需要的坐标形式。注意虽然立体坐标用到了三个分量但由于xyz0的约束它本质上仍然是二维的。你可以把y看作是一个依赖项但在存储和计算时三个分量一起使用会让逻辑更清晰。2.2 屏幕坐标的两种世界平顶与尖顶六边形在屏幕上的渲染主要分为两种朝向平顶Flat-top和尖顶Pointy-top。这个选择直接影响所有坐标转换公式。平顶六边形六边形的上下边是水平的。这种布局在策略游戏中很常见因为它在水平方向上有更自然的延伸感。尖顶六边形六边形的左右顶点在垂直线上。这种布局在垂直方向上有更好的连续性。我们的封装必须同时支持这两种模式。关键在于理解两者的几何差异对于平顶六边形其宽度外接圆水平直径是size * 2高度外接圆垂直直径是size * sqrt(3)。而对于尖顶六边形宽度和高度恰好相反宽度是size * sqrt(3)高度是size * 2。这里的size通常指六边形中心到顶点的距离即外接圆半径。2.3 偏移坐标应对现实世界的“不完美”立体坐标很美但我们的地图数据在内存或存储时可能更习惯用二维数组行和列来索引。这就是偏移坐标Offset Coordinates的用武之地。它用(col, row)来定位一个六边形直观且易于遍历。然而偏移坐标有个“坑”奇数列和偶数列的六边形其行坐标的偏移规律不同对于平顶地图是奇数列偏移对于尖顶地图是奇数行偏移。我们的封装需要正确处理这种奇偶性实现偏移坐标与立体坐标之间的无损转换。3. 核心转换算法的封装与实现理解了理论基础我们来看封装的核心。我将所有转换算法集中在一个静态类HexCoordinateSystem中它不依赖于任何MonoBehaviour是纯粹的数据工具类。3.1 定义核心数据结构与配置首先我们需要一个结构体来封装立体坐标并定义地图的配置参数。using UnityEngine; namespace HexMapUtilities { // 六边形朝向枚举 public enum HexOrientation { FlatTop, // 平顶 PointyTop // 尖顶 } // 立体坐标结构体 [System.Serializable] public struct CubeCoord { public int x; public int y; public int z; // 约束x y z 0 public CubeCoord(int x, int y, int z) { this.x x; this.y y; this.z z; // 简易验证更严格的校正应在转换函数中处理 // Debug.Assert(x y z 0, Invalid cube coordinate: sum must be 0.); } // 方便打印调试 public override string ToString() $({x}, {y}, {z}); } // 地图配置类可序列化以便在Inspector中调整 [System.Serializable] public class HexMapConfig { public HexOrientation orientation HexOrientation.FlatTop; public float hexSize 1.0f; // 六边形外接圆半径 public float hexWidth orientation HexOrientation.FlatTop ? hexSize * 2f : hexSize * Mathf.Sqrt(3f); public float hexHeight orientation HexOrientation.FlatTop ? hexSize * Mathf.Sqrt(3f) : hexSize * 2f; public Vector2 horizontalSpacing new Vector2(1f, 0f); // 可自定义非均匀间距 public Vector2 verticalSpacing new Vector2(0f, 1f); } }HexMapConfig类是关键它集中了所有影响坐标转换的物理参数。hexWidth和hexHeight是计算属性根据朝向自动给出正确的值。horizontalSpacing和verticalSpacing允许你定义非正方形的网格间距这在某些艺术风格或特殊布局中很有用。3.2 立体坐标 ↔ 屏幕世界坐标这是最核心的转换。屏幕坐标通常指Unity世界空间中的Vector3位置。平顶六边形的转换公式推导 假设六边形中心在原点size为外接圆半径。水平方向相邻六边形中心相距width size * 2。垂直方向因为六边形是错开的垂直方向相邻行中心相距height size * sqrt(3)。并且奇数列或偶数列取决于你的偏移规则的六边形会向下偏移height / 2。从立体坐标(x, y, z)转换到世界坐标(worldX, worldY)的公式为worldX size * (3f/2f * x) worldY size * (Mathf.Sqrt(3f) * (z x / 2f))注意这里我们利用了y -x - z的约束消去了y。封装实现public static class HexCoordinateSystem { // 立体坐标 - 世界坐标 public static Vector3 CubeToWorld(CubeCoord cube, HexMapConfig config) { float x 0f, y 0f; float size config.hexSize; if (config.orientation HexOrientation.FlatTop) { x size * (3f / 2f * cube.x); y size * (Mathf.Sqrt(3f) * (cube.z cube.x / 2f)); } else // PointyTop { x size * (Mathf.Sqrt(3f) * (cube.x cube.z / 2f)); y size * (3f / 2f * cube.z); } // 应用自定义间距 Vector3 worldPos new Vector3( x * config.horizontalSpacing.x y * config.verticalSpacing.x, 0, // 假设Y轴向上这里用Z轴作为世界空间的垂直方向。可根据项目调整。 x * config.horizontalSpacing.y y * config.verticalSpacing.y ); return worldPos; } // 世界坐标 - 立体坐标 (像素/世界点取整到最近的六边形) public static CubeCoord WorldToCube(Vector3 worldPos, HexMapConfig config) { // 首先移除自定义间距的影响需要解一个简单的线性方程组 // 为简化这里假设horizontalSpacing和verticalSpacing是正交且缩放均匀的。 // 复杂情况下可能需要矩阵求逆。这里给出基础版本的实现。 float localX worldPos.x; float localY worldPos.z; // 同样假设世界空间Y轴向上XZ平面是地面。 float size config.hexSize; float q, r; if (config.orientation HexOrientation.FlatTop) { q (2f/3f * localX) / size; r (-1f/3f * localX Mathf.Sqrt(3f)/3f * localY) / size; } else { q (Mathf.Sqrt(3f)/3f * localX - 1f/3f * localY) / size; r (2f/3f * localY) / size; } // 将轴向坐标(q, r)转换为立体坐标(x, y, z) return RoundAxialToCube(q, r); } // 辅助函数将浮点数轴向坐标舍入到最近的整数立体坐标 private static CubeCoord RoundAxialToCube(float q, float r) { float x q; float z r; float y -x - z; int rx Mathf.RoundToInt(x); int ry Mathf.RoundToInt(y); int rz Mathf.RoundToInt(z); // 由于四舍五入可能导致 xyz ! 0需要校正 float xDiff Mathf.Abs(rx - x); float yDiff Mathf.Abs(ry - y); float zDiff Mathf.Abs(rz - z); if (xDiff yDiff xDiff zDiff) rx -ry - rz; else if (yDiff zDiff) ry -rx - rz; else rz -rx - ry; return new CubeCoord(rx, ry, rz); } }实操心得WorldToCube中的RoundAxialToCube函数是实现“点击选中六边形”功能的核心。直接对q, r, y四舍五入再校正是标准且稳定的算法。我遇到过自己写的复杂判断逻辑在边界情况下出错的情况最终回归到这个经典算法才解决。3.3 立体坐标 ↔ 偏移坐标为了兼容基于数组的地图存储我们需要实现与偏移坐标的转换。public static CubeCoord OffsetToCube(int col, int row, HexMapConfig config, bool isOddRowOffset true) { // isOddRowOffset: true表示奇数行偏移false表示偶数行偏移对于尖顶地图 // 对于平顶地图通常使用奇数列偏移逻辑类似参数名可改为isOddColOffset。 // 这里以尖顶地图的奇数行偏移为例。 if (config.orientation HexOrientation.PointyTop) { int x col; int z row - (col - (col 1)) / 2; // 关键根据奇偶列调整 int y -x - z; return new CubeCoord(x, y, z); } else // FlatTop 假设使用奇数列偏移 { // 实现逻辑类似但调整的是行坐标 int q col; int r row - (col - (col 1)) / 2; return new CubeCoord(q, -q - r, r); } } public static (int col, int row) CubeToOffset(CubeCoord cube, HexMapConfig config, bool isOddRowOffset true) { if (config.orientation HexOrientation.PointyTop) { int col cube.x; int row cube.z (cube.x - (cube.x 1)) / 2; return (col, row); } else { int col cube.x; int row cube.z (cube.x - (cube.x 1)) / 2; return (col, row); } }注意事项偏移坐标的奇偶性规则必须与你的地图生成、寻路算法保持一致。我建议在项目初期就明确约定并使用一个全局配置避免混合使用不同规则导致难以调试的错位问题。3.4 邻居查找、距离计算与范围获取基于立体坐标这些操作变得异常简单。// 预定义的六个方向向量立体坐标 private static readonly CubeCoord[] CubeDirections new CubeCoord[] { 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), }; public static CubeCoord GetNeighbor(CubeCoord coord, int direction /*0-5*/) { if (direction 0 || direction 6) return coord; CubeCoord dir CubeDirections[direction]; return new CubeCoord(coord.x dir.x, coord.y dir.y, coord.z dir.z); } public static int CubeDistance(CubeCoord a, CubeCoord b) { return (Mathf.Abs(a.x - b.x) Mathf.Abs(a.y - b.y) Mathf.Abs(a.z - b.z)) / 2; } // 获取指定半径范围内的所有六边形坐标 public static ListCubeCoord GetHexesInRange(CubeCoord center, int range) { ListCubeCoord results new ListCubeCoord(); for (int dx -range; dx range; dx) { for (int dy Mathf.Max(-range, -dx-range); dy Mathf.Min(range, -dxrange); dy) { int dz -dx - dy; results.Add(new CubeCoord(center.x dx, center.y dy, center.z dz)); } } return results; }4. 在Unity项目中的集成与使用示例封装好了工具类如何在Unity项目中优雅地使用它呢4.1 创建可配置的HexGrid组件我们可以创建一个HexGridMonoBehaviour它挂载在空物体上负责管理整个六边形地图的逻辑坐标与视觉表现。using UnityEngine; using System.Collections.Generic; public class HexGrid : MonoBehaviour { public HexMapConfig mapConfig; public GameObject hexPrefab; // 六边形的预制体包含MeshRenderer等 public int gridRadius 5; // 生成地图的半径 private DictionaryCubeCoord, HexCell _cellDict new DictionaryCubeCoord, HexCell(); void Start() { GenerateGrid(); } void GenerateGrid() { _cellDict.Clear(); var hexes HexCoordinateSystem.GetHexesInRange(new CubeCoord(0,0,0), gridRadius); foreach(var cube in hexes) { Vector3 worldPos HexCoordinateSystem.CubeToWorld(cube, mapConfig); worldPos transform.position; // 考虑Grid GameObject的位置偏移 GameObject hexGo Instantiate(hexPrefab, worldPos, Quaternion.identity, this.transform); hexGo.name $Hex_{cube.x}_{cube.y}_{cube.z}; var cell hexGo.GetComponentHexCell(); if (cell null) cell hexGo.AddComponentHexCell(); cell.Initialize(cube, this); _cellDict[cube] cell; } } // 根据世界坐标如鼠标点击获取六边形单元格 public HexCell GetCellFromWorldPosition(Vector3 worldPosition) { CubeCoord cube HexCoordinateSystem.WorldToCube(worldPosition - transform.position, mapConfig); if (_cellDict.TryGetValue(cube, out HexCell cell)) { return cell; } return null; // 点击位置没有单元格 } // 其他实用方法寻路、高亮范围等... }4.2 实现HexCell与交互逻辑每个六边形实例可以挂载一个HexCell脚本用于存储逻辑状态和处理交互。public class HexCell : MonoBehaviour { public CubeCoord Coord { get; private set; } public HexGrid ParentGrid { get; private set; } // 游戏逻辑相关的属性 public int MovementCost 1; public bool IsWalkable true; public Unit OccupyingUnit null; private Material _originalMat; private MeshRenderer _meshRenderer; public void Initialize(CubeCoord coord, HexGrid grid) { this.Coord coord; this.ParentGrid grid; _meshRenderer GetComponentInChildrenMeshRenderer(); if (_meshRenderer ! null) _originalMat _meshRenderer.material; } void OnMouseEnter() { Highlight(true); } void OnMouseExit() { Highlight(false); } void OnMouseDown() { Debug.Log($Clicked on Hex: {Coord}); // 触发游戏事件如移动单位、显示信息等 // EventSystem.Instance?.Invoke(new HexClickedEvent { cell this }); } public void Highlight(bool enable) { if (_meshRenderer ! null) { // 简单的高亮方式更换材质或修改颜色 // 更复杂的可以Shader实现 _meshRenderer.material.color enable ? Color.yellow : Color.white; } } }4.3 实现A*寻路算法示例有了坐标系统和邻居查找实现六边形网格上的A*寻路就水到渠成了。using System.Collections.Generic; using UnityEngine; public static class HexPathfinding { public static ListCubeCoord FindPath(CubeCoord start, CubeCoord goal, HexGrid grid) { // 简易A*实现忽略地形代价等复杂因素 var openSet new PriorityQueueNode(); var cameFrom new DictionaryCubeCoord, CubeCoord(); var gScore new DictionaryCubeCoord, int(); var fScore new DictionaryCubeCoord, int(); gScore[start] 0; fScore[start] Heuristic(start, goal); openSet.Enqueue(new Node(start, fScore[start])); while (openSet.Count 0) { var current openSet.Dequeue().Coord; if (current.Equals(goal)) { return ReconstructPath(cameFrom, current); } for (int dir 0; dir 6; dir) { CubeCoord neighbor HexCoordinateSystem.GetNeighbor(current, dir); HexCell neighborCell grid.GetCellFromCube(neighbor); // 假设Grid有这个方法 if (neighborCell null || !neighborCell.IsWalkable) continue; int tentativeGScore gScore[current] neighborCell.MovementCost; if (!gScore.ContainsKey(neighbor) || tentativeGScore gScore[neighbor]) { cameFrom[neighbor] current; gScore[neighbor] tentativeGScore; fScore[neighbor] tentativeGScore Heuristic(neighbor, goal); if (!openSet.Contains(neighbor)) { openSet.Enqueue(new Node(neighbor, fScore[neighbor])); } } } } return null; // 未找到路径 } private static int Heuristic(CubeCoord a, CubeCoord b) { return HexCoordinateSystem.CubeDistance(a, b); } private static ListCubeCoord ReconstructPath(DictionaryCubeCoord, CubeCoord cameFrom, CubeCoord current) { var path new ListCubeCoord { current }; while (cameFrom.ContainsKey(current)) { current cameFrom[current]; path.Insert(0, current); } return path; } private class Node : System.IComparableNode { public CubeCoord Coord; public int FScore; public Node(CubeCoord coord, int fScore) { Coord coord; FScore fScore; } public int CompareTo(Node other) FScore.CompareTo(other.FScore); } // 简易优先队列实际项目建议使用更高效的实现如BinaryHeap private class PriorityQueueT where T : System.IComparableT { private ListT data new ListT(); public int Count data.Count; public void Enqueue(T item) { data.Add(item); data.Sort(); } public T Dequeue() { T item data[0]; data.RemoveAt(0); return item; } public bool Contains(T item) data.Contains(item); } }5. 性能优化与高级技巧当你的地图变得很大或者需要每帧进行大量坐标转换如大量单位的移动、范围检测时性能就变得重要了。5.1 缓存与预计算坐标转换结果缓存对于静态地图每个六边形的世界位置是固定的。可以在HexCell.Initialize()时计算并缓存WorldPosition避免每次访问都进行公式计算。邻居列表缓存同样每个六边形的六个邻居坐标也是固定的。可以在初始化时预计算并存储。距离矩阵预计算对于小范围、频繁使用的距离判断如技能释放范围可以预计算一个查找表。public class HexCell : MonoBehaviour { // ... 其他属性 private Vector3 _cachedWorldPos; private CubeCoord[] _cachedNeighbors; private DictionaryCubeCoord, int _cachedDistances; // 可选针对固定目标 public void Initialize(CubeCoord coord, HexGrid grid) { // ... 原有初始化 _cachedWorldPos HexCoordinateSystem.CubeToWorld(coord, grid.mapConfig); _cachedNeighbors new CubeCoord[6]; for(int i0; i6; i) _cachedNeighbors[i] HexCoordinateSystem.GetNeighbor(coord, i); } public Vector3 WorldPosition _cachedWorldPos; // 直接使用缓存 public CubeCoord GetNeighborCoord(int dir) _cachedNeighbors[dir]; // 快速获取 }5.2 使用Jobs System与Burst Compiler进行批量计算对于超大规模地图的生成、大规模单位移动的路径预测、战争迷雾计算等CPU密集型任务可以考虑使用Unity的C# Job System和Burst编译器。你可以将CubeCoord结构体定义为IJobParallelFor可用的形式然后并行处理成千上万个六边形的坐标转换或属性计算。using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; [BurstCompile] public struct HexCoordinateConversionJob : IJobParallelFor { [ReadOnly] public NativeArrayCubeCoord InputCoords; [WriteOnly] public NativeArrayfloat3 OutputPositions; public float HexSize; public int Orientation; // 0 for FlatTop, 1 for PointyTop public void Execute(int index) { CubeCoord coord InputCoords[index]; float3 pos; // 将转换公式用Burst兼容的数学函数重写... // 这是一个简化示例 if (Orientation 0) // FlatTop { pos.x HexSize * (1.5f * coord.x); pos.z HexSize * (math.sqrt(3f) * (coord.z coord.x * 0.5f)); // 注意Unity中Z轴常作为水平面纵轴 } else { pos.x HexSize * (math.sqrt(3f) * (coord.x coord.z * 0.5f)); pos.z HexSize * (1.5f * coord.z); } pos.y 0; OutputPositions[index] pos; } } // 在MonoBehaviour中调度Job void BatchConvertCoordinates() { var inputCoords new NativeArrayCubeCoord(allCoordsArray, Allocator.TempJob); var outputPositions new NativeArrayfloat3(allCoordsArray.Length, Allocator.TempJob); var job new HexCoordinateConversionJob { InputCoords inputCoords, OutputPositions outputPositions, HexSize mapConfig.hexSize, Orientation (int)mapConfig.orientation }; JobHandle handle job.Schedule(allCoordsArray.Length, 64); handle.Complete(); // 从outputPositions中读取结果... inputCoords.Dispose(); outputPositions.Dispose(); }注意事项使用Job System需要处理数据依赖和线程安全。CubeCoord需要是纯值类型blittable type。对于简单的int类型它是安全的。复杂的类对象无法直接传入Job。5.3 编辑器扩展在Scene视图可视化网格为了方便设计可以编写一个Editor脚本在Scene视图中绘制出六边形网格的Gizmos。#if UNITY_EDITOR using UnityEditor; using UnityEngine; [CustomEditor(typeof(HexGrid))] public class HexGridEditor : Editor { void OnSceneGUI() { HexGrid grid (HexGrid)target; if (grid.mapConfig null || grid.gridRadius 0) return; Handles.color Color.cyan; var hexes HexCoordinateSystem.GetHexesInRange(new CubeCoord(0,0,0), grid.gridRadius); foreach(var cube in hexes) { Vector3 center HexCoordinateSystem.CubeToWorld(cube, grid.mapConfig) grid.transform.position; DrawHexGizmo(center, grid.mapConfig); } } void DrawHexGizmo(Vector3 center, HexMapConfig config) { Vector3[] corners new Vector3[6]; float angleStep 60f * Mathf.Deg2Rad; float startAngle (config.orientation HexOrientation.FlatTop) ? 0f : 30f * Mathf.Deg2Rad; for (int i 0; i 6; i) { float angle startAngle angleStep * i; corners[i] center new Vector3( Mathf.Cos(angle) * config.hexSize, 0, Mathf.Sin(angle) * config.hexSize ); } for (int i 0; i 6; i) { Handles.DrawLine(corners[i], corners[(i 1) % 6]); } // 在中心画一个小点 Handles.DrawSolidDisc(center, Vector3.up, config.hexSize * 0.1f); } } #endif6. 常见问题排查与调试技巧在实际使用封装库的过程中你可能会遇到一些典型问题。这里记录了我踩过的坑和解决方法。6.1 坐标转换错位或偏移症状屏幕上的六边形位置不对或者点击位置获取的坐标不正确。排查步骤检查HexMapConfig首先确认hexSize是否与你的六边形预制体模型尺寸匹配。hexSize是外接圆半径通常是你建模时使用的参考尺寸。检查朝向确认orientation设置正确。平顶和尖顶的公式完全不同用错了会导致严重的拉伸或错位。检查奇偶偏移规则如果你使用了偏移坐标确保OffsetToCube和CubeToOffset函数中的奇偶性规则isOddRowOffset在整个项目中一致。地图生成、保存、加载、寻路都必须使用同一套规则。验证原点CubeToWorld返回的是相对于HexGrid组件原点的局部坐标。确保你在实例化或计算时正确加上了grid.transform.position。使用Debug Draw在Update或通过Editor脚本用Debug.DrawLine绘制出几个关键六边形的轮廓和中心点直观检查它们的世界位置是否正确。6.2 邻居查找或范围获取结果异常症状GetNeighbor返回的坐标看起来是错的或者GetHexesInRange得到的形状不是完美的六边形环。排查步骤验证立体坐标约束确保你使用的CubeCoord满足xyz0。在创建或转换坐标后可以添加一个调试断言Debug.Assert(cube.x cube.y cube.z 0)。检查方向向量确认CubeDirections数组的定义与你对方向0-5的认知一致。通常0代表“东”或“右上”但你可以自定义。最好在Gizmos中把六个方向用不同颜色的线画出来看看。范围算法边界GetHexesInRange中的双层循环边界条件Mathf.Max(-range, -dx-range)和Mathf.Min(range, -dxrange)是确保只生成满足dxdydz0且距离在range以内的坐标。如果结果不对可以手动计算几个边界坐标验证。6.3 性能问题症状地图很大时帧率下降特别是在生成网格或频繁进行坐标转换时。优化方向实施缓存如5.1节所述这是提升性能最直接有效的方法。减少不必要的计算例如只在单位移动或地图变化时重新计算路径和范围而不是每帧都计算。考虑空间分区对于超大规模地图如数万格以上可以使用空间数据结构如网格、四叉树来快速剔除无关的六边形而不是遍历所有单元格。评估Job System如果确实有批量计算需求且性能瓶颈在CPU可以考虑使用Job System进行多线程计算。6.4 与美术资源的对齐问题症状六边形的视觉模型Mesh与逻辑网格对不齐边缘有缝隙或重叠。解决方案统一原点确保你的六边形预制体的轴心点Pivot在模型的几何中心。在3D建模软件中导出时就要设置好。匹配尺寸hexSize必须等于你的六边形模型外接圆的半径。你可以在Unity中测量模型的尺寸来反推这个值。使用Shader或材质平铺对于无缝的地面纹理可以使用基于世界坐标的Shader来平铺而不是在每个六边形模型上单独贴图这样可以避免接缝问题。封装这个六边形坐标转换工具库的初衷就是把那些重复、枯燥且容易出错的数学计算从日常开发中剥离出去。经过多个项目的迭代现在的版本已经能稳定处理大多数常见需求。当你不再需要为“这个坐标该怎么算”而分心时就能更专注于构建那些真正让游戏变得有趣的规则和内容了。如果你在使用的过程中发现了新的边界情况或者有更好的优化思路非常欢迎一起交流。完整的项目代码我已经整理好你可以直接拿来作为你下一个六边形地图项目的基石。