Unity六边形网格开发指南:从坐标转换到A*寻路实战

📅 2026/7/15 14:53:28
Unity六边形网格开发指南:从坐标转换到A*寻路实战
1. 项目概述为什么你需要关注 UnityHexGrid如果你正在 Unity 里捣鼓策略游戏、模拟经营、战棋或者任何需要一张规整地图的项目那么“六边形网格”Hex Grid这个概念你肯定绕不过去。最近我在 Asset Store 上翻箱倒柜想找一个轻量、免费且能快速上手的六边形网格解决方案结果还真让我找到了一个宝藏——由 Daniel Carrier 开发的UnityHexGrid。这玩意儿在商店里直接标着“FREE”下载量和使用评价都相当不错。我自己上手折腾了几个项目从原型搭建到功能扩展感觉它确实解决了从零手搓 Hex Grid 的不少痛点。今天这篇分享我就来给你彻底拆解一下这个项目聊聊它到底能干什么、怎么用以及我在实际项目中踩过的那些坑和总结出来的技巧。简单来说UnityHexGrid 是一个专注于六边形网格地图生成与编辑的框架。它不是一个完整的游戏而是一套工具和脚本的集合帮你处理六边形地图中最核心也最繁琐的那些数学计算和数据结构问题比如坐标转换立方体坐标、偏移坐标、世界坐标之间的互转、邻居查找、路径搜索A*算法以及网格编辑功能。对于独立开发者和小团队而言能省下大量重复造轮子的时间让你能把精力集中在游戏玩法本身。2. 核心功能与设计思路拆解2.1 六边形网格的核心优势与挑战在深入 UnityHexGrid 之前我们得先明白为什么游戏开发者对六边形网格情有独钟。相比于正方形网格六边形网格最大的优势在于距离一致性和移动公平性。在正方形网格中从一个格子中心到相邻格子中心对角线方向的距离约1.414倍边长要大于上下左右方向的距离1倍边长。这会导致在计算移动范围、攻击范围或路径时斜向移动“占便宜”。而六边形网格中每个相邻格子的中心距离都是完全相等的这使得距离计算、范围搜索和路径规划变得极其自然和公平特别适合战棋、策略和模拟类游戏。然而实现一个健壮的六边形网格系统挑战不小。首要难题就是坐标系统。我们熟悉的二维笛卡尔坐标系x, y并不直观。业内普遍采用三种坐标模型立方体坐标 (Cube Coordinates) 用一个三维向量 (x, y, z) 来表示但约束条件是 x y z 0。这是进行六边形向量运算如距离计算、邻居查找最数学优雅的方式。轴向坐标 (Axial Coordinates) 从立方体坐标中去掉 z 轴用 (q, r) 表示因为 z -x - y 可以推导出来。这是存储和显示时更紧凑的形式。偏移坐标 (Offset Coordinates) 尝试将六边形强行“拍扁”成类似二维数组的行列形式分为“奇数行偏移”和“偶数行偏移”等。这种方式虽然便于用二维数组存储但在进行邻居计算和距离判断时逻辑会变得复杂容易出错。UnityHexGrid 的设计聪明之处在于它在内部很可能采用了轴向坐标或立方体坐标作为核心数据模型以保证数学运算的正确性同时对外提供了与 Unity 世界坐标World Position和易于理解的偏移坐标如数组索引之间进行转换的接口。这样你既可以用你熟悉的grid[x, y]方式来存取数据底层又能保证所有六边形操作的几何正确性。2.2 UnityHexGrid 的架构与核心组件根据在 Asset Store 上的描述和实际使用体验UnityHexGrid 的架构可以拆解为以下几个核心部分HexGrid 核心管理器 这是整个系统的大脑。它负责管理整个网格的尺寸、布局平顶六边形还是尖顶六边形、六边形尺寸半径等全局参数。通常你需要创建一个空的 GameObject挂上这个脚本并进行基础配置。HexCell 单元数据 代表网格中的每一个六边形格子。它至少会包含该格子的坐标信息可能是多种坐标形式的封装并且可以扩展存储游戏逻辑数据例如地形类型、通行成本、所属势力、资源数量等。UnityHexGrid 通常会提供一个基础的HexCell类或结构体你需要继承或组合它来添加自己的游戏数据。坐标转换工具集 (HexCoordinates) 这是一系列静态方法或工具类是项目的精华所在。它封装了所有令人头疼的坐标转换数学WorldPositionToHexCoord(Vector3 worldPos): 将鼠标点击或单位所在的 Unity 世界坐标转换为对应的六边形网格坐标。HexCoordToWorldPosition(HexCoord coord): 将六边形坐标转换回世界坐标用于放置建筑、单位或高亮显示。GetDistance(HexCoord a, HexCoord b): 计算两个六边形格子之间的“步数”距离。GetNeighbors(HexCoord coord): 获取指定格子的所有六个邻居的坐标。网格生成与可视化 这部分负责在场景中实际创建和渲染网格。可能是通过代码动态生成 Mesh也可能是预置了六边形的预制体Prefab并按规律实例化。可视化部分通常与逻辑层分离方便你更换美术资源。路径查找系统 (APathfinding)* 许多 Hex Grid 框架会集成一个 A* 算法实现它利用GetNeighbors和每个HexCell上定义的通行成本Cost来计算最优路径。这是策略游戏移动和 AI 寻路的基石。编辑器扩展 (Optional) 高级的框架会提供自定义的 Inspector 面板或在 Scene 视图中进行网格编辑的工具比如刷地形、设置属性等能极大提升开发效率。注意 由于 UnityHexGrid 是免费资源其功能深度可能不及一些付费的完整解决方案如 Hexagonal Grids Pro。它更侧重于提供稳定可靠的核心算法和数据结构高级的编辑器工具和可视化效果可能需要你自己基于它进行二次开发。3. 快速上手从零搭建你的第一个六边形地图理论说再多不如动手做一遍。下面我就带你走一遍使用 UnityHexGrid 创建基础地图的典型流程。这里假设你已经从 Unity Asset Store 下载并导入了 UnityHexGrid 包。3.1 环境准备与基础设置首先在 Unity 中创建一个新场景。然后我们需要设置网格的基本参数。创建网格控制器 在场景中创建一个空的 GameObject命名为 “HexGridManager”。将 HexGrid 核心脚本具体类名需查看导入的包可能是HexGrid,HexMap等挂载上去。配置网格参数 在 Inspector 面板中你会看到类似以下的参数需要配置Grid Width / Height 网格的宽度和高度以六边形格子数量计。例如设置 Width10, Height10就是一个10x10的网格。Hex Radius / Size 六边形外接圆或内切圆的半径取决于具体实现这决定了每个格子的大小。通常Hex Radius指中心到顶点的距离。你可以先设为 1。Orientation 六边形的朝向是Pointy Top尖顶朝上还是Flat Top平顶朝上。这会影响坐标计算和视觉表现。战棋类游戏常用 Pointy Top因为移动方向更符合上下左右的概念而某些策略地图可能用 Flat Top。根据你的游戏类型选择一旦确定后续所有逻辑都要与之匹配。Hex Prefab 用于可视化每个格子的预制体。你需要先创建一个六边形面的预制体比如一个 Plane 缩放成六边形或者一个专门的六边形 Mesh然后拖拽到这里。3.2 生成与可视化网格配置好参数后通常需要通过代码或在编辑器中点击一个按钮来生成网格。查看 HexGrid 脚本它很可能有一个GenerateGrid()或Awake/Start中自动生成的方法。// 假设 HexGrid 脚本有一个公共方法 public class HexGrid : MonoBehaviour { public int width 10; public int height 10; public float hexRadius 1f; public GameObject hexPrefab; private HexCell[,] cells; // 内部存储的单元格数组 void Start() { GenerateGrid(); } public void GenerateGrid() { cells new HexCell[width, height]; for (int z 0; z height; z) { for (int x 0; x width; x) { // 1. 计算世界坐标 Vector3 worldPos CalculateWorldPosition(x, z); // 2. 实例化视觉预制体 GameObject hexGo Instantiate(hexPrefab, worldPos, Quaternion.identity, this.transform); hexGo.name $Hex_{x}_{z}; // 3. 创建并初始化 HexCell 数据关联视觉对象 cells[x, z] new HexCell(new HexCoordinates(x, z), worldPos, hexGo); // 4. 可以将 HexCell 引用传递给视觉对象上的某个脚本用于交互 HexDisplay display hexGo.GetComponentHexDisplay(); if (display ! null) display.LinkCell(cells[x, z]); } } } private Vector3 CalculateWorldPosition(int x, int z) { // 这是关键根据朝向和半径计算位置 float xPos, zPos; bool isPointyTop (orientation Orientation.PointyTop); float horizDist, vertDist; if (isPointyTop) { horizDist hexRadius * Mathf.Sqrt(3f); // 水平方向间距 vertDist hexRadius * 1.5f; // 垂直方向间距 xPos x * horizDist; zPos z * vertDist; // 偶数行或奇数行偏移以实现交错效果 if (z % 2 1) xPos horizDist / 2f; } else // Flat Top { horizDist hexRadius * 1.5f; vertDist hexRadius * Mathf.Sqrt(3f); xPos x * horizDist; zPos z * vertDist; if (x % 2 1) zPos vertDist / 2f; } return new Vector3(xPos, 0, zPos); // Y轴可用于高度 } }运行场景你应该能看到一个规整的六边形网格出现在场景中。这是第一步也是最基础的一步。3.3 实现鼠标交互与坐标转换地图生成了接下来要让它能“点”。我们需要实现点击某个六边形格子获取其逻辑坐标。为每个六边形预制体添加碰撞器 在 Hex Prefab 上添加一个Mesh Collider如果使用自定义 Mesh或Box Collider调整大小以适应六边形。这是射线检测的基础。编写交互脚本 在 HexGridManager 或一个单独的控制器上编写鼠标点击检测代码。public class HexMapInteraction : MonoBehaviour { public HexGrid hexGrid; // 引用你的 HexGrid 组件 public Camera mainCamera; void Update() { if (Input.GetMouseButtonDown(0)) // 左键点击 { Ray ray mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { GameObject hitObject hit.collider.gameObject; // 方法1通过视觉对象反查逻辑单元如果之前关联了的话 HexDisplay display hitObject.GetComponentHexDisplay(); if (display ! null display.LinkedCell ! null) { Debug.Log($点击了格子坐标 {display.LinkedCell.Coordinates}); // 在这里处理点击逻辑例如高亮、移动单位等 HighlightHex(display.LinkedCell); } // 方法2更通用的通过世界坐标转换 Vector3 hitPoint hit.point; HexCoordinates hexCoord hexGrid.WorldPositionToHexCoord(hitPoint); Debug.Log($点击世界位置 {hitPoint} 对应的六边形坐标{hexCoord}); HexCell cell hexGrid.GetCellAt(hexCoord); if (cell ! null) { // 处理这个 cell } } } } void HighlightHex(HexCell cell) { // 简单的颜色变化示例 Renderer rend cell.VisualComponent.GetComponentRenderer(); if (rend ! null) { rend.material.color Color.yellow; // 注意最好用一个单独的高亮材质或Shader而不是直接改颜色避免状态混乱。 } } }这里的关键是hexGrid.WorldPositionToHexCoord(hitPoint)方法。它封装了将三维射线碰撞点逆向计算回六边形网格坐标的复杂数学。UnityHexGrid 的价值在这里就体现出来了——你不需要自己去推导那个反函数公式。4. 核心功能扩展与实战应用有了基础网格和交互我们就可以在此基础上构建游戏功能了。下面介绍几个最常用的扩展方向。4.1 为格子添加游戏逻辑数据基础的HexCell类可能只包含坐标和世界位置。你需要扩展它来存储游戏状态。[System.Serializable] public class GameHexCell : HexCell // 假设继承自框架的基类 { // 扩展的游戏逻辑属性 public TerrainType Terrain { get; set; } public int MovementCost { get; set; } // 通行成本用于寻路 public bool IsPassable { get; set; } true; public Unit OccupyingUnit { get; set; } // 当前格子的单位 public Building ConstructedBuilding { get; set; } // 建筑 public ResourceType Resource { get; set; } public int ResourceAmount { get; set; } public GameHexCell(HexCoordinates coords, Vector3 worldPos, GameObject visual) : base(coords, worldPos, visual) { // 初始化默认值 Terrain TerrainType.Plains; MovementCost 1; } }然后在HexGrid.GenerateGrid()中实例化你自己的GameHexCell。你还可以创建一个ScriptableObject来定义不同地形如草地、森林、山地、水域的属性模板成本、是否可通行、视觉表现等在生成网格或编辑时应用。4.2 实现网格寻路A*算法移动是策略游戏的核心。虽然 UnityHexGrid 可能自带或推荐了寻路实现但理解其原理至关重要。下面是一个基于此框架的简化 A* 实现思路定义节点 每个GameHexCell就是一个寻路节点。成本计算 从格子 A 移动到相邻格子 B 的成本通常是 B 格子的MovementCost。如果 B 不可通行 (IsPassable false)则成本为无穷大。启发函数 在六边形网格中使用HexCoordinates.GetDistance(A, B)作为启发式距离H值这是非常准确且高效的。算法实现public class HexPathfinder { public static ListHexCell FindPath(HexGrid grid, HexCell start, HexCell target) { // 优先队列可用SortedList, ListSort或第三方库模拟 var openSet new ListHexCell { start }; var cameFrom new DictionaryHexCell, HexCell(); var gScore new DictionaryHexCell, float(); var fScore new DictionaryHexCell, float(); foreach (var cell in grid.AllCells) { gScore[cell] float.MaxValue; fScore[cell] float.MaxValue; } gScore[start] 0; fScore[start] HexCoordinates.GetDistance(start.Coordinates, target.Coordinates); while (openSet.Count 0) { // 取出fScore最小的节点 var current openSet.OrderBy(c fScore[c]).First(); if (current target) { return ReconstructPath(cameFrom, current); } openSet.Remove(current); foreach (var neighborCoord in current.Coordinates.GetNeighbors()) { var neighbor grid.GetCellAt(neighborCoord); if (neighbor null || !neighbor.IsPassable) continue; // 使用扩展的属性 // 计算从起点到neighbor的临时gScore float tentativeGScore gScore[current] neighbor.MovementCost; // 使用neighbor的成本 if (tentativeGScore gScore[neighbor]) { cameFrom[neighbor] current; gScore[neighbor] tentativeGScore; fScore[neighbor] tentativeGScore HexCoordinates.GetDistance(neighbor.Coordinates, target.Coordinates); if (!openSet.Contains(neighbor)) { openSet.Add(neighbor); } } } } // 开放集为空未找到路径 return null; } private static ListHexCell ReconstructPath(DictionaryHexCell, HexCell cameFrom, HexCell current) { var path new ListHexCell { current }; while (cameFrom.ContainsKey(current)) { current cameFrom[current]; path.Insert(0, current); } return path; } }这个寻路器可以集成到你的HexGrid或一个独立的PathfindingSystem中。调用FindPath即可获得从起点到终点的格子列表然后让你的单位按顺序移动过去。4.3 范围搜索与区域高亮除了寻路策略游戏中经常需要显示移动范围、攻击范围或技能影响范围。这本质上是一种基于距离或特定规则的广度优先搜索BFS。public class RangeFinder { public static HashSetHexCell GetCellsInRange(HexGrid grid, HexCell centerCell, int range, bool ignoreObstacles false) { var visited new HashSetHexCell { centerCell }; // 使用元组记录格子和剩余步数 var frontier new Queue(HexCell cell, int stepsLeft)(); frontier.Enqueue((centerCell, range)); while (frontier.Count 0) { var (current, steps) frontier.Dequeue(); if (steps 0) continue; foreach (var neighborCoord in current.Coordinates.GetNeighbors()) { var neighbor grid.GetCellAt(neighborCoord); if (neighbor null || visited.Contains(neighbor)) continue; // 如果不忽略障碍且格子不可通行则不能穿过 if (!ignoreObstacles !neighbor.IsPassable) continue; visited.Add(neighbor); frontier.Enqueue((neighbor, steps - 1)); } } visited.Remove(centerCell); // 通常不包含中心自身 return visited; } }你可以用这个函数获取所有在指定步数内可达的格子然后遍历它们改变其视觉表现如变为半透明的蓝色来高亮显示移动范围。5. 性能优化与高级技巧当网格变大如100x100单位变多时性能问题就会浮现。下面分享几个优化点对象池管理视觉单元 不要频繁地Instantiate和Destroy六边形的高亮或特效。使用对象池来复用 GameObject。减少每帧的射线检测 鼠标交互的射线检测放在Update里没问题但对于AI决策或频繁的范围计算要避免每帧对大量格子进行物理检测。尽量使用逻辑坐标进行计算。寻路优化缓存 对于静态地形可以预计算每个格子的邻居列表和基础移动成本。分层寻路 对于大地图可以先在由多个格子组成的“区块”层级进行粗寻路再在目标区块内进行精细寻路。使用更高效的数据结构 上述示例中的openSet.OrderBy(...).First()效率很低。应使用真正的优先队列如PriorityQueue.NET 6或自己实现一个二叉堆。Shader 实现高亮与效果 用 MaterialPropertyBlock 来动态改变大量格子的颜色而不是为每个格子创建新的 Material 实例。更好的办法是编写一个自定义 Shader通过传递格子坐标和状态信息如是否高亮、地形类型到 GPU由 Shader 统一处理颜色和效果这是性能最高的方式。序列化与地图编辑 如果你需要设计复杂的地图考虑开发一个简单的编辑器。扩展 Unity Editor在 Scene 视图中通过画笔工具来绘制地形、设置属性并将最终的地图数据一个二维数组的格子信息保存为 ScriptableObject 或 JSON 文件在游戏运行时加载。6. 常见问题与排查实录在实际使用 UnityHexGrid 或自研类似系统时你肯定会遇到一些坑。以下是我总结的几个典型问题问题1六边形格子错位或间距不对现象 生成的网格看起来歪歪扭扭格子之间有空隙或重叠。排查首先检查Hex Radius参数。这个“半径”具体指什么是中心到顶点的距离外接圆半径还是中心到边的垂直距离内切圆半径不同的公式计算位置时用的系数不同。仔细阅读框架文档或代码注释确认其定义。检查CalculateWorldPosition函数中的计算公式特别是Mathf.Sqrt(3f)和1.5f这些系数。对于 Pointy Top水平间距是sqrt(3) * radius垂直间距是1.5 * radius。对于 Flat Top 则相反。确认交错偏移的逻辑是否正确。if (z % 2 1)还是if (x % 2 1)这取决于你的坐标循环顺序和朝向。问题2鼠标点击坐标转换不准确现象 点击格子边缘或角落时识别到了错误的格子。排查确保用于射线检测的碰撞器Collider大小和形状与视觉网格完全匹配。如果视觉网格是复杂的六边形使用MeshCollider比BoxCollider更精确。检查WorldPositionToHexCoord函数的实现。这是整个交互的基石。一个健壮的实现通常会先将世界坐标转换到“六边形空间”的轴向坐标再进行四舍五入取整。你可以写一个测试脚本在场景中摆放一些测试点打印出转换前后的坐标验证其正确性。考虑给点击添加一个微小的容差epsilon或者使用格子中心点进行二次判断。问题3寻路速度慢卡顿现象 地图稍大一点单位寻路时就有明显帧率下降。排查与解决诊断 使用 Unity Profiler 查看 CPU 耗时定位是FindPath函数本身慢还是其中调用的GetNeighbors、GetDistance或成本计算函数慢。优化数据结构 如前所述将openSet.OrderBy(...).First()替换为优先队列。减少搜索范围 如果游戏允许可以设置一个最大寻路距离超出则直接返回“不可达”。异步寻路 将寻路计算放到另一个线程或使用Unity.Jobs和Burst Compiler对于超大规模网格进行加速。注意这需要将你的网格数据转换为可并行的结构。预计算 对于完全静态的地形可以预计算所有格子之间的最短路径Floyd-Warshall 算法但这只适用于小地图因为空间复杂度是 O(n²)。问题4扩展数据后保存/加载地图出错现象 自己为HexCell添加了很多字段但在序列化如用 JsonUtility.ToJson或保存时数据丢失或报错。解决确保你扩展的数据类[System.Serializable]并且其所有字段都是可序列化的类型基本类型、可序列化类、数组等。避免使用委托、接口等。设计一个轻量级的、只包含必要数据的HexCellData结构体用于序列化。在保存时将GameHexCell转换成HexCellData数组加载时再根据数据重建GameHexCell。考虑使用ScriptableObject来存储地形等静态配置而每个格子的动态数据如单位引用可能需要用唯一ID来间接引用并在加载后重新建立关联。问题5网格编辑效率低下现象 在 Unity Editor 里手动摆放格子或设置属性非常耗时。解决学习编写简单的 Editor 脚本。你可以创建一个自定义 Inspector 窗口提供画笔、填充、选择地形类型等功能批量修改格子属性。利用Handles和Gizmos在 Scene 视图绘制交互式工具。例如点击一个格子然后在 Inspector 中选择一种地形点击“应用”按钮将该地形应用到所有选中的格子上。将编辑好的地图导出为配置文件这样可以在不同场景和构建版本中复用。最后关于 UnityHexGrid 这个具体资源由于是免费包它的文档和支持可能有限。遇到问题时最有效的方法是直接阅读其源代码理解作者的实现思路。通常这些核心算法坐标转换、邻居查找的代码并不长但非常精妙。通过阅读和学习你不仅能解决问题还能深刻理解六边形网格的数学原理未来即使脱离这个框架也能自己实现所需的功能。这或许才是使用这类开源或免费资源最大的收获——站在别人的肩膀上看清前路然后走得更远。