Godot 4 GDScript 教程 / 导航与寻路(NavigationServer)
导航与寻路(NavigationServer)
概述
Godot 4 的导航系统基于 NavigationServer 单例,通过导航网格(Navigation Mesh)实现自动寻路。支持 2D/3D 导航、动态障碍物、多层导航和路径平滑。
| 节点 | 说明 |
|---|
NavigationRegion3D/2D | 导航区域(包含导航网格) |
NavigationAgent3D/2D | 导航代理(移动寻路) |
NavigationObstacle3D/2D | 动态障碍物 |
NavigationLink3D/2D | 导航连接(跳板、电梯) |
NavigationRegion3D(3D 导航区域)
设置导航区域
extends Node3D
func _ready():
# 在编辑器中:
# 1. 添加 NavigationRegion3D 节点
# 2. 创建 NavigationMesh 资源
# 3. 设置参数后烘焙
pass
# 代码中烘焙导航网格
func bake_navigation():
var region: NavigationRegion3D = $NavigationRegion3D
# 烘焙需要场景中的 StaticBody3D 有碰撞形状
region.bake_navigation_mesh()
NavigationMesh 参数
| 参数 | 说明 | 默认值 |
|---|
agent_radius | 代理半径(边缘裁切) | 0.5 |
agent_height | 代理高度(桥检测) | 1.0 |
agent_max_slope | 最大坡度角(度) | 45 |
agent_max_climb | 最大攀爬高度 | 0.25 |
cell_size | 体素大小 | 0.3 |
cell_height | 体素高度 | 0.2 |
detail_sampling_distance | 细节采样距离 | 6.0 |
detail_sampling_error | 细节采样误差 | 1.0 |
filter_baking_aabb | 烘焙区域 | 全场景 |
NavigationAgent3D(导航代理)
基本寻路
extends CharacterBody3D
@onready var agent: NavigationAgent3D = $NavigationAgent3D
@export var speed: float = 3.0
var target_position: Vector3
func _ready():
# 设置导航代理参数
agent.path_desired_distance = 0.5 # 到达路径点的阈值
agent.target_desired_distance = 1.0 # 到达目标的阈值
agent.path_max_distance = 50.0 # 最大路径长度
agent.avoidance_enabled = true # 启用避障
func set_target(target: Vector3):
target_position = target
agent.target_position = target
func _physics_process(delta):
if agent.is_navigation_finished():
return
var next_path_pos = agent.get_next_path_position()
var direction = (next_path_pos - global_position).normalized()
velocity = direction * speed
# 朝向移动方向
if direction.length() > 0.1:
look_at(global_position + Vector3(direction.x, 0, direction.z))
move_and_slide()
NavigationAgent3D 信号
func _ready():
agent.navigation_finished.connect(_on_nav_finished)
agent.path_changed.connect(_on_path_changed)
agent.target_reached.connect(_on_target_reached)
agent.link_reached.connect(_on_link_reached)
agent.waypoint_reached.connect(_on_waypoint_reached)
agent.velocity_computed.connect(_on_velocity_computed)
func _on_nav_finished():
print("到达目标")
func _on_target_reached():
print("到达目标位置")
func _on_waypoint_reached(details: Dictionary):
print("到达路径点: ", details)
func _on_velocity_computed(safe_velocity: Vector3):
# 避障计算后的安全速度
velocity = safe_velocity
move_and_slide()
NavigationServer API
extends Node
func _ready():
# 获取默认导航地图
var map = NavigationServer3D.get_maps()[0]
# 查询路径
var start = Vector3(0, 0, 0)
var end = Vector3(10, 0, 10)
var path = NavigationServer3D.map_get_path(map, start, end, true)
print("路径点: ", path)
# 获取最近的导航点
var closest = NavigationServer3D.map_get_closest_point(map, Vector3(5, 0, 5))
print("最近导航点: ", closest)
# 判断点是否在导航网格上
var is_on_nav = NavigationServer3D.map_get_closest_point(map, global_position)
var distance = global_position.distance_to(is_on_nav)
if distance > 1.0:
print("不在导航网格上")
# 区域查询
func query_region():
var map = NavigationServer3D.get_maps()[0]
var regions = NavigationServer3D.map_get_regions(map)
for region in regions:
print("区域: ", region)
NavigationServer3D 常用方法
| 方法 | 说明 |
|---|
get_maps() | 获取所有导航地图 |
map_get_path() | 查询路径 |
map_get_closest_point() | 获取最近导航点 |
map_get_closest_point_normal() | 获取最近点法线 |
map_get_closest_point_owner() | 获取最近点所属区域 |
map_get_regions() | 获取地图中的区域 |
region_get_path() | 从区域查询路径 |
obstacle_set_avoidance_enabled() | 设置障碍物避障 |
💡 提示:如果 map_get_path() 返回空数组,说明起始点或目标点不在导航网格上。使用 map_get_closest_point() 将点吸附到最近的导航网格位置。
2D 导航
extends CharacterBody2D
@onready var agent: NavigationAgent2D = $NavigationAgent2D
@export var speed: float = 200.0
func set_target(target: Vector2):
agent.target_position = target
func _physics_process(delta):
if agent.is_navigation_finished():
velocity = Vector2.ZERO
return
var next_pos = agent.get_next_path_position()
var direction = (next_pos - global_position).normalized()
velocity = direction * speed
if direction.length() > 0.1:
rotation = direction.angle()
move_and_slide()
动态障碍物 Obstacle3D
extends NavigationObstacle3D
func _ready():
# 设置障碍物参数
radius = 2.0 # 圆形障碍半径
# 或使用高度/速度
height = 3.0
avoidance_enabled = true
代码创建动态障碍
extends Node3D
func create_obstacle(pos: Vector3, size: float = 1.0):
var obstacle = NavigationObstacle3D.new()
obstacle.position = pos
obstacle.radius = size
obstacle.avoidance_enabled = true
add_child(obstacle)
多层导航
不同类型的单位使用不同的导航层:
extends CharacterBody3D
@onready var agent: NavigationAgent3D = $NavigationAgent3D
func _ready():
# 设置导航层掩码
# 地面单位使用第 1 层
agent.navigation_layers = 1 # 二进制:0b0001
# 飞行单位使用第 2 层
# agent.navigation_layers = 2 # 二进制:0b0010
# 水生单位使用第 3 层
# agent.navigation_layers = 4 # 二进制:0b0100
# 多层导航区域
# 在编辑器中为不同 NavigationRegion3D 设置不同的 navigation_layers
AI 巡逻与追击
extends CharacterBody3D
enum State { PATROL, CHASE, SEARCH }
var current_state: State = State.PATROL
@onready var agent: NavigationAgent3D = $NavigationAgent3D
@onready var vision_area: Area3D = $VisionArea
@export var patrol_speed: float = 2.0
@export var chase_speed: float = 4.0
@export var patrol_points: Array[Vector3] = []
@export var detection_range: float = 15.0
var current_patrol_index: int = 0
var player: Node3D = null
var last_known_player_pos: Vector3
var search_timer: float = 0.0
func _ready():
agent.path_desired_distance = 1.0
agent.target_desired_distance = 1.5
agent.avoidance_enabled = true
vision_area.body_entered.connect(_on_player_entered)
vision_area.body_exited.connect(_on_player_exited)
if patrol_points.size() > 0:
agent.target_position = patrol_points[0]
func _physics_process(delta):
match current_state:
State.PATROL:
patrol(delta)
State.CHASE:
chase(delta)
State.SEARCH:
search(delta)
func patrol(delta):
if agent.is_navigation_finished():
current_patrol_index = (current_patrol_index + 1) % patrol_points.size()
agent.target_position = patrol_points[current_patrol_index]
move_toward_target(patrol_speed)
look_at_target()
func chase(delta):
if player:
agent.target_position = player.global_position
last_known_player_pos = player.global_position
move_toward_target(chase_speed)
look_at_target()
# 检查是否到达最后已知位置
if not player and agent.is_navigation_finished():
current_state = State.SEARCH
search_timer = 5.0
func search(delta):
search_timer -= delta
if search_timer <= 0:
current_state = State.PATROL
agent.target_position = patrol_points[current_patrol_index]
func move_toward_target(speed: float):
if agent.is_navigation_finished():
velocity = Vector3.ZERO
return
var next_pos = agent.get_next_path_position()
var direction = (next_pos - global_position).normalized()
velocity = direction * speed
move_and_slide()
func look_at_target():
if velocity.length() > 0.1:
var target = global_position + Vector3(velocity.x, 0, velocity.z).normalized()
look_at(target, Vector3.UP)
func _on_player_entered(body: Node3D):
if body.is_in_group("player"):
player = body
current_state = State.CHASE
func _on_player_exited(body: Node3D):
if body == player:
player = null
路径平滑
路径点简化
extends CharacterBody3D
@onready var agent: NavigationAgent3D = $NavigationAgent3D
var smoothed_path: PackedVector3Array = []
var path_index: int = 0
func set_target(target: Vector3):
agent.target_position = target
# 获取完整路径
var raw_path = agent.get_current_navigation_path()
smoothed_path = smooth_path(raw_path, 0.5)
path_index = 0
func smooth_path(path: PackedVector3Array, tolerance: float) -> PackedVector3Array:
if path.size() <= 2:
return path
# Ramer-Douglas-Peucker 算法简化路径
var result = PackedVector3Array()
result.append(path[0])
rdp_simplify(path, 0, path.size() - 1, tolerance, result)
result.append(path[path.size() - 1])
return result
func rdp_simplify(path: PackedVector3Array, start: int, end: int, tolerance: float, result: PackedVector3Array):
var max_dist = 0.0
var max_idx = start
for i in range(start + 1, end):
var dist = point_to_line_distance(path[i], path[start], path[end])
if dist > max_dist:
max_dist = dist
max_idx = i
if max_dist > tolerance:
rdp_simplify(path, start, max_idx, tolerance, result)
result.append(path[max_idx])
rdp_simplify(path, max_idx, end, tolerance, result)
func point_to_line_distance(point: Vector3, line_start: Vector3, line_end: Vector3) -> float:
var line_vec = line_end - line_start
var point_vec = point - line_start
var t = clampf(point_vec.dot(line_vec) / line_vec.length_squared(), 0.0, 1.0)
var closest = line_start + line_vec * t
return point.distance_to(closest)
贝塞尔路径平滑
func bezier_smooth(path: PackedVector3Array, segments: int = 5) -> PackedVector3Array:
var result = PackedVector3Array()
for i in range(path.size() - 1):
var p0 = path[maxi(0, i - 1)]
var p1 = path[i]
var p2 = path[min(path.size() - 1, i + 1)]
var p3 = path[min(path.size() - 1, i + 2)]
for t_step in range(segments + 1):
var t = float(t_step) / float(segments)
var point = catmull_rom(p0, p1, p2, p3, t)
result.append(point)
return result
func catmull_rom(p0: Vector3, p1: Vector3, p2: Vector3, p3: Vector3, t: float) -> Vector3:
var t2 = t * t
var t3 = t2 * t
return 0.5 * (
(2 * p1) +
(-p0 + p2) * t +
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +
(-p0 + 3 * p1 - 3 * p2 + p3) * t3
)
导航性能优化
| 策略 | 说明 |
|---|
| 降低 NavigationMesh 细度 | 增大 cell_size |
| 限制同时寻路的代理数 | 分帧计算路径 |
| 使用路径缓存 | 相同目标不重复查询 |
| 简化导航网格 | 只保留必要区域 |
| 禁用远处代理的避障 | agent.avoidance_enabled = false |
| 使用导航层过滤 | 不同层分开计算 |
# 分帧寻路:每帧只处理 N 个代理
var agents: Array[NavigationAgent3D] = []
var current_batch: int = 0
const BATCH_SIZE = 5
func _physics_process(delta):
var start = current_batch
var end = mini(start + BATCH_SIZE, agents.size())
for i in range(start, end):
update_agent(agents[i])
current_batch = end if end < agents.size() else 0
复杂地图导航策略
多 NavigationRegion 拼接
# 大型地图分块导航
# 每个区域有独立的 NavigationRegion3D
# NavigationServer 自动合并相邻区域
# 导航连接(NavigationLink3D)用于非标准路径
# 例如:跳跃平台、电梯、传送门
extends NavigationLink3D
func _ready():
# 设置连接起止点
start_position = Vector3(0, 0, 0)
end_position = Vector3(0, 3, 5) # 跳到高处
# 导航层
navigation_layers = 1
# 是否双向
bidirectional = true
NavigationLink3D 使用场景
| 场景 | 说明 |
|---|
| 跳跃平台 | 从低处跳到高处 |
| 电梯 | 垂直移动 |
| 传送门 | 瞬间移动 |
| 梯子 | 攀爬连接 |
| 断桥 | 跨越间隙 |
游戏开发场景
| 场景 | 推荐方案 |
|---|
| RTS 单位寻路 | NavigationAgent + 避障 |
| RPG NPC 巡逻 | 巡逻点 + NavigationAgent |
| 塔防怪物路径 | 固定 NavigationRegion |
| 开放世界 AI | 多层导航 + 路径缓存 |
| 2D 地牢探索 | NavigationRegion2D + 动态烘焙 |
⚠️ 常见陷阱
- 导航网格不覆盖的地方无法寻路,确保目标点在网格上
agent_radius 过大会导致窄通道不可通过- NavigationLink3D 必须在 NavigationRegion3D 的范围内
- 动态障碍物不会自动改变导航网格,只是改变避障
map_get_path() 是同步调用,大量调用会卡顿
扩展阅读