强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 3 GDScript 教程 / Godot 3 GDScript 教程(十四):导航与寻路(Navigation2D)

导航与寻路(Navigation2D)

在 2D 游戏中,AI 角色的移动逻辑是核心系统之一。Godot 3 提供了 Navigation2D 节点用于实现寻路功能,配合导航多边形和 A* 算法,可以构建复杂的 AI 导航行为。


基本用法

场景树结构:
- 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)
方法说明
get_simple_path(from, to, optimize)获取从 from 到 to 的路径
get_closest_point(point)获取导航网格上最近的点
is_point_reachable(point)检查点是否可达

💡 提示get_simple_path() 返回 PoolVector2Arrayoptimize = 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

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 状态机是管理巡逻/追击行为的标准模式。