如何使用内置AStar算法生成转向最少的最短路径?
如何让Godot内置AStar选择转向最少的最短路径
这个问题我之前也碰到过——Godot内置的AStar(3.x)确实不支持直接设置边权重,只能给节点设权重,这就导致默认寻路只会优先最短距离,容易出现频繁转向的路径。不过我们可以通过扩展节点状态的方式模拟边的转向成本,让AStar优先选择转向少的路径,同时保证总长度最短。
核心思路
既然没法给边加权重,我们就把「网格坐标+进入方向」组合成一个唯一的节点。这样:
- 当移动方向和之前一致时,路径权重保持基础值(比如1)
- 当需要转向时,给这条路径加一个极小的惩罚值(比如0.1),让转向多的路径总权重更高,AStar就会自动避开这类路径
这种方法的好处是:惩罚值远小于基础权重,不会让路径变长,只会在长度相同的路径中优先选择转向最少的。
具体实现代码
下面是适配你现有GridMap代码的完整实现:
extends GridMap var _astar = AStar.new() # 定义我们用到的两个移动方向(对应你代码里的right和above) const DIRS = [Vector3.RIGHT, Vector3.FORWARD] # 额外增加一个"无方向"状态,用于起点 const DIR_NONE = -1 func _ready(): var id = 0 # 第一步:为每个可通行网格点的所有方向状态创建节点 for c in get_used_cells(): var cell_pos = Vector3(c.x, c.y, c.z) # 判断当前网格是否可通行 var is_passable = not _get_cover(c.x, c.y, c.z) if not is_passable: continue # 为"无方向"和两个移动方向分别创建节点 for dir_state in [DIR_NONE, 0, 1]: _astar.add_point(id, cell_pos, 1.0) id += 1 # 第二步:连接节点,处理转向惩罚 for c in get_used_cells(): var cell_pos = Vector3(c.x, c.y, c.z) var is_passable = not _get_cover(c.x, c.y, c.z) if not is_passable: continue # 连接到右侧(X+方向)的网格 var right_pos = cell_pos + Vector3.RIGHT if _is_cell_passable(right_pos): # 遍历当前节点的所有方向状态,连接到右侧节点的X方向状态 for from_dir in [DIR_NONE, 0, 1]: var from_id = _get_node_id(cell_pos, from_dir) var to_id = _get_node_id(right_pos, 0) # 0对应X方向 # 计算权重:转向时加惩罚 var weight = 1.0 if from_dir != DIR_NONE and from_dir != 0: weight += 0.1 _astar.connect_points(from_id, to_id, true, weight) # 连接到上方(Z+方向)的网格 var above_pos = cell_pos + Vector3.FORWARD if _is_cell_passable(above_pos): for from_dir in [DIR_NONE, 0, 1]: var from_id = _get_node_id(cell_pos, from_dir) var to_id = _get_node_id(above_pos, 1) # 1对应Z方向 var weight = 1.0 if from_dir != DIR_NONE and from_dir != 1: weight += 0.1 _astar.connect_points(from_id, to_id, true, weight) # 辅助函数:根据网格坐标和方向状态获取节点ID func _get_node_id(cell_pos: Vector3, dir_state: int) -> int: # 先获取网格点在used_cells中的索引,再结合方向状态计算唯一ID var cell_idx = get_used_cells().find(Vector3i(cell_pos.x, cell_pos.y, cell_pos.z)) # 每个网格点对应3个节点(无方向+两个移动方向) return cell_idx * 3 + (dir_state + 1) # 把DIR_NONE转成0索引 # 辅助函数:判断网格点是否可通行 func _is_cell_passable(pos: Vector3) -> bool: var cell = Vector3i(pos.x, pos.y, pos.z) if not is_cell_used(cell): return false return not _get_cover(cell.x, cell.y, cell.z) # 自定义寻路函数,返回去重后的路径坐标 func find_optimized_path(start_pos: Vector3, end_pos: Vector3) -> Array[Vector3]: if not _is_cell_passable(start_pos) or not _is_cell_passable(end_pos): return [] # 起点用"无方向"状态的节点 var start_id = _get_node_id(start_pos, DIR_NONE) # 遍历终点的所有方向状态,找到成本最低的路径 var min_cost = INF var best_end_id = -1 for dir_state in [DIR_NONE, 0, 1]: var end_id = _get_node_id(end_pos, dir_state) if _astar.has_point(end_id): var cost = _astar.get_point_cost(start_id, end_id) if cost < min_cost: min_cost = cost best_end_id = end_id if best_end_id == -1: return [] # 提取路径并去重(连续节点可能是同一坐标不同方向) var path_ids = _astar.get_id_path(start_id, best_end_id) var final_path = [] for id in path_ids: var pos = _astar.get_point_position(id) if final_path.empty() or pos != final_path[-1]: final_path.append(pos) return final_path
方案说明
- 节点扩展:每个可通行网格点对应3个节点,分别代表「无方向(起点)」「从X方向进入」「从Z方向进入」,这样转向操作会对应不同的节点跳转。
- 转向惩罚:当从一个方向转向另一个方向时,给这条连接边加0.1的权重,让转向多的路径总权重更高,AStar会优先选择转向少的路径。
- 路径去重:因为路径中会出现同一坐标的不同方向节点,所以最后需要去重得到干净的坐标路径。
如果你使用的是Godot 4.x,也可以尝试用AStar3D的set_heuristic_function自定义启发式,但这种方法需要跟踪路径方向,实现起来更复杂,上面的节点扩展方案兼容性和可靠性更好。
内容的提问来源于stack exchange,提问作者rcorre
相关产品推荐
相关产品推荐

