簡介

Godot 4 擁有一套建立於 NavigationServer2D/3D、NavigationRegion 節點與 NavigationAgent 節點之上的強大導航系統。相較於 Godot 3 的系統,它是一次顯著的改進 — 更靈活、效能更好,而且在配置動態障礙物與多種代理類型等複雜情境時容易得多。

快速概覽: NavigationRegion 定義代理可以行走的範圍。NavigationAgent 負責處理個別實體的路徑尋找。NavigationServer 在幕後管理所有事務。

核心概念

  • NavigationRegion2D / NavigationRegion3D — 使用 NavigationPolygon(2D)或 NavigationMesh(3D)定義可行走區域。你可以擁有多個區域;伺服器會自動將它們合併。
  • NavigationAgent2D / NavigationAgent3D — 附加到 CharacterBody 上以處理路徑尋找。它會計算下一個路徑位置、處理避讓,並在導航完成時發出訊號。
  • NavigationServer2D / NavigationServer3D — 管理所有導航資料的單例(Singleton)。你很少直接與它互動,但它負責處理地圖更新、路徑查詢與區域連接。

設定 2D 導航

步驟 1:加入 NavigationRegion2D

  1. 在你的場景中加入一個 NavigationRegion2D 節點。
  2. 在檢視器(Inspector)中建立一個新的 NavigationPolygon 資源。
  3. 在 2D 編輯器中繪製可行走的多邊形。此多邊形定義代理可以行走的範圍。

TileMap 整合: NavigationPolygon 可以從 TileMap 資料烘焙產生。如果你的圖塊在 TileSet 中定義了導航多邊形,只要加入一個 NavigationRegion2D 並烘焙 — 它便會依照你的圖塊佈局自動產生可行走區域。

加入 NavigationAgent2D

將 NavigationAgent2D 加為你的 CharacterBody2D 的子節點。以下是一份完整的移動程式碼:

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    # Wait for the navigation map to be ready
    await get_tree().physics_frame

    nav_agent.path_desired_distance = 4.0
    nav_agent.target_desired_distance = 4.0

func set_target(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

重要: NavigationServer 需要一個物理影格來完成同步。在 _ready() 中設定第一個目標位置之前,請務必執行 await get_tree().physics_frame,否則代理可能找不到有效路徑。

NavigationAgent2D 的主要屬性

  • target_position — 代理想要前往的位置
  • path_desired_distance — 代理需要多接近每個路徑點,才會前進至下一個
  • target_desired_distance — 代理需要多接近目標,才會將導航視為完成
  • max_speed — 用於避讓計算(不會限制你的移動程式碼)

3D 導航

3D 導航系統的運作方式與 2D 完全相同,只是使用不同的節點類型:

  • NavigationRegion3D + NavigationMesh
  • NavigationAgent3D
  • NavigationServer3D
extends CharacterBody3D

@export var speed: float = 5.0
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D

func _ready() -> void:
    await get_tree().physics_frame
    nav_agent.path_desired_distance = 0.5
    nav_agent.target_desired_distance = 0.5

func set_target(target_pos: Vector3) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    velocity.y -= 9.8 * delta  # Apply gravity
    move_and_slide()

烘焙導航網格

在 3D 中,你通常會從關卡幾何體烘焙導航網格,而非手動繪製。

烘焙設定

NavigationMesh 資源中的主要屬性:

  • Agent Radius — 代理與牆壁保持的距離。數值越大,產生的路徑越保守。
  • Agent Height — 可行走區域的最低天花板高度。
  • Agent Max Climb — 代理可以向上跨越的最大階梯高度(例如樓梯)。
  • Agent Max Slope — 可行走的最大坡度角度(以度為單位)。
# Bake navigation mesh at runtime
var nav_region: NavigationRegion3D = $NavigationRegion3D
nav_region.bake_navigation_mesh()

# Wait for baking to complete
await nav_region.bake_finished
print("Navigation mesh baked!")

導航圖層

導航圖層讓你能為不同的代理類型區隔可行走區域。例如,地面單位與飛行單位可以擁有不同的導航網格。

# Set navigation layers on the agent
nav_agent.set_navigation_layer_value(1, true)   # Ground layer
nav_agent.set_navigation_layer_value(2, false)  # Not flying layer

# Set navigation layers on the region
nav_region.set_navigation_layer_value(1, true)   # This region is for ground
nav_region.set_navigation_layer_value(2, false)  # Not for flying

一個典型的圖層設定:

  • Layer 1 — 地面單位(士兵、車輛)
  • Layer 2 — 飛行單位(無人機、鳥類)
  • Layer 3 — 大型單位(僅限寬敞通道)

避讓(Avoidance)

NavigationAgent 內建了本地避讓機制,用以防止代理彼此重疊。啟用避讓後,代理會計算出一個能閃避其他代理的安全速度。

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    await get_tree().physics_frame
    nav_agent.avoidance_enabled = true
    nav_agent.radius = 20.0
    nav_agent.max_speed = speed
    nav_agent.velocity_computed.connect(_on_velocity_computed)

func set_target(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    # Set the desired velocity — the agent will compute a safe one
    nav_agent.velocity = direction * speed

func _on_velocity_computed(safe_velocity: Vector2) -> void:
    velocity = safe_velocity
    move_and_slide()

運作原理: 你不直接使用速度,而是將 nav_agent.velocity 設定為你想要的速度。接著代理會透過 velocity_computed 訊號計算出一個能閃避鄰近代理的 safe_velocity。你在回呼(callback)中套用這個安全速度。

動態導航

你可以在執行時修改導航網格,以處理門、可破壞的牆壁或變化中的地形:

# Enable/disable a navigation region (e.g., open/close a door)
var door_region: NavigationRegion2D = $DoorNavigationRegion
door_region.enabled = false  # Block this path
door_region.enabled = true   # Open this path

# Add a navigation obstacle (blocks agent paths)
var obstacle := NavigationObstacle2D.new()
obstacle.radius = 30.0
add_child(obstacle)

# Re-bake after level changes (3D)
nav_region.bake_navigation_mesh()
await nav_region.bake_finished

效能提示: 在執行時烘焙 3D 導航網格的成本很高。對於 3D 中的動態障礙物,建議使用 NavigationObstacle3D 而非重新烘焙。在 2D 中,你可以低成本地切換 NavigationRegion2D.enabled

常見模式

追蹤玩家的敵人 AI

extends CharacterBody2D

@export var speed: float = 150.0
@export var chase_range: float = 300.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var player: Node2D

func _ready() -> void:
    await get_tree().physics_frame
    player = get_tree().get_first_node_in_group("player")

func _physics_process(delta: float) -> void:
    if not player:
        return

    var distance := global_position.distance_to(player.global_position)
    if distance > chase_range:
        return  # Too far, don't chase

    # Update target every frame (or throttle to every N frames)
    nav_agent.target_position = player.global_position

    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

NPC 巡邏路線

extends CharacterBody2D

@export var speed: float = 100.0
@export var patrol_points: Array[Vector2] = []
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var current_patrol_index: int = 0

func _ready() -> void:
    await get_tree().physics_frame
    if patrol_points.size() > 0:
        nav_agent.target_position = patrol_points[0]

func _physics_process(delta: float) -> void:
    if patrol_points.size() == 0:
        return

    if nav_agent.is_navigation_finished():
        # Move to next patrol point
        current_patrol_index = (current_patrol_index + 1) % patrol_points.size()
        nav_agent.target_position = patrol_points[current_patrol_index]
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

點擊移動(俯視角)

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    await get_tree().physics_frame

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed:
        nav_agent.target_position = get_global_mouse_position()

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

Godot 3 到 Godot 4 的變更

Godot 3 Godot 4
Navigation2D 節點 已移除。請改用 NavigationRegion2D + NavigationAgent2D
Navigation.get_simple_path() NavigationServer2D.map_get_path()
手動路徑追蹤 NavigationAgent.get_next_path_position() 處理
無內建避讓 NavigationAgent.avoidance_enabled
無導航圖層 每張地圖 32 個導航圖層
無障礙物 NavigationObstacle2D/3D