--- name: godot-navigation-system description: NavigationAgent2D使用、AStarGrid2D算法、自定义A*实现、流式寻路及性能优化。用于2D/3D导航系统、动态障碍物避让、游戏AI导航等场景。 --- # Godot 导航与寻路系统 Godot 4 导航系统完整指南,涵盖 NavigationAgent2D、AStarGrid2D、自定义 A* 算法、流场寻路及性能优化策略。 ## 何时使用此技能 - 需要 NavigationServer2D/3D 导航系统 - 实现游戏AI寻路导航 - 需要动态障碍物和NavLink连接 - 优化大量单位的寻路性能 ## 1. NavigationAgent2D 使用 NavigationAgent2D 是 Godot 4 推荐的 2D 导航解决方案,封装了 NavigationServer 的复杂操作。 ### 基础设置 ```gdscript # navigation_agent_2d.gd class_name NavigationAgent2D extends NavigationAgent2D signal navigation_finished signal path_changed signal target_reached @export var actor: CharacterBody2D @export var move_speed: float = 200.0 var _target_position: Vector2 = Vector2.ZERO func _ready() -> void: # 设置代理半径(障碍物避让) agent_height = 0 agent_max_speed = move_speed # 连接信号 navigation_finished.connect(_on_navigation_finished) path_changed.connect(_on_path_changed) target_reached.connect(_on_target_reached) # 等待NavigationServer同步 await get_tree().physics_frame await get_tree().physics_frame func _physics_process(delta: float) -> void: if actor and _target_position != Vector2.ZERO: if is_navigation_finished(): return var next_pos := get_next_path_position() var current_pos := actor.global_position var new_velocity := (next_pos - current_pos).normalized() * move_speed actor.velocity = new_velocity actor.move_and_slide() func set_target(world_position: Vector2) -> void: _target_position = world_position target_position = world_position func _on_navigation_finished() -> void: navigation_finished.emit() func _on_path_changed() -> void: path_changed.emit() func _on_target_reached() -> void: target_reached.emit() ``` ### NavigationRegion2D 导航区域 ```gdscript # navigation_region.gd class_name NavigationRegion extends NavigationRegion2D @export var tile_map: TileMap @export var bake_on_ready: bool = true func _ready() -> void: if bake_on_ready: await get_tree().physics_frame bake_navigation_polygon() # 从 TileMap 几何数据生成导航多边形 func bake_from_tilemap() -> void: var polygon := NavigationPolygon.new() var outline: Array[Vector2] = [] var used_rect := tile_map.get_used_rect() for x in range(used_rect.size.x): for y in range(used_rect.size.y): var cell := Vector2i(used_rect.position.x + x, used_rect.position.y + y) var tile_data := tile_map.get_cell_tile_data(0, cell) if tile_data and tile_data.get_custom_data("obstacle"): # 障碍物格子不加入导航 continue var world_pos := tile_map.map_to_local(cell) outline.append(world_pos) if not outline.is_empty(): polygon.add_outline(outline) polygon.make_polygons_from_outlines() navigation_polygon = polygon bake_navigation_polygon() ``` ### 动态障碍物 ```gdscript # dynamic_obstacle.gd class_name DynamicObstacle extends Area2D @export var radius: float = 32.0 @export var navigation_region: NavigationRegion var _last_position: Vector2 func _ready() -> void: area_entered.connect(_on_area_entered) area_exited.connect(_on_area_exited) func _physics_process(_delta: float) -> void: if global_position != _last_position: _last_position = global_position _update_navigation() func _update_navigation() -> void: # 简单实现:移动时重新烘焙导航网格 # 生产环境建议使用 NavigationMesh::update() if navigation_region: navigation_region.bake_navigation_polygon() func _on_area_entered(area: Area2D) -> void: # 障碍物进入逻辑 pass func _on_area_exited(area: Area2D) -> void: # 障碍物离开逻辑 pass ``` ## 2. AStarGrid2D 算法实现 AStarGrid2D 是 Godot 4 内置的高效网格寻路组件,适合规则网格的快速 A* 搜索。 ### 基础 AStarGrid2D ```gdscript # astar_grid_2d.gd class_name AStarGrid2D extends Node @export var tile_map: TileMap @export var obstacles_layer: int = 0 var _grid: AStarGrid2D func _ready() -> void: _setup_grid() func _setup_grid() -> void: var region := tile_map.get_used_rect() _grid = AStarGrid2D.new() _grid.size = region.size _grid.offset = tile_map.tile_set.tile_size / 2 _grid.cell_size = tile_map.tile_set.tile_size _grid.center = false _grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER _grid.update() # 标记障碍物 for cell in tile_map.get_used_cells(obstacles_layer): var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell) if tile_data and tile_data.get_custom_data("obstacle"): _grid.set_point_solid(cell - region.position) func find_path(start: Vector2i, end: Vector2i) -> PackedVector2Array: var region := tile_map.get_used_rect() var local_start := start - region.position var local_end := end - region.position if not _grid.is_point_inside(local_start) or not _grid.is_point_inside(local_end): return PackedVector2Array() if _grid.is_point_solid(local_start) or _grid.is_point_solid(local_end): return PackedVector2Array() var path := _grid.get_point_path(local_start, local_end) # 转换回世界坐标 var world_path := PackedVector2Array() for point in path: world_path.append(tile_map.map_to_local(point + region.position)) return world_path func is_walkable(cell: Vector2i) -> bool: var region := tile_map.get_used_rect() var local_cell := cell - region.position if not _grid.is_point_inside(local_cell): return false return not _grid.is_point_solid(local_cell) func set_obstacle(cell: Vector2i, obstacle: bool) -> void: var region := tile_map.get_used_rect() var local_cell := cell - region.position if obstacle: _grid.set_point_solid(local_cell) else: _grid.clear_point(local_cell) ``` ### 带权重的 AStarGrid2D ```gdscript # weighted_astar_grid.gd class_name WeightedAStarGrid extends AStarGrid2D var _cell_weights: Dictionary = {} func _ready() -> void: _setup_grid() func _setup_grid() -> void: var region := tile_map.get_used_rect() _grid = AStarGrid2D.new() _grid.size = region.size _grid.offset = tile_map.tile_set.tile_size / 2 _grid.cell_size = tile_map.tile_set.tile_size _grid.center = false _grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES _grid.update() # 初始化权重 for x in range(region.size.x): for y in range(region.size.y): var cell := Vector2i(region.position.x + x, region.position.y + y) _initialize_cell_weight(cell) # 标记障碍物 for cell in tile_map.get_used_cells(obstacles_layer): var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell) if tile_data and tile_data.get_custom_data("obstacle"): _grid.set_point_solid(cell - region.position) func _initialize_cell_weight(cell: Vector2i) -> void: var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell) if tile_data: var weight := tile_data.get_custom_data("weight") if weight != null: _cell_weights[cell] = weight else: _cell_weights[cell] = 1.0 else: _cell_weights[cell] = 1.0 func find_path(start: Vector2i, end: Vector2i) -> PackedVector2Array: # 使用默认的 A* 路径(权重需要在使用时自定义处理) return super.find_path(start, end) # 估计成本(启发式函数) func _estimate_cost(from: Vector2i, to: Vector2i) -> float: var weight := _cell_weights.get(to, 1.0) return (from - to).length() * weight ``` ## 3. 自定义 A* 实现 ### 标准 A* 算法 ```gdscript # custom_astar.gd # 自定义 A* 寻路实现 class_name CustomAStar extends Node @export var tile_map: TileMap @export var obstacles_layer: int = 0 var _grid_size: Vector2i var _walkable: Dictionary = {} class AStarNode: var cell: Vector2i var g_cost: float # 从起点到当前节点的实际成本 var h_cost: float # 从当前节点到终点的估计成本 var f_cost: float: # g_cost + h_cost return g_cost + h_cost var parent: AStarNode = null func _init(c: Vector2i, g: float, h: float) -> void: cell = c g_cost = g h_cost = h func _ready() -> void: _initialize_grid() func _initialize_grid() -> void: _grid_size = tile_map.get_used_rect().size var origin := tile_map.get_used_rect().position for x in range(_grid_size.x): for y in range(_grid_size.y): var cell := Vector2i(origin.x + x, origin.y + y) var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell) _walkable[cell] = tile_data == null or not tile_data.get_custom_data("obstacle") func find_path(start: Vector2i, end: Vector2i) -> Array[Vector2i]: if not _walkable.has(start) or not _walkable.has(end): return [] if not _walkable.get(end, false): # 目标不可达,寻找最近的可通行点 end = _find_nearest_walkable(end) if end == Vector2i(-1, -1): return [] var open_set: Array[AStarNode] = [] var closed_set: Dictionary = {} var start_node := AStarNode.new(start, 0.0, _heuristic(start, end)) open_set.append(start_node) while not open_set.is_empty(): # 找到 f_cost 最低的节点 open_set.sort_custom(func(a, b): return a.f_cost < b.f_cost) var current := open_set.pop_front() if current.cell == end: return _reconstruct_path(current) closed_set[current.cell] = current for neighbor in _get_neighbors(current.cell): if closed_set.has(neighbor) or not _walkable.get(neighbor, false): continue var g_cost := current.g_cost + _get_move_cost(current.cell, neighbor) var h_cost := _heuristic(neighbor, end) var existing := _find_in_open_set(open_set, neighbor) if existing == null: var new_node := AStarNode.new(neighbor, g_cost, h_cost) new_node.parent = current open_set.append(new_node) elif g_cost < existing.g_cost: existing.g_cost = g_cost existing.parent = current return [] func _find_in_open_set(open_set: Array[AStarNode], cell: Vector2i) -> AStarNode: for node in open_set: if node.cell == cell: return node return null func _heuristic(a: Vector2i, b: Vector2i) -> float: # 曼哈顿距离 return absf(a.x - b.x) + absf(a.y - b.y) func _get_move_cost(from: Vector2i, to: Vector2i) -> float: # 斜向移动 if from.x != to.x and from.y != to.y: return 1.414 return 1.0 func _get_neighbors(cell: Vector2i) -> Array[Vector2i]: return [ cell + Vector2i(0, -1), cell + Vector2i(1, 0), cell + Vector2i(0, 1), cell + Vector2i(-1, 0), cell + Vector2i(1, -1), cell + Vector2i(1, 1), cell + Vector2i(-1, 1), cell + Vector2i(-1, -1), ] func _find_nearest_walkable(target: Vector2i) -> Vector2i: var closest: Vector2i = Vector2i(-1, -1) var min_dist := INF for cell in _walkable.keys(): if _walkable[cell]: var dist := (cell - target).length() if dist < min_dist: min_dist = dist closest = cell return closest func _reconstruct_path(end_node: AStarNode) -> Array[Vector2i]: var path: Array[Vector2i] = [] var current: AStarNode = end_node while current != null: path.push_front(current.cell) current = current.parent return path ``` ## 4. 流式寻路(Flow Field Navigation) 流场寻路特别适合大量单位同时寻路的 RTS 游戏场景。 ### NavigationServer 流场 ```gdscript # nav_flow_field.gd # 使用 NavigationServer 实现流场 class_name NavFlowField extends Node2D @export var navigation_region: NavigationRegion2D @export var tile_map: TileMap @export var obstacles_layer: int = 0 @export var destination_layer: int = 1 var _nav_rid: RID var _flow_map: Dictionary = {} # Vector2i -> Vector2 var _update_needed: bool = false func _ready() -> void: _nav_rid = navigation_region.navigation_rid _build_initial_flow_field() func _physics_process(_delta: float) -> void: if _update_needed: _build_flow_field() _update_needed = false func request_update() -> void: _update_needed = true func _build_initial_flow_field() -> void: _build_flow_field() func _build_flow_field() -> void: _flow_map.clear() # 获取所有可行走格子 var walkable_cells: Array[Vector2i] = [] var destination_cells: Array[Vector2i] = [] for cell in tile_map.get_used_cells(0): var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell) if tile_data and tile_data.get_custom_data("obstacle"): continue walkable_cells.append(cell) for cell in tile_map.get_used_cells(destination_layer): destination_cells.append(cell) if destination_cells.is_empty(): return # BFS 构建距离场 var distance_field: Dictionary = {} var queue: Array[Vector2i] = destination_cells.duplicate() for dest in destination_cells: distance_field[dest] = 0.0 while not queue.is_empty(): var current := queue.pop_front() var current_dist := distance_field[current] for neighbor in _get_neighbors(current): if not _walkable(neighbor): continue if not distance_field.has(neighbor): distance_field[neighbor] = current_dist + 1.0 queue.append(neighbor) # 构建流场 for cell in distance_field.keys(): _flow_map[cell] = _calculate_flow_direction(cell, distance_field) func _walkable(cell: Vector2i) -> bool: var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell) return tile_data == null or not tile_data.get_custom_data("obstacle") func _get_neighbors(cell: Vector2i) -> Array[Vector2i]: return [ cell + Vector2i(0, -1), cell + Vector2i(1, 0), cell + Vector2i(0, 1), cell + Vector2i(-1, 0), ] func _calculate_flow_direction(cell: Vector2i, distance_field: Dictionary) -> Vector2: var neighbors := _get_neighbors(cell) var best_dir := Vector2.ZERO var lowest_dist := INF for neighbor in neighbors: if distance_field.has(neighbor): var dist := distance_field[neighbor] if dist < lowest_dist: lowest_dist = dist best_dir = Vector2(neighbor - cell).normalized() return best_dir func get_flow_direction(world_pos: Vector2) -> Vector2: var cell := tile_map.local_to_map(world_pos) if _flow_map.has(cell): return _flow_map[cell] return Vector2.ZERO ``` ## 5. 性能优化 ### 分组寻路(Pathfinding Batching) ```gdscript # batched_pathfinding.gd # 分组批量寻路,减少每帧计算量 class_name BatchedPathfinding extends Node signal batch_completed(paths: Dictionary) @export var max_paths_per_frame: int = 5 var _pending_requests: Array[Dictionary] = [] var _completed_paths: Dictionary = {} var _current_batch: int = 0 class PathRequest: var requester_id: int var start: Vector2i var end: Vector2i var priority: int func _init(id: int, s: Vector2i, e: Vector2i, p: int = 0) -> void: requester_id = id start = s end = e priority = p func _physics_process(_delta: float) -> void: _process_batch() func request_path(requester_id: int, start: Vector2i, end: Vector2i, priority: int = 0) -> void: _pending_requests.append(PathRequest.new(requester_id, start, end, priority)) func _process_batch() -> void: if _pending_requests.is_empty(): return # 按优先级排序 _pending_requests.sort_custom(func(a, b): return a.priority > b.priority) var processed: int = 0 while not _pending_requests.is_empty() and processed < max_paths_per_frame: var request := _pending_requests.pop_front() as PathRequest var path := _calculate_path(request.start, request.end) _completed_paths[request.requester_id] = path processed += 1 if _pending_requests.is_empty(): batch_completed.emit(_completed_paths) _completed_paths.clear() func _calculate_path(start: Vector2i, end: Vector2i) -> Array[Vector2i]: # 这里使用自定义的 A* 或其他寻路算法 var astar: CustomAStar = $CustomAStar return astar.find_path(start, end) func get_completed_path(requester_id: int) -> Array[Vector2i]: if _completed_paths.has(requester_id): return _completed_paths[requester_id] return [] ``` ### 节流寻路(Throttled Pathfinding) ```gdscript # throttled_pathfinding.gd # 节流寻路,避免频繁计算 class_name ThrottledPathfinding extends Node @export var throttle_duration: float = 0.2 # 秒 var _path_cache: Dictionary = {} var _last_update_time: float = 0.0 var _pending_requests: Dictionary = {} var _needs_update: bool = false var _astar: CustomAStar func _ready() -> void: _astar = $CustomAStar func _physics_process(delta: float) -> void: if _needs_update: _last_update_time += delta if _last_update_time >= throttle_duration: _execute_throttled_update() _last_update_time = 0.0 _needs_update = false func request_path(id: int, start: Vector2i, end: Vector2i) -> void: _pending_requests[id] = {"start": start, "end": end, "path": null} _needs_update = true func _execute_throttled_update() -> void: for id in _pending_requests.keys(): var request := _pending_requests[id] var path := _astar.find_path(request.start, request.end) _path_cache[id] = path request.path = path _pending_requests.clear() func get_path(id: int) -> Array[Vector2i]: return _path_cache.get(id, []) ``` ### LOD 寻路(Level of Detail) ```gdscript # lod_pathfinding.gd # 分层寻路,远距离用粗糙网格 class_name LODPathfinding extends Node enum LODLevel { HIGH, MEDIUM, LOW } @export var tile_map: TileMap @export var obstacles_layer: int = 0 var _lod_grid_sizes: Dictionary = { LODLevel.HIGH: Vector2i(1, 1), LODLevel.MEDIUM: Vector2i(4, 4), LODLevel.LOW: Vector2i(8, 8), } var _lod_astar: Dictionary = {} func _ready() -> void: _initialize_lod_grids() func _initialize_lod_grids() -> void: for level in _lod_grid_sizes.keys(): _create_lod_grid(level) func _create_lod_grid(level: LODLevel) -> void: var grid_size := _lod_grid_sizes[level] var region := tile_map.get_used_rect() var astar := AStarGrid2D.new() var coarse_size := Vector2i( ceili(region.size.x / float(grid_size.x)), ceili(region.size.y / float(grid_size.y)) ) astar.size = coarse_size astar.cell_size = tile_map.tile_set.tile_size * grid_size astar.center = false astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER astar.update() _lod_astar[level] = astar func find_path(start: Vector2i, end: Vector2i) -> Array[Vector2i]: var distance := (start - end).length() var level: LODLevel if distance < 200: level = LODLevel.HIGH elif distance < 500: level = LODLevel.MEDIUM else: level = LODLevel.LOW return _find_path_at_level(start, end, level) func _find_path_at_level(start: Vector2i, end: Vector2i, level: LODLevel) -> Array[Vector2i]: var astar: AStarGrid2D = _lod_astar[level] # 转换到 LOD 网格坐标 var grid_size := _lod_grid_sizes[level] var region := tile_map.get_used_rect() var local_start := (start - region.position) / grid_size var local_end := (end - region.position) / grid_size if astar.is_point_inside(local_start) and astar.is_point_inside(local_end): return astar.get_point_path(local_start, local_end) return [] ``` ## 6. NavLink 连接 NavLink 用于连接不连续的导航区域,实现跳跃、传送等效果。 ### 自定义 NavLink ```gdscript # custom_nav_link.gd class_name CustomNavLink extends NavigationLink2D @export var link_type: int = 0 # 0: 传送, 1: 跳跃, 2: 桥梁 var _is_active: bool = true func _ready() -> void: navigation_layers = 1 # 设置导航层 func _get_navigation_links(start_position: Vector2, end_position: Vector2) -> Array[Vector2]: if not _is_active: return [] return [start_position, end_position] func set_active(active: bool) -> void: _is_active = active ``` ### 使用 NavLink 实现跳跃 ```gdscript # platform_nav_link.gd class_name PlatformNavLink extends NavigationLink2D @export var jump_height: float = 100.0 @export var jump_duration: float = 0.5 var _start_pos: Vector2 var _end_pos: Vector2 func _ready() -> void: var owner := get_parent() if owner is Node2D: _start_pos = owner.global_position _end_pos = global_position func get_jump_path(start: Vector2, end: Vector2) -> PackedVector2Array: if not _is_enabled(): return PackedVector2Array() var path := PackedVector2Array() path.append(start) # 抛物线中间点 var mid_point := (start + end) / 2.0 mid_point.y -= jump_height path.append(mid_point) path.append(end) return path func _is_enabled() -> bool: # 检查平台是否可用 var platform := get_parent() if platform.has_method("is_active"): return platform.is_active() return true ``` ## 7. 完整示例:AI 单位导航系统 ```gdscript # ai_navigation_controller.gd # 完整的 AI 单位导航控制器 class_name AINavigationController extends CharacterBody2D signal destination_reached signal path_updated(path: PackedVector2Array) @export var navigation_agent: NavigationAgent2D @export var move_speed: float = 150.0 @export var path_reach_distance: float = 10.0 @export var use_flow_field: bool = false @export var flow_field: NavFlowField @export var use_lod: bool = false @export var lod_controller: LODPathfinding var _target_position: Vector2 = Vector2.ZERO var _current_path: PackedVector2Array = [] var _path_index: int = 0 func _ready() -> void: navigation_agent.velocity_computed.connect(_on_velocity_computed) set_physics_process(false) await get_tree().physics_frame set_physics_process(true) func _physics_process(delta: float) -> void: if navigation_agent.is_navigation_finished(): destination_reached.emit() return var next_pos: Vector2 if use_flow_field and flow_field: # 流场导航 next_pos = _get_flow_field_next_position(delta) else: # 标准导航 next_pos = navigation_agent.get_next_path_position() var current_pos := global_position var new_velocity := (next_pos - current_pos).normalized() * move_speed if navigation_agent.velocity_computed.size() > 0: velocity = new_velocity move_and_slide() else: navigation_agent.velocity = new_velocity func _get_flow_field_next_position(delta: float) -> Vector2: var flow_dir := flow_field.get_flow_direction(global_position) if flow_dir.length() > 0.01: return global_position + flow_dir * move_speed * delta else: return global_position func set_destination(world_position: Vector2) -> void: _target_position = world_position if use_lod and lod_controller: var start_cell := navigation_agent.get_current_navigation_region() var end_cell := (world_position / lod_controller.tile_map.tile_set.tile_size).floor() _current_path = Array(lod_controller.find_path(start_cell, end_cell)) _path_index = 0 path_updated.emit(_current_path) navigation_agent.target_position = world_position func _on_velocity_computed(velocity: Vector2) -> void: self.velocity = velocity move_and_slide() ``` ## 性能优化建议 1. **使用 AStarGrid2D**:内置优化,比自定义 A* 更快 2. **流场共享**:大量单位共享流场,避免重复计算 3. **LOD 寻路**:远距离使用粗糙网格 4. **路径缓存**:相同起点终点复用缓存结果 5. **批量处理**:每帧限制寻路请求数量 6. **节流更新**:动态障碍物变化后延迟更新 ## 最佳实践 - 优先使用 NavigationAgent2D,它与 NavigationServer 集成更好 - 导航网格变化时使用 `bake_navigation_polygon()` 重新烘焙 - NavLink 用于连接分离的导航区域 - 大量单位使用流场寻路 - 动态障碍物使用分块更新而非全局重算