Introduction

Godot 4 dispose d'un système de navigation robuste bâti sur NavigationServer2D/3D, les nœuds NavigationRegion et les nœuds NavigationAgent. C'est une nette amélioration par rapport au système de Godot 3 — plus flexible, plus performant et bien plus facile à configurer pour des scénarios complexes comme les obstacles dynamiques et les multiples types d'agents.

Aperçu rapide : NavigationRegion définit où les agents peuvent se déplacer. NavigationAgent gère la recherche de chemin pour chaque entité. NavigationServer orchestre tout en arrière-plan.

Concepts de base

  • NavigationRegion2D / NavigationRegion3D — définit une zone traversable avec un NavigationPolygon (2D) ou un NavigationMesh (3D). Tu peux avoir plusieurs régions ; le serveur les fusionne automatiquement.
  • NavigationAgent2D / NavigationAgent3D — à attacher à un CharacterBody pour gérer la recherche de chemin. Il calcule la prochaine position du chemin, gère l'évitement et émet des signaux lorsque la navigation est terminée.
  • NavigationServer2D / NavigationServer3D — singleton qui gère toutes les données de navigation. Tu interagis rarement directement avec lui, mais il prend en charge les mises à jour de carte, les requêtes de chemin et les connexions entre régions.

Configurer la navigation 2D

Étape 1 : ajouter un NavigationRegion2D

  1. Ajoute un nœud NavigationRegion2D à ta scène.
  2. Dans l'inspecteur, crée une nouvelle ressource NavigationPolygon.
  3. Dessine le polygone traversable dans l'éditeur 2D. Le polygone définit où les agents peuvent se déplacer.

Intégration TileMap : un NavigationPolygon peut être généré (baked) à partir des données de TileMap. Si tes tuiles ont des polygones de navigation définis dans le TileSet, ajoute simplement un NavigationRegion2D et lance le baking — il génère automatiquement la zone traversable à partir de la disposition de tes tuiles.

Ajouter un NavigationAgent2D

Ajoute un NavigationAgent2D comme enfant de ton CharacterBody2D. Voici un script de déplacement complet :

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()

Important : le NavigationServer a besoin d'une frame de physique pour se synchroniser. Exécute toujours await get_tree().physics_frame avant de définir la première position cible dans _ready(), sinon l'agent risque de ne pas trouver de chemin valide.

Propriétés clés de NavigationAgent2D

  • target_position — où l'agent souhaite aller
  • path_desired_distance — à quelle distance l'agent doit être de chaque point du chemin pour passer au suivant
  • target_desired_distance — à quelle distance l'agent doit être de la cible pour que la navigation soit considérée comme terminée
  • max_speed — utilisée pour les calculs d'évitement (ne limite pas ton code de déplacement)

Navigation 3D

Le système de navigation 3D fonctionne de façon identique au système 2D, mais avec des types de nœuds différents :

  • 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()

Baking des meshes de navigation

En 3D, tu génères habituellement le mesh de navigation à partir de la géométrie de ton niveau plutôt que de le dessiner à la main.

Configuration du baking

Propriétés clés de la ressource NavigationMesh :

  • Agent Radius — à quelle distance les agents restent des murs. Des valeurs plus grandes produisent des chemins plus prudents.
  • Agent Height — hauteur de plafond minimale pour les zones traversables.
  • Agent Max Climb — hauteur de marche maximale que les agents peuvent gravir (par exemple des escaliers).
  • Agent Max Slope — angle de pente maximal traversable, en degrés.
# 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!")

Calques de navigation

Les calques de navigation te permettent de séparer les zones traversables pour différents types d'agents. Par exemple, les unités au sol et les unités volantes peuvent avoir des meshes de navigation distincts.

# 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

Une configuration de calques typique :

  • Layer 1 — unités au sol (soldats, véhicules)
  • Layer 2 — unités volantes (drones, oiseaux)
  • Layer 3 — grandes unités (couloirs larges uniquement)

Évitement (Avoidance)

NavigationAgent dispose d'un évitement local intégré pour empêcher les agents de se chevaucher. Lorsque l'évitement est activé, l'agent calcule une vitesse sûre qui esquive les autres agents.

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()

Comment ça marche : au lieu d'utiliser la vitesse directement, tu définis nav_agent.velocity sur ta vitesse souhaitée. L'agent calcule ensuite, via le signal velocity_computed, une safe_velocity qui esquive les agents proches. Tu appliques cette vitesse sûre dans le callback.

Navigation dynamique

Tu peux modifier le mesh de navigation à l'exécution pour gérer les portes, les murs destructibles ou un terrain changeant :

# 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

Note sur les performances : générer un mesh de navigation 3D à l'exécution est coûteux. Pour les obstacles dynamiques en 3D, privilégie NavigationObstacle3D plutôt qu'un nouveau baking. En 2D, tu peux basculer NavigationRegion2D.enabled à faible coût.

Motifs courants

IA d'ennemi qui pourchasse le joueur

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()

Trajets de patrouille des PNJ

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()

Clic pour se déplacer (vue de dessus)

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()

Changements de Godot 3 à Godot 4

Godot 3 Godot 4
Nœud Navigation2D Supprimé. Utilise NavigationRegion2D + NavigationAgent2D
Navigation.get_simple_path() NavigationServer2D.map_get_path()
Suivi manuel du chemin NavigationAgent.get_next_path_position() s'en charge
Pas d'évitement intégré NavigationAgent.avoidance_enabled
Pas de calques de navigation 32 calques de navigation par carte
Pas d'obstacles NavigationObstacle2D/3D