2026/8/6 5:54:31

Godot 4.2实战:A*算法与六边形网格寻路系统开发指南

Godot 4.2实战:A*算法与六边形网格寻路系统开发指南 1. 项目概述当A*算法遇上六边形世界在游戏开发里寻路是个老生常谈但又绕不开的核心问题。无论是RTS里的小兵冲锋还是RPG里的角色移动一个高效、智能的寻路系统直接决定了玩家的游戏体验。我们熟知的A*A-Star算法以其在网格地图上的优异表现成为了游戏开发者的首选工具之一。但当你把目光投向《文明》、《英雄无敌》这类策略游戏时会发现它们的地图并非由常见的正方形网格构成而是一个个紧密排列的六边形。这种六边形网格Hex Grid在策略游戏中备受青睐因为它让移动方向从正方形的4个或8个增加到了6个使得对角移动和距离计算更加自然和公平没有正方形网格中“走对角线更快”的尴尬。那么在Godot 4.2这个日益流行的开源游戏引擎中如何利用其内置的AStar2D类为这样一个六边形世界打造一套既高效又能体现策略深度的寻路系统呢答案就在于“自定义权重”。这不仅仅是简单地将正方形网格的A算法套用到六边形上更需要我们深入理解六边形网格的坐标表示、邻居关系并巧妙地将游戏规则——比如不同地形平原、森林、山脉、河流对移动力的不同消耗——转化为A算法中的“代价”Cost或“权重”Weight。本文将带你从零开始在Godot 4.2中实战构建一套《文明》风格的六边形网格寻路系统。我们会从最基础的六边形网格坐标系讲起一步步实现网格的生成与可视化然后深入AStar2D的核心为其注入自定义的地形权重逻辑。最终你将得到一个可以动态计算最优路径、并能直观显示地形消耗的完整解决方案。无论你是刚接触Godot的新手还是想为你的策略游戏寻找更优寻路方案的老兵相信这篇详尽的实战指南都能给你带来直接的帮助。2. 核心思路与六边形网格基础2.1 为什么是六边形坐标系的选择是关键在正方形网格中一个点的邻居很容易定义上下左右四个四方向或者再加上四个对角线方向八方向。但六边形每个单元格有六个邻居这带来了第一个挑战我们该如何在代码中表示一个六边形的位置常见的六边形网格坐标系主要有三种偏移坐标Offset Coordinates、立方体坐标Cube Coordinates和轴向坐标Axial Coordinates。为了与Godot的2D平面和AStar2D类更好地结合我们选择轴向坐标Axial Coordinates。它非常直观只需要两个轴q, r就能唯一确定一个六边形。你可以把六边形网格想象成一个倾斜的网格。q轴指向水平右侧r轴指向右下方或右上方取决于你的网格方向。每个六边形由其q, r值对表示。这种表示法的最大优点是计算两个六边形之间的距离、寻找邻居都变得非常规整和简单。邻居方向向量在轴向坐标系中一个六边形的六个邻居方向是固定的。我们定义一个方向数组const HEX_DIRECTIONS [ Vector2i(1, 0), Vector2i(1, -1), Vector2i(0, -1), Vector2i(-1, 0), Vector2i(-1, 1), Vector2i(0, 1) ]给定一个六边形坐标hex Vector2i(q, r)它的所有邻居坐标就是hex dir其中dir遍历HEX_DIRECTIONS数组。这个简单的规则是后续所有寻路逻辑的基石。坐标转换我们的游戏场景最终需要将六边形的逻辑坐标q, r转换为屏幕上的像素坐标x, y进行绘制。这里涉及一点几何计算。假设六边形的边长从中心到顶点的距离为size那么转换公式大致如下func axial_to_pixel(hex: Vector2i, size: float) - Vector2: var q hex.x var r hex.y var x size * (sqrt(3.0) * q sqrt(3.0)/2.0 * r) var y size * (3.0/2.0 * r) return Vector2(x, y)这个公式确保了六边形能够紧密、无重叠地排列在屏幕上。在实际操作中你可能需要根据网格的朝向“平顶”还是“尖顶”微调这个公式本文以常见的“平顶”六边形为例。2.2 AStar2D 与自定义权重的结合点Godot内置的AStar2D类是一个通用的图寻路实现。它的核心是管理一系列“点”id和连接这些点的“边”。你需要告诉它有哪些点通过add_point(id, position)。哪些点之间是相连的通过connect_points(id1, id2, bidirectional)。从一个点移动到其相连的另一个点代价cost是多少。默认情况下AStar2D使用两点之间的欧几里得距离作为移动代价。但在我们的策略游戏中移动代价主要取决于目的地单元格的地形。穿过平原可能消耗1点移动力而穿过山脉可能消耗3点甚至无法通行。这就是“自定义权重”要发挥作用的地方。AStar2D提供了一个关键的回调函数_compute_cost(from_id, to_id)。我们可以重写这个函数让它不再返回简单的距离而是返回基于目标单元格地形权重计算出的代价。具体思路是每个六边形单元格都是一个AStar2D图中的点其id可以由坐标q, r唯一生成例如id q * 1000 r确保唯一即可。连接一个单元格和它的六个邻居如果邻居存在且可通行。在_compute_cost中我们根据to_id对应的单元格的地形类型返回预设的移动力消耗值。这样当A*算法寻找从A到B的最短路径时它计算的不再是物理距离最短而是移动力总消耗最少的路径。这正是策略游戏所需要的你的单位会主动绕开高山选择平原行进。3. 实战构建从网格生成到寻路查询3.1 第一步定义数据结构与生成六边形网格首先我们创建一个HexCell资源用来保存每个六边形单元格的数据。# hex_cell.gd (继承自 Resource) class_name HexCell extends Resource export var coordinates: Vector2i # 轴向坐标 (q, r) export var terrain_type: String plains # 地形类型 export var movement_cost: float 1.0 # 基础移动消耗 export var is_walkable: bool true # 是否可通行 # 你可以扩展更多属性如资源、所属玩家等接下来创建主要的网格管理脚本hex_grid.gd。我们在这里初始化网格并将每个单元格添加到AStar2D图中。# hex_grid.gd extends Node2D export var grid_width: int 10 export var grid_height: int 10 export var hex_size: float 64.0 var astar AStar2D.new() var hex_map: Dictionary {} # 存储坐标到HexCell的映射 var id_to_coord: Dictionary {} # 存储id到坐标的映射方便反向查找 func _ready(): generate_hex_grid() setup_astar_graph() func generate_hex_grid(): hex_map.clear() for q in range(-grid_width, grid_width): for r in range(-grid_height, grid_height): # 简单的矩形范围生成你也可以按六边形区域生成 var coord Vector2i(q, r) var new_cell HexCell.new() new_cell.coordinates coord # 这里可以随机或按规则设置地形和消耗 new_cell.terrain_type _assign_terrain(coord) new_cell.movement_cost _get_cost_by_terrain(new_cell.terrain_type) new_cell.is_walkable new_cell.terrain_type ! mountain # 假设山脉不可通行 hex_map[coord] new_cell func _assign_terrain(coord: Vector2i) - String: # 一个简单的地形分配示例可以用噪声图生成更自然的效果 var rand_val randf() if rand_val 0.6: return plains elif rand_val 0.8: return forest elif rand_val 0.95: return hills else: return mountain func _get_cost_by_terrain(type: String) - float: match type: plains: return 1.0 forest: return 1.5 hills: return 2.0 mountain: return 999.0 # 用极大值代表不可通行或在is_walkable中处理 _: return 1.03.2 第二步集成AStar2D并实现自定义权重现在在setup_astar_graph方法中我们填充A*图并重写成本计算函数。func setup_astar_graph(): astar AStar2D.new() id_to_coord.clear() # 1. 添加所有可通行的点到A*图中 for coord in hex_map.keys(): var cell hex_map[coord] if cell.is_walkable: var point_id _coord_to_id(coord) var pixel_pos axial_to_pixel(coord, hex_size) astar.add_point(point_id, pixel_pos) id_to_coord[point_id] coord # 2. 连接相邻的可通行点 for point_id in astar.get_point_ids(): var coord id_to_coord[point_id] for dir in HEX_DIRECTIONS: var neighbor_coord coord dir if hex_map.has(neighbor_coord): var neighbor_cell hex_map[neighbor_coord] if neighbor_cell.is_walkable: var neighbor_id _coord_to_id(neighbor_coord) # 双向连接。如果存在单向通行地形可以在这里处理。 if astar.has_point(neighbor_id) and not astar.are_points_connected(point_id, neighbor_id): astar.connect_points(point_id, neighbor_id, true) # 3. 关键重写计算成本的方法 astar._compute_cost _compute_custom_cost func _coord_to_id(coord: Vector2i) - int: # 一个简单的哈希函数确保(q,r)映射到唯一ID。注意网格范围不要超过此函数的容量。 # 例如id (coord.x 1000) * 2000 (coord.y 1000) return (coord.x 1000) * 2000 (coord.y 1000) func _compute_custom_cost(from_id: int, to_id: int) - float: # A*算法在探索路径时会调用此函数。 # 我们忽略“来自”哪个单元格只关心“去往”的单元格的地形消耗。 # 这是策略游戏的常见设计移动消耗由目的地地形决定。 var to_coord id_to_coord.get(to_id) if to_coord and hex_map.has(to_coord): var target_cell hex_map[to_coord] return target_cell.movement_cost # 如果找不到目标点理论上不应发生返回一个高代价 return 999.0注意_compute_cost是AStar2D的一个“虚方法”在GDScript中通过赋值函数引用实现重写。它的返回值应该是从from_id移动到to_id的代价。在我们的模型中代价等于目标地形的movement_cost。这意味着即使两个单元格物理位置相邻如果一个是平原成本1一个是森林成本1.5那么走进森林的代价就是1.5。3.3 第三步实现寻路查询与可视化有了图之后寻路就变得非常简单。我们提供一个函数输入起点和终点的六边形坐标返回一个由坐标或像素位置构成的路径数组。func find_path(start_coord: Vector2i, end_coord: Vector2i) - PackedVector2Array: var start_id _coord_to_id(start_coord) var end_id _coord_to_id(end_coord) if not (astar.has_point(start_id) and astar.has_point(end_id)): print(起点或终点不可通行) return PackedVector2Array() # 获取由点ID构成的路径 var id_path: Array astar.get_point_path(start_id, end_id) # 将点ID路径转换回像素坐标路径便于直接使用 var pixel_path PackedVector2Array() for pid in id_path: pixel_path.append(astar.get_point_position(pid)) return pixel_path # 辅助函数将屏幕像素坐标转换为最近的六边形逻辑坐标用于鼠标点击选择 func pixel_to_axial(pixel_pos: Vector2) - Vector2i: # 这是轴向坐标转像素的逆运算涉及四舍五入到最近的六边形。 # 这里提供一种常用算法四舍五入立方体坐标法的轴向坐标版本简化实现 var q (sqrt(3.0)/3.0 * pixel_pos.x - 1.0/3.0 * pixel_pos.y) / hex_size var r (2.0/3.0 * pixel_pos.y) / hex_size return _axial_round(Vector2(q, r)) func _axial_round(frac: Vector2) - Vector2i: # 将分数坐标四舍五入到最近的整数六边形坐标 # 可通过转换为立方体坐标进行舍入再转回轴向坐标此处省略详细实现步骤。 # 建议参考经典的“立方体坐标舍入”算法并适配到轴向坐标。 # 这是一个需要确保正确的关键函数否则点击选择会不准。 # 简化返回 return Vector2i(round(frac.x), round(frac.y))为了直观展示你可以在_draw()函数中绘制六边形网格和路径func _draw(): # 绘制所有六边形 for coord in hex_map.keys(): var cell hex_map[coord] var center axial_to_pixel(coord, hex_size) var color Color.LIGHT_GRAY match cell.terrain_type: plains: color Color.GREEN forest: color Color.DARK_GREEN hills: color Color.SADDLE_BROWN mountain: color Color.GRAY draw_hexagon(center, hex_size, color) # 绘制最后计算的路径 if current_path.size() 1: draw_polyline(current_path, Color.RED, 3.0) func draw_hexagon(center: Vector2, size: float, color: Color): var points PackedVector2Array() for i in range(6): var angle_deg 60 * i var angle_rad deg_to_rad(angle_deg) var point center Vector2(size * cos(angle_rad), size * sin(angle_rad)) points.append(point) draw_colored_polygon(points, color)4. 高级技巧与性能优化4.1 动态更新权重与障碍物策略游戏中地形消耗并非一成不变。例如玩家可能修建道路降低移动消耗或者某个单位拥有“无视森林地形”的特技。这就需要我们能动态更新AStar2D图中的权重。直接更新单元格数据并重连最直接的方法是修改HexCell的movement_cost或is_walkable然后更新A*图中对应的点。但AStar2D没有提供直接修改已存在点成本权重的API。我们的成本是在_compute_custom_cost中动态计算的所以只要更新底层HexCell的数据下次寻路时就会生效。func update_cell_terrain(coord: Vector2i, new_terrain: String, new_cost: float, walkable: bool): if hex_map.has(coord): var cell hex_map[coord] cell.terrain_type new_terrain cell.movement_cost new_cost cell.is_walkable walkable var point_id _coord_to_id(coord) if not walkable and astar.has_point(point_id): # 如果变得不可通行需要从A*图中移除这个点及其所有连接 astar.remove_point(point_id) id_to_coord.erase(point_id) elif walkable and not astar.has_point(point_id): # 如果变得可通行需要将其添加回A*图并重新连接邻居 var pixel_pos axial_to_pixel(coord, hex_size) astar.add_point(point_id, pixel_pos) id_to_coord[point_id] coord _reconnect_point(point_id, coord)注意动态添加或移除点后必须重新连接该点与周围可通行点的边。_reconnect_point函数需要遍历六个方向检查邻居是否存在且可通行然后调用astar.connect_points。移除点时AStar2D会自动清理与之相关的连接。4.2 处理单位移动力与可达范围显示在《文明》中单位有固定的移动力如“移动力3”。寻路不仅要找最短路径还要找出在移动力耗尽前所有能到达的格子。这可以用AStar2D的get_point_connections和 Dijkstra 算法的思想来实现。我们可以写一个函数计算从起点出发在给定移动力预算内所有可到达的格子。func get_reachable_cells(start_coord: Vector2i, movement_points: float) - Array[Vector2i]: var start_id _coord_to_id(start_coord) if not astar.has_point(start_id): return [] var frontier: Array [] # 待探索的边界元素为 [point_id, remaining_movement] var reached: Dictionary {} # 已到达的格子key: point_id, value: 剩余移动力 frontier.append([start_id, movement_points]) reached[start_id] movement_points while not frontier.is_empty(): var current frontier.pop_front() var current_id current[0] var current_mp current[1] for neighbor_id in astar.get_point_connections(current_id): # 计算移动到邻居的消耗 var cost astar._compute_cost(current_id, neighbor_id) if cost 999.0: # 不可通行 continue var remaining_mp current_mp - cost if remaining_mp 0: continue # 移动力不足无法到达 # 如果这是一个新格子或者找到了一条剩余移动力更多的路径 if not reached.has(neighbor_id) or reached[neighbor_id] remaining_mp: reached[neighbor_id] remaining_mp frontier.append([neighbor_id, remaining_mp]) # 将到达的ID集合转换回坐标集合 var reachable_coords: Array[Vector2i] [] for pid in reached.keys(): reachable_coords.append(id_to_coord[pid]) return reachable_coords这个函数返回所有在移动力约束下可以到达的格子坐标。你可以在UI上高亮这些格子给玩家清晰的战略视野。4.3 性能考量与大规模地图优化对于非常大的地图比如100x100的六边形网格即有上万个单元格每次寻路都遍历整个图是不现实的。AStar2D本身很高效但构建整个图的连接关系在初始化时可能耗时。此外get_reachable_cells这样的函数如果移动力很高探索范围会很大。优化建议分块加载将大地图分成多个区块Chunk只加载和激活玩家当前视野范围内的区块到AStar2D图中。当玩家移动时动态加载和卸载区块。层次化寻路HPA*对于超大规模地图可以先在由多个六边形组成的“超级节点”之间进行高层寻路找到大致方向后再在涉及的局部网格内进行精细寻路。这需要更复杂的数据结构。缓存路径如果游戏中有很多单位重复走相似的固定路线如贸易路线可以缓存计算出的路径避免重复计算。使用AStar2D的get_point_ids()和get_point_connections()时注意这些函数返回的是数组的拷贝。在频繁调用的循环中如果点数很多可能会产生垃圾回收压力。在性能关键处需留意。对于大多数中小型策略游戏Godot的AStar2D直接处理几千个六边形网格的实时寻路是完全没有问题的。关键在于合理设计数据结构避免在每帧进行全图范围的复杂操作。5. 常见问题与调试技巧5.1 路径看起来“绕远路”或不自然可能原因1成本函数设计不合理。检查你的_compute_custom_cost是否真的只返回了目标地形的消耗如果错误地包含了起点地形或计算了距离会导致权重失衡。确保它返回的是target_cell.movement_cost。调试打印出路径上每个格子的坐标和地形成本手动计算总消耗看A*算法选择的是否真的是消耗最小的路径。可能原因2不可通行区域处理不当。检查山脉、水域等不可通行区域是否已将其对应的点从AStar2D图中移除astar.remove_point或者在其成本函数中返回一个极大的值如999如果只是设置了is_walkablefalse但点仍在图中且与邻居相连A*算法仍会尝试穿过它导致路径诡异。调试可视化所有AStar2D图中的点例如绘制小圆点确认不可通行区域没有点。可能原因3六边形邻居连接错误。检查HEX_DIRECTIONS数组是否正确确保六个方向向量没有遗漏或重复。使用轴向坐标时方向是固定的。调试写一个函数高亮显示选中格子的所有邻居检查连接关系是否正确。5.2 鼠标点击坐标转换不准确这是实现六边形网格交互最常见的坑。根本原因pixel_to_axial函数中的舍入算法不精确。解决方案实现健壮的立方体坐标舍入算法。虽然我们使用轴向坐标(q, r)但最准确的舍入通常先转换到立方体坐标(x, y, z)进行舍入再转回来。# 更稳健的像素到轴向坐标转换 func pixel_to_axial_robust(pixel_pos: Vector2) - Vector2i: var size hex_size var q (sqrt(3.0)/3.0 * pixel_pos.x - 1.0/3.0 * pixel_pos.y) / size var r (2.0/3.0 * pixel_pos.y) / size # 转换为立方体坐标进行舍入 var x q var z r var y -x - z var rx round(x) var ry round(y) var rz round(z) var x_diff abs(rx - x) var y_diff abs(ry - y) var z_diff abs(rz - z) # 立方体坐标约束x y z 0舍入可能破坏此约束需修正 if x_diff y_diff and x_diff z_diff: rx -ry - rz elif y_diff z_diff: ry -rx - rz else: rz -rx - ry # 将舍入后的立方体坐标转回轴向坐标 return Vector2i(int(rx), int(rz))视觉辅助在绘制每个六边形时将其逻辑坐标(q, r)以文本形式绘制在中心这样点击时你可以立即看到转换后的坐标是否正确。5.3 寻路性能突然下降检查动态更新是否在游戏运行时频繁调用astar.remove_point()和astar.add_point()特别是每帧都在调用。这些操作有一定开销。尽量批量更新或在非关键帧如玩家回合开始进行处理。检查路径长度是否在请求极长距离的寻路如跨越整个地图对于超长距离寻路考虑是否真的需要。可以设置一个最大寻路距离或者使用上面提到的层次化寻路思路。使用性能分析器Godot编辑器的“调试器”面板中有“性能分析器”Profiler。录制一段游戏过程查看_process或_physics_process中哪个函数耗时最多。重点关注find_path和get_reachable_cells的调用。5.4 AStar2D 报告“点未找到”错误检查ID生成_coord_to_id函数必须为每个唯一的(q, r)坐标生成全局唯一的整数ID。确保你的网格范围不会导致不同的坐标产生相同的ID即哈希冲突。使用足够大的乘数。检查点是否存在在调用astar.connect_points或astar.get_point_path之前务必用astar.has_point()检查起点和终点ID是否已存在于图中。动态地图中单位可能站在一个刚刚因地形变化而被移除的点上。初始化顺序确保在调用任何寻路函数之前setup_astar_graph()已经执行完毕图已构建完成。6. 扩展思路让寻路系统更具策略性基础寻路系统搭建完成后你可以在此基础上添加更多策略游戏元素使其更加丰富。1. 区域控制与通行权 为每个HexCell添加一个controller字段表示控制它的玩家或势力。在_compute_custom_cost中不仅检查地形还可以检查目标格子是否被敌方控制。如果是可以返回极高的代价模拟“敌境”难以通行或者根据单位能力如“无视控制区”动态调整。2. 多单位协同与路径预留 避免多个单位寻路时穿过彼此的位置。可以在AStar2D图中临时标记已被单位占据的格子为“障碍”暂时移除点或设置极高成本直到该单位离开。这需要维护一个实时的“占用地图”。3. 复杂地形效果 移动消耗可以不仅仅是基础地形值。可以结合海拔上坡消耗更多下坡消耗更少。道路/铁路网大幅降低特定连接上的移动消耗。这可以通过在连接两点时传入一个自定义的“权重缩放因子”到connect_points的最后一个参数weight_scale来实现。AStar2D的_compute_cost返回值会乘以这个缩放因子。单位特性骑兵在平原移动快但在森林慢登山家在山区如履平地。这可以通过在计算最终成本时引入一个“单位地形系数表”来实现。4. 非移动力资源寻路 除了移动力你的单位可能还有“燃油”、“补给”等资源。你可以将AStar2D的成本概念泛化用来计算消耗最少“燃油”的路径或者寻找一条在“补给”耗尽前能到达基地的路径。这只需要重新定义_compute_custom_cost返回的资源消耗类型即可。实现这些扩展的关键在于将游戏的所有规则都抽象为对“图”中“边”的“权重”的影响。AStar2D作为一个通用的图搜索算法为你处理了最复杂的路径搜索逻辑而你只需要专注于如何根据游戏状态正确地为每条边赋予权重。这种数据驱动的设计使得系统非常灵活和强大。