news 2026/7/9 21:47:28

Unity六边形网格坐标转换工具库:从原理到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity六边形网格坐标转换工具库:从原理到工程实践

1. 项目概述:告别坐标计算的“苦力活”

如果你正在开发一款策略游戏、模拟经营游戏,或者任何需要六边形网格(Hex Grid)地图的项目,那么“坐标转换”这个坎儿,你肯定绕不过去。从逻辑上的立体坐标(Cube Coordinates)或轴向坐标(Axial Coordinates),到屏幕上实际的像素或世界坐标,这中间的计算公式,网上教程一搜一大把。但每次新开一个项目,或者地图参数稍有变动,就得把这些公式重新推导、复制粘贴、调试一遍,是不是感觉像在重复造轮子,而且还是个容易出错的轮子?更别提还要处理六边形网格的奇偶偏移、不同朝向(平顶还是尖顶)带来的差异了。这个项目的核心,就是帮你把这些繁琐、易错但又至关重要的数学计算,封装成一个干净、高效、即拿即用的C#工具库。让你能把精力真正放在游戏逻辑和玩法设计上,而不是一遍又一遍地跟三角函数和偏移量较劲。

2. 六边形地图坐标系统深度解析

在动手封装之前,我们必须彻底理解六边形地图常用的几种坐标系统。这是所有转换逻辑的基石,理解透了,后面的封装才能知其然更知其所以然。

2.1 为什么是立体坐标(Cube Coordinates)?

你可能见过轴向坐标(Axial, 即q, r)、偏移坐标(Offset)等。但在这个封装里,我选择以立体坐标(x, y, z)作为核心的内部逻辑坐标。原因有三:

  1. 计算对称优雅:在立体坐标系中,一个六边形的坐标满足x + y + z = 0这个约束条件。这个特性使得距离计算、邻居查找等操作变得极其简单。例如,两个六边形a和b之间的“网格距离”,就是它们各坐标分量差值的绝对值之和除以2:(abs(dx) + abs(dy) + abs(dz)) / 2。这种简洁性是其他坐标系难以比拟的。
  2. 方向定义清晰:从中心六边形出发,到其六个邻居的方向向量可以整齐地定义为一个数组:CubeDirectionVectors = [ (+1, -1, 0), (+1, 0, -1), (0, +1, -1), (-1, +1, 0), (-1, 0, +1), (0, -1, +1) ]。遍历邻居只需简单向量加法。
  3. 易于与其他系统互转:立体坐标与轴向坐标(q, r)的转换是线性的(通常令x = q, z = r, y = -x-z),与屏幕坐标的转换也有一套成熟的几何公式。以它为中枢,可以无歧义地转换到其他任何需要的坐标形式。

注意:虽然立体坐标用到了三个分量,但由于x+y+z=0的约束,它本质上仍然是二维的。你可以把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类是关键,它集中了所有影响坐标转换的物理参数。hexWidthhexHeight是计算属性,根据朝向自动给出正确的值。horizontalSpacingverticalSpacing允许你定义非正方形的网格间距,这在某些艺术风格或特殊布局中很有用。

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); // 由于四舍五入可能导致 x+y+z != 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 List<CubeCoord> GetHexesInRange(CubeCoord center, int range) { List<CubeCoord> results = new List<CubeCoord>(); for (int dx = -range; dx <= range; dx++) { for (int dy = Mathf.Max(-range, -dx-range); dy <= Mathf.Min(range, -dx+range); 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 Dictionary<CubeCoord, HexCell> _cellDict = new Dictionary<CubeCoord, 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.GetComponent<HexCell>(); if (cell == null) cell = hexGo.AddComponent<HexCell>(); 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 = GetComponentInChildren<MeshRenderer>(); 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 List<CubeCoord> FindPath(CubeCoord start, CubeCoord goal, HexGrid grid) { // 简易A*实现,忽略地形代价等复杂因素 var openSet = new PriorityQueue<Node>(); var cameFrom = new Dictionary<CubeCoord, CubeCoord>(); var gScore = new Dictionary<CubeCoord, int>(); var fScore = new Dictionary<CubeCoord, 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 List<CubeCoord> ReconstructPath(Dictionary<CubeCoord, CubeCoord> cameFrom, CubeCoord current) { var path = new List<CubeCoord> { current }; while (cameFrom.ContainsKey(current)) { current = cameFrom[current]; path.Insert(0, current); } return path; } private class Node : System.IComparable<Node> { 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 PriorityQueue<T> where T : System.IComparable<T> { private List<T> data = new List<T>(); 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 Dictionary<CubeCoord, int> _cachedDistances; // 可选,针对固定目标 public void Initialize(CubeCoord coord, HexGrid grid) { // ... 原有初始化 _cachedWorldPos = HexCoordinateSystem.CubeToWorld(coord, grid.mapConfig); _cachedNeighbors = new CubeCoord[6]; for(int i=0; i<6; 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 NativeArray<CubeCoord> InputCoords; [WriteOnly] public NativeArray<float3> 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 NativeArray<CubeCoord>(allCoordsArray, Allocator.TempJob); var outputPositions = new NativeArray<float3>(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); } } #endif

6. 常见问题排查与调试技巧

在实际使用封装库的过程中,你可能会遇到一些典型问题。这里记录了我踩过的坑和解决方法。

6.1 坐标转换错位或偏移

  • 症状:屏幕上的六边形位置不对,或者点击位置获取的坐标不正确。
  • 排查步骤
    1. 检查HexMapConfig:首先确认hexSize是否与你的六边形预制体模型尺寸匹配。hexSize是外接圆半径,通常是你建模时使用的参考尺寸。
    2. 检查朝向:确认orientation设置正确。平顶和尖顶的公式完全不同,用错了会导致严重的拉伸或错位。
    3. 检查奇偶偏移规则:如果你使用了偏移坐标,确保OffsetToCubeCubeToOffset函数中的奇偶性规则(isOddRowOffset)在整个项目中一致。地图生成、保存、加载、寻路都必须使用同一套规则。
    4. 验证原点CubeToWorld返回的是相对于HexGrid组件原点的局部坐标。确保你在实例化或计算时,正确加上了grid.transform.position
    5. 使用Debug Draw:在Update或通过Editor脚本,用Debug.DrawLine绘制出几个关键六边形的轮廓和中心点,直观检查它们的世界位置是否正确。

6.2 邻居查找或范围获取结果异常

  • 症状GetNeighbor返回的坐标看起来是错的,或者GetHexesInRange得到的形状不是完美的六边形环。
  • 排查步骤
    1. 验证立体坐标约束:确保你使用的CubeCoord满足x+y+z=0。在创建或转换坐标后,可以添加一个调试断言Debug.Assert(cube.x + cube.y + cube.z == 0)
    2. 检查方向向量:确认CubeDirections数组的定义与你对方向(0-5)的认知一致。通常,0代表“东”或“右上”,但你可以自定义。最好在Gizmos中把六个方向用不同颜色的线画出来看看。
    3. 范围算法边界GetHexesInRange中的双层循环边界条件Mathf.Max(-range, -dx-range)Mathf.Min(range, -dx+range)是确保只生成满足dx+dy+dz=0且距离在range以内的坐标。如果结果不对,可以手动计算几个边界坐标验证。

6.3 性能问题

  • 症状:地图很大时,帧率下降,特别是在生成网格或频繁进行坐标转换时。
  • 优化方向
    1. 实施缓存:如5.1节所述,这是提升性能最直接有效的方法。
    2. 减少不必要的计算:例如,只在单位移动或地图变化时重新计算路径和范围,而不是每帧都计算。
    3. 考虑空间分区:对于超大规模地图(如数万格以上),可以使用空间数据结构(如网格、四叉树)来快速剔除无关的六边形,而不是遍历所有单元格。
    4. 评估Job System:如果确实有批量计算需求,且性能瓶颈在CPU,可以考虑使用Job System进行多线程计算。

6.4 与美术资源的对齐问题

  • 症状:六边形的视觉模型(Mesh)与逻辑网格对不齐,边缘有缝隙或重叠。
  • 解决方案
    1. 统一原点:确保你的六边形预制体的轴心点(Pivot)在模型的几何中心。在3D建模软件中导出时就要设置好。
    2. 匹配尺寸hexSize必须等于你的六边形模型外接圆的半径。你可以在Unity中测量模型的尺寸来反推这个值。
    3. 使用Shader或材质平铺:对于无缝的地面纹理,可以使用基于世界坐标的Shader来平铺,而不是在每个六边形模型上单独贴图,这样可以避免接缝问题。

封装这个六边形坐标转换工具库的初衷,就是把那些重复、枯燥且容易出错的数学计算从日常开发中剥离出去。经过多个项目的迭代,现在的版本已经能稳定处理大多数常见需求。当你不再需要为“这个坐标该怎么算”而分心时,就能更专注于构建那些真正让游戏变得有趣的规则和内容了。如果你在使用的过程中发现了新的边界情况,或者有更好的优化思路,非常欢迎一起交流。完整的项目代码我已经整理好,你可以直接拿来作为你下一个六边形地图项目的基石。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 21:45:44

AI驱动的配置漂移检测:用机器学习识别K8s集群中的非预期配置变更

AI驱动的配置漂移检测&#xff1a;用机器学习识别K8s集群中的非预期配置变更 一、配置漂移的定义与运维风险 配置漂移&#xff08;Configuration Drift&#xff09;是指集群或系统的实际运行配置偏离了预期基准状态的现象。在Kubernetes集群中&#xff0c;配置漂移的表现形式…

作者头像 李华
网站建设 2026/7/9 21:44:38

Hermes Agent:轻量级本地AI Agent运行时框架解析

1. Hermes Agent 是什么&#xff1a;从热词迷雾中看清它的技术轮廓最近在多个开发者社区、低代码平台讨论区和本地大模型部署圈子&#xff0c;频繁刷到“Hermes Agent”这个词——它不像 Dify、Ollama 或 Llama.cpp 那样有清晰的官方文档站或 GitHub star 数量背书&#xff0c;…

作者头像 李华
网站建设 2026/7/9 21:44:27

锂电池主动均衡系统设计与I2C通信实现

1. 项目背景与核心需求在锂电池组应用中&#xff0c;单体电池之间的电压差异是影响整体性能和寿命的关键因素。当多个电池串联使用时&#xff0c;由于制造工艺、温度分布和使用状态的差异&#xff0c;各单体电池的电压会逐渐出现不平衡。这种不平衡会导致充电时部分电池过充、放…

作者头像 李华
网站建设 2026/7/9 21:43:51

Pulsar原生多租户架构:Java云原生消息系统的租户隔离实践

1. 项目概述&#xff1a;为什么Pulsar正在成为Java云原生多租户消息系统的事实标准你是不是也经历过这样的场景&#xff1a;团队用Kafka搭了一套订单通知系统&#xff0c;刚上线三个月&#xff0c;运营突然提出要给新接入的三家大客户做数据隔离——不是简单加个前缀&#xff0…

作者头像 李华
网站建设 2026/7/9 21:42:01

Claude Code 国内使用指南:从API接入到IDE集成的完整实战

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 Claude Code 是一个专注于代码生成、理解和调试的 AI 模型&#xff0c;由 Anthropic 公司开发。它并非一个独立的桌面应用&#xff0c…

作者头像 李华