Navigation — NavigationServer and AI
NavigationRegion3D, NavigationAgent3D — pathfinding and a chasing enemy.
Navigation in Godot 4 consists of NavigationServer3D (the low-level server) and three high-level nodes: NavigationRegion3D, NavigationAgent3D, NavigationLink3D.
The Idea
A NavigationMesh is a simplified polygonal “walkability map.” It is baked from the scene’s collisions. Agents pathfind across this map (A* under the hood).
World
├── Ground (StaticBody3D + MeshInstance3D)
├── Walls / Platforms
└── NavigationRegion3D
└── navigation_mesh: NavigationMesh (baked)
Enemy
├── (model + collider)
└── NavigationAgent3D (target_position = player_position)
Baking the NavigationMesh
- Assemble the static geometry.
- Add a NavigationRegion3D node to the scene.
- In the Inspector, create a new NavigationMesh resource on this node.
- The Bake NavigationMesh button in the top toolbar → baking.
In Geometry → Source Geometry Mode on the NavigationMesh you choose what to build the mesh from:
SOURCE_GEOMETRY_ROOT_NODE_CHILDREN— processes the children of the NavigationRegion3D (place the static geometry inside the region).SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN— nodes from a given group + their children.SOURCE_GEOMETRY_GROUPS_EXPLICIT— only the nodes from the group.
NavigationMesh parameters:
agent_radius— the agent’s radius (offset from walls).agent_height— the agent’s height.agent_max_climb— the maximum step height the agent will step over.agent_max_slope— the maximum slope angle in degrees.cell_size/cell_height— the mesh precision.
If you have several NavigationRegion3D nodes, they connect automatically at their borders. Convenient for modular levels: each room is its own region.
NavigationAgent3D — Movement Across the Mesh
NavigationAgent3D is a child node of the body that needs to walk across the mesh. It does not move
the body itself; it computes the next path point:
extends CharacterBody3D
@export var speed: float = 4.0
@export var target: Node3D
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
nav_agent.path_desired_distance = 0.5
nav_agent.target_desired_distance = 1.0
func _physics_process(delta: float) -> void:
if target == null:
return
nav_agent.target_position = target.global_position
if nav_agent.is_navigation_finished():
return
var next_point := nav_agent.get_next_path_position()
var direction := (next_point - global_position).normalized()
velocity.x = direction.x * speed
velocity.z = direction.z * speed
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
The main methods:
get_next_path_position()— where to go this tick.is_navigation_finished()— the path is complete.is_target_reachable()— whether a path to the target exists.distance_to_target()— the remaining distance.
Signals:
target_reached— the target was reached.waypoint_reached(details)— an intermediate point was reached.navigation_finished— the path was traversed completely.velocity_computed(safe_velocity)— when avoidance is enabled.
Avoidance — Agents Stepping Aside
If there are many agents on the scene heading to one point, without avoidance they will pile up
into a “stack.” Enable avoidance_enabled = true on the NavigationAgent3D — RVO2 (reciprocal
velocity obstacles) will kick in.
nav_agent.avoidance_enabled = true
nav_agent.radius = 0.6 # avoidance radius
nav_agent.neighbor_distance = 5.0 # we see neighbors within this radius
nav_agent.velocity_computed.connect(_on_safe_velocity)
func _on_safe_velocity(safe_vel: Vector3) -> void:
# Use safe_vel instead of the raw velocity
velocity.x = safe_vel.x
velocity.z = safe_vel.z
move_and_slide()
func _physics_process(delta: float) -> void:
# ...
nav_agent.set_velocity(desired_velocity) # we pass the desired one — we get the safe one via the signal
NavigationLink3D — Jumps and Teleports
A NavigationMesh will not understand “you can jump across a gap here.” For that there is NavigationLink3D: a direct connection between two points. Agents travel along it like along a regular graph edge.
In the Inspector:
start_position+end_position— the link’s endpoints.bidirectional— whether it is two-way or not.
When traversing such an edge, the agent moves geometrically straight — which means you need to perform the “jump” (animation + physics) yourself in a script, by detecting the passage through the link.
NavigationObstacle3D — Dynamic Obstacles
Doors, crates, fallen trees — dynamic obstacles. Two modes:
- Without
affect_navigation_mesh— simply pushes agents away via avoidance. - With
affect_navigation_mesh = true— “carves” a hole in the navmesh (requires re-baking the region on the fly). Expensive, but precise.
A Simple Enemy with a State Machine
The full example from the Unity NavMesh chapter, rewritten for Godot:
extends CharacterBody3D
class_name EnemyChaser
enum State { PATROL, CHASE, ATTACK }
@export var patrol_points: Array[Node3D] = []
@export var player: Node3D
@export var speed: float = 3.0
@export var sight_range: float = 12.0
@export var attack_range: float = 2.0
@export var attack_cooldown: float = 1.2
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
var state: State = State.PATROL
var patrol_index: int = 0
var next_attack_time: float = 0.0
func _ready() -> void:
if patrol_points.size() > 0:
nav_agent.target_position = patrol_points[0].global_position
func _physics_process(delta: float) -> void:
var dist_to_player = global_position.distance_to(player.global_position)
match state:
State.PATROL:
if dist_to_player < sight_range and _has_line_of_sight():
state = State.CHASE
elif nav_agent.is_navigation_finished():
patrol_index = (patrol_index + 1) % patrol_points.size()
nav_agent.target_position = patrol_points[patrol_index].global_position
State.CHASE:
nav_agent.target_position = player.global_position
if dist_to_player < attack_range:
state = State.ATTACK
elif dist_to_player > sight_range * 1.5:
state = State.PATROL
State.ATTACK:
nav_agent.target_position = global_position # stand still
var now = Time.get_ticks_msec() / 1000.0
if now >= next_attack_time:
_attack()
next_attack_time = now + attack_cooldown
if dist_to_player > attack_range + 0.5:
state = State.CHASE
_move(delta)
func _move(delta: float) -> void:
if state != State.ATTACK and not nav_agent.is_navigation_finished():
var next_point = nav_agent.get_next_path_position()
var dir = (next_point - global_position).normalized()
velocity.x = dir.x * speed
velocity.z = dir.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
func _has_line_of_sight() -> bool:
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
global_position + Vector3.UP, player.global_position
)
query.exclude = [self]
var hit = space.intersect_ray(query)
return hit.is_empty() or hit.collider == player
func _attack() -> void:
if player.has_method("take_damage"):
player.take_damage(10)
Note the thresholds: entry into CHASE at sight_range, exit at sight_range * 1.5. This is
hysteresis, which keeps the enemy from “jittering” between states at the boundary. The same
technique as in the Unity chapter.
The next chapter covers particles.