Godot 3 GDScript 教程 / Godot 3 GDScript 教程(十四):导航与寻路(Navigation2D)
导航与寻路(Navigation2D)
在 2D 游戏中,AI 角色的移动逻辑是核心系统之一。Godot 3 提供了 Navigation2D 节点用于实现寻路功能,配合导航多边形和 A* 算法,可以构建复杂的 AI 导航行为。
Navigation2D 节点
基本用法
场景树结构:
- Navigation2D
├── NavigationPolygonInstance (可行走区域)
├── StaticBody2D (障碍物)
└── Character (AI角色)
extends Node2D
onready var nav = $Navigation2D
func get_path_to_target(from: Vector2, to: Vector2) -> PoolVector2Array:
return nav.get_simple_path(from, to, true)
# 第三个参数 optimize: true 优化路径,减少拐点
func _draw():
var path = get_path_to_target($Start.global_position, $End.global_position)
if path.size() > 1:
for i in range(path.size() - 1):
draw_line(path[i], path[i + 1], Color.green, 2.0)
Navigation2D 常用方法
| 方法 | 说明 |
|---|---|
get_simple_path(from, to, optimize) | 获取从 from 到 to 的路径 |
get_closest_point(point) | 获取导航网格上最近的点 |
is_point_reachable(point) | 检查点是否可达 |
💡 提示:get_simple_path() 返回 PoolVector2Array,optimize = true 可减少路径点数量。
导航多边形 NavigationPolygon
代码创建导航多边形
# 动态生成导航网格(适合程序化生成地图)
func create_navigation_polygon(rect: Rect2) -> NavigationPolygon:
var nav_poly = NavigationPolygon.new()
nav_poly.clear()
var outline = PoolVector2Array([
rect.position,
Vector2(rect.end.x, rect.position.y),
rect.end,
Vector2(rect.position.x, rect.end.y)
])
nav_poly.add_outline(outline)
nav_poly.make_polygons_from_outlines()
return nav_poly
⚠️ 注意:导航多边形的顶点顺序必须是逆时针方向,否则寻路计算可能出错。
A* 寻路算法
当 Navigation2D 不能满足需求时,可以自己实现 A* 算法。
class_name AStar2DCustom
var grid: Dictionary = {}
var grid_width: int
var grid_height: int
var cell_size: Vector2
func _init(width: int, height: int, size: Vector2):
grid_width = width
grid_height = height
cell_size = size
func find_path(start: Vector2, end: Vector2) -> Array:
var start_grid = world_to_grid(start)
var end_grid = world_to_grid(end)
var open_list = [start_grid]
var closed_list = {}
var came_from = {}
var g_score = {start_grid: 0}
var f_score = {start_grid: heuristic(start_grid, end_grid)}
while open_list.size() > 0:
var current = open_list[0]
for node in open_list:
if f_score.get(node, INF) < f_score.get(current, INF):
current = node
if current == end_grid:
return reconstruct_path(came_from, current)
open_list.erase(current)
closed_list[current] = true
var neighbors = [
current + Vector2(0, -1), current + Vector2(0, 1),
current + Vector2(-1, 0), current + Vector2(1, 0)
]
for neighbor in neighbors:
if neighbor in closed_list or not is_walkable(neighbor):
continue
var tentative_g = g_score[current] + 1.0
if tentative_g < g_score.get(neighbor, INF):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + heuristic(neighbor, end_grid)
if not neighbor in open_list:
open_list.append(neighbor)
return []
func heuristic(a: Vector2, b: Vector2) -> float:
return abs(a.x - b.x) + abs(a.y - b.y)
func reconstruct_path(came_from: Dictionary, current: Vector2) -> Array:
var path = [grid_to_world(current)]
while current in came_from:
current = came_from[current]
path.push_front(grid_to_world(current))
return path
func world_to_grid(pos: Vector2) -> Vector2:
return Vector2(int(pos.x / cell_size.x), int(pos.y / cell_size.y))
func grid_to_world(g: Vector2) -> Vector2:
return Vector2(g.x * cell_size.x + cell_size.x / 2, g.y * cell_size.y + cell_size.y / 2)
func is_walkable(g: Vector2) -> bool:
return g.x >= 0 and g.x < grid_width and g.y >= 0 and g.y < grid_height
💡 提示:自定义 A* 适合高度定制化场景(如不同地形有不同通行代价)。简单寻路直接使用 Navigation2D 更高效。
动态避障
extends KinematicBody2D
export var speed: float = 150.0
export var avoidance_radius: float = 30.0
var path: PoolVector2Array = []
var current_path_index: int = 0
func move_along_path():
if current_path_index >= path.size():
return
var target = path[current_path_index]
var direction = (target - global_position).normalized()
# 计算避障力
var avoidance = Vector2.ZERO
for obstacle in get_tree().get_nodes_in_group("obstacles"):
var dist = global_position.distance_to(obstacle.global_position)
if dist < avoidance_radius and dist > 0:
var away = (global_position - obstacle.global_position).normalized()
avoidance += away * (1.0 - dist / avoidance_radius)
var final_dir = (direction + avoidance).normalized()
move_and_slide(final_dir * speed)
if global_position.distance_to(target) < 10.0:
current_path_index += 1
路径平滑
# 使用视线检测(Line of Sight)简化路径
func simplify_path(path: PoolVector2Array, space: Physics2DDirectSpaceState) -> PoolVector2Array:
if path.size() <= 2:
return path
var simplified = PoolVector2Array()
simplified.append(path[0])
var idx = 0
while idx < path.size() - 1:
var farthest = idx + 1
for i in range(path.size() - 1, idx, -1):
if not space.intersect_ray(path[idx], path[i], [], 1):
farthest = i
break
simplified.append(path[farthest])
idx = farthest
return simplified
AI 巡逻与追击
extends KinematicBody2D
enum State { PATROL, CHASE, SEARCH }
var current_state: int = State.PATROL
var path: PoolVector2Array = []
var patrol_points: Array = []
var patrol_index: int = 0
var target: Node2D = null
var speed: float = 100.0
var chase_speed: float = 160.0
var detection_range: float = 200.0
onready var nav: Navigation2D = get_parent().get_node("Navigation2D")
func _ready():
patrol_points = [Vector2(100,100), Vector2(500,100), Vector2(500,400), Vector2(100,400)]
_navigate_to(patrol_points[0])
func _physics_process(delta):
match current_state:
State.PATROL:
if _move_along_path(speed):
patrol_index = (patrol_index + 1) % patrol_points.size()
_navigate_to(patrol_points[patrol_index])
State.CHASE:
if target:
_navigate_to(target.global_position)
_move_along_path(chase_speed)
if global_position.distance_to(target.global_position) > 300:
current_state = State.SEARCH
State.SEARCH:
if _move_along_path(speed * 0.5):
current_state = State.PATROL
_navigate_to(patrol_points[patrol_index])
_check_for_player()
func _navigate_to(pos: Vector2):
path = nav.get_simple_path(global_position, pos, true)
func _move_along_path(move_speed: float) -> bool:
if path.size() == 0:
return true
var next = path[0]
if global_position.distance_to(next) < 5.0:
path.remove(0)
return path.size() == 0
move_and_slide((next - global_position).normalized() * move_speed)
return false
func _check_for_player():
var players = get_tree().get_nodes_in_group("player")
if players.size() > 0 and global_position.distance_to(players[0].global_position) < detection_range:
target = players[0]
current_state = State.CHASE
NavigationAgent(Godot 3.5+)
extends KinematicBody2D
onready var agent = $NavigationAgent2D
var speed = 200.0
func set_target(target_pos: Vector2):
agent.set_target_location(target_pos)
func _physics_process(delta):
if agent.is_navigation_finished():
return
var next_pos = agent.get_next_location()
var direction = global_position.direction_to(next_pos)
move_and_slide(direction * speed)
💡 提示:NavigationAgent2D 自动处理路径更新和到达检测,推荐在 Godot 3.5+ 中使用。
游戏开发场景
场景一:塔防敌人沿路行走
export var waypoint_path: NodePath
var waypoints: Array = []
var current: int = 0
func _ready():
for child in get_node(waypoint_path).get_children():
waypoints.append(child.global_position)
func _physics_process(delta):
if current >= waypoints.size():
reach_end()
return
var direction = (waypoints[current] - global_position).normalized()
move_and_slide(direction * speed)
if global_position.distance_to(waypoints[current]) < 5.0:
current += 1
场景二:宠物跟随系统
extends KinematicBody2D
export var follow_distance: float = 60.0
export var speed: float = 120.0
var owner_node: Node2D = null
func _physics_process(delta):
if owner_node == null:
return
if global_position.distance_to(owner_node.global_position) > follow_distance:
var nav = get_parent().get_node("Navigation2D")
var p = nav.get_simple_path(global_position, owner_node.global_position, true)
if p.size() > 1:
move_and_slide((p[1] - global_position).normalized() * speed)
扩展阅读
💡 总结:Navigation2D 适合大多数 2D 寻路需求。简单场景用
get_simple_path(),复杂场景考虑自定义 A* 或分层导航。AI 状态机是管理巡逻/追击行为的标准模式。