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),与屏幕坐标的转换也有一套成熟的几何公式。以它为中枢,可以无歧义地转换到其他任何需要的坐标形式。
注意:虽然立体坐标用到了三个分量,但由于
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类是关键,它集中了所有影响坐标转换的物理参数。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); // 由于四舍五入可能导致 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); } } #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满足x+y+z=0。在创建或转换坐标后,可以添加一个调试断言Debug.Assert(cube.x + cube.y + cube.z == 0)。 - 检查方向向量:确认
CubeDirections数组的定义与你对方向(0-5)的认知一致。通常,0代表“东”或“右上”,但你可以自定义。最好在Gizmos中把六个方向用不同颜色的线画出来看看。 - 范围算法边界:
GetHexesInRange中的双层循环边界条件Mathf.Max(-range, -dx-range)和Mathf.Min(range, -dx+range)是确保只生成满足dx+dy+dz=0且距离在range以内的坐标。如果结果不对,可以手动计算几个边界坐标验证。
- 验证立体坐标约束:确保你使用的
6.3 性能问题
- 症状:地图很大时,帧率下降,特别是在生成网格或频繁进行坐标转换时。
- 优化方向:
- 实施缓存:如5.1节所述,这是提升性能最直接有效的方法。
- 减少不必要的计算:例如,只在单位移动或地图变化时重新计算路径和范围,而不是每帧都计算。
- 考虑空间分区:对于超大规模地图(如数万格以上),可以使用空间数据结构(如网格、四叉树)来快速剔除无关的六边形,而不是遍历所有单元格。
- 评估Job System:如果确实有批量计算需求,且性能瓶颈在CPU,可以考虑使用Job System进行多线程计算。
6.4 与美术资源的对齐问题
- 症状:六边形的视觉模型(Mesh)与逻辑网格对不齐,边缘有缝隙或重叠。
- 解决方案:
- 统一原点:确保你的六边形预制体的轴心点(Pivot)在模型的几何中心。在3D建模软件中导出时就要设置好。
- 匹配尺寸:
hexSize必须等于你的六边形模型外接圆的半径。你可以在Unity中测量模型的尺寸来反推这个值。 - 使用Shader或材质平铺:对于无缝的地面纹理,可以使用基于世界坐标的Shader来平铺,而不是在每个六边形模型上单独贴图,这样可以避免接缝问题。
封装这个六边形坐标转换工具库的初衷,就是把那些重复、枯燥且容易出错的数学计算从日常开发中剥离出去。经过多个项目的迭代,现在的版本已经能稳定处理大多数常见需求。当你不再需要为“这个坐标该怎么算”而分心时,就能更专注于构建那些真正让游戏变得有趣的规则和内容了。如果你在使用的过程中发现了新的边界情况,或者有更好的优化思路,非常欢迎一起交流。完整的项目代码我已经整理好,你可以直接拿来作为你下一个六边形地图项目的基石。