원클릭으로
godot-navigation-system
NavigationAgent2D使用、AStarGrid2D算法、自定义A*实现、流式寻路及性能优化。用于2D/3D导航系统、动态障碍物避让、游戏AI导航等场景。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
NavigationAgent2D使用、AStarGrid2D算法、自定义A*实现、流式寻路及性能优化。用于2D/3D导航系统、动态障碍物避让、游戏AI导航等场景。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Godot 4.x .tscn 场景文件纯文本格式规范与 AI 操作指南 — 文件结构、资源引用、 Transform 变换、属性值格式、信号连接,以及 AI 直接读写 .tscn 文件的操作方法。
Godot 4.x .tscn 场景文件纯文本格式规范与 AI 操作指南 — 文件结构、资源引用、 Transform 变换、属性值格式、信号连接,以及 AI 直接读写 .tscn 文件的操作方法。
Godot 4 Autoload单例模式最佳实践,包含全局管理、事件总线、信号通信。
Godot 4 Buff系统完整实现,支持状态效果管理、持续时间、循环效果、层叠机制等。使用Resource+Component架构。
Godot 4 建造系统实现,包含GridMap方块管理、第一人称视角、射线交互。
Godot 4 ECS实体组件系统模式,包含组件化设计、数据与逻辑分离、组件通信。
| name | godot-navigation-system |
| description | NavigationAgent2D使用、AStarGrid2D算法、自定义A*实现、流式寻路及性能优化。用于2D/3D导航系统、动态障碍物避让、游戏AI导航等场景。 |
Godot 4 导航系统完整指南,涵盖 NavigationAgent2D、AStarGrid2D、自定义 A* 算法、流场寻路及性能优化策略。
NavigationAgent2D 是 Godot 4 推荐的 2D 导航解决方案,封装了 NavigationServer 的复杂操作。
# 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()
# 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()
# 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
AStarGrid2D 是 Godot 4 内置的高效网格寻路组件,适合规则网格的快速 A* 搜索。
# 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)
# 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
# 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
流场寻路特别适合大量单位同时寻路的 RTS 游戏场景。
# 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
# 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.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_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 []
NavLink 用于连接不连续的导航区域,实现跳跃、传送等效果。
# 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
# 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
# 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()
bake_navigation_polygon() 重新烘焙