Installs into .claude/skills of the current project.
Are you the author of Ai Navigation?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jame581-ai-navigation)
---
name: ai-navigation
description: Use when implementing AI movement — NavigationAgent2D/3D, steering behaviors, behavior trees, and patrol patterns
---
# AI Navigation in Godot 4.3+
Cover NavigationAgent2D/3D, steering behaviors, behavior trees, and patrol patterns. All examples target Godot 4.3+ with no deprecated APIs.
> **Related skills:** **state-machine** for AI state management, **component-system** for modular AI behaviors, **player-controller** for movement physics patterns, **math-essentials** for pathfinding vectors and steering math, **limboai** for BT + HSM with a visual editor, **beehave** for lightweight GDScript behavior trees. For a structured behavior tree (rather than steering/navigation), see the comparison tables in **limboai** and **beehave**.
---
## 1. Navigation Setup
### Scene Structure
```
World (Node2D or Node3D)
└── NavigationRegion2D (or NavigationRegion3D)
├── TileMapLayer / StaticBody2D (geometry)
└── Enemy (CharacterBody2D with NavigationAgent2D child)
```
### NavigationRegion2D / NavigationRegion3D
1. Add a **NavigationRegion2D** (or **NavigationRegion3D**) node to your scene.
2. Assign a **NavigationPolygon** (2D) or **NavigationMesh** (3D) resource to it.
3. Draw the walkable area in the NavigationPolygon editor, or configure the NavigationMesh bounds in 3D.
4. **Bake the mesh at edit time:** select the NavigationRegion node → click **Bake NavigationPolygon** (2D) or **Bake NavigationMesh** (3D) in the toolbar.
5. **Bake at runtime** when the world changes dynamically:
```gdscript
# 2D
$NavigationRegion2D.bake_navigation_polygon()
# 3D
$NavigationRegion3D.bake_navigation_mesh()
```
```csharp
// 2D
GetNode<NavigationRegion2D>("NavigationRegion2D").BakeNavigationPolygon();
// 3D
GetNode<NavigationRegion3D>("NavigationRegion3D").BakeNavigationMesh();
```
### Async Navigation Baking (Godot 4.4+)
Navigation baking can cause frame drops on large maps. Godot 4.4 supports baking on a background thread: pass `true` to `bake_navigation_polygon(true)` (2D) or `bake_navigation_mesh(true)` (3D) and connect the region's `bake_finished` signal (use `CONNECT_ONE_SHOT`) to know when the mesh is ready.
> See [references/async-baking.md](references/async-baking.md) for the full GDScript and C# background-thread bake examples (2D and 3D).
> **When to use async baking:** Procedurally generated levels, destructible terrain, or any scene where the navigation mesh must be rebuilt at runtime. The game continues running while the mesh bakes.
### Navigation Layers
Navigation layers let you separate walkable areas for different agent types (ground troops, flying units, large enemies).
```gdscript
# Assign layer bits on the NavigationRegion (Inspector or code)
# Layer 1 = ground, Layer 2 = air, Layer 3 = large
# On the NavigationAgent, set matching layers:
$NavigationAgent2D.navigation_layers = 1 # ground only
$NavigationAgent2D.navigation_layers = 2 # air only
$NavigationAgent2D.navigation_layers = 1 | 2 # both (bitwise OR)
```
```csharp
// Assign layer bits on the NavigationRegion (Inspector or code)
// Layer 1 = ground, Layer 2 = air, Layer 3 = large
var navAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
navAgent.NavigationLayers = 1; // ground only
navAgent.NavigationLayers = 2; // air only
navAgent.NavigationLayers = 1 | 2; // both (bitwise OR)
```
> Set `navigation_layers` on both the **NavigationRegion** and the **NavigationAgent** so they match. Mismatched layers are one of the most common reasons an agent finds no path.
---
## 2. NavigationAgent2D Basic Usage
### GDScript
```gdscript
extends CharacterBody2D
@export var speed: float = 120.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
func _ready() -> void:
# velocity_computed fires when avoidance calculates a safe velocity
nav_agent.velocity_computed.connect(_on_velocity_computed)
func _physics_process(delta: float) -> void:
if nav_agent.is_navigation_finished():
return
var next_pos: Vector2 = nav_agent.get_next_path_position()
var direction: Vector2 = (next_pos - global_position).normalized()
var desired_velocity: Vector2 = direction * speed
if nav_agent.avoidance_enabled:
# Hand desired velocity to the avoidance system; wait for the signal
nav_agent.velocity = desired_velocity
else:
velocity = desired_velocity
move_and_slide()
func _on_velocity_computed(safe_velocity: Vector2) -> void:
velocity = safe_velocity
move_and_slide()
func set_target(target_pos: Vector2) -> void:
nav_agent.target_position = target_pos
```
**Key NavigationAgent2D properties:**
| Property | Purpose |
|---|---|
| `target_position` | World-space destination |
| `path_desired_distance` | How close to each waypoint counts as reached (default 1) |
| `target_desired_distance` | How close to the final target counts as finished (default 10) |
| `avoidance_enabled` | Enable RVO obstacle avoidance |
| `radius` | Agent collision radius for avoidance |
| `time_horizon_agents` | Seconds of avoidance look-ahead (tune to reduce jitter) |
### C#
```csharp
using Godot;
public partial class Enemy2D : CharacterBody2D
{
[Export] public float Speed { get; set; } = 120f;
private NavigationAgent2D _navAgent;
public override void _Ready()
{
_navAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navAgent.VelocityComputed += OnVelocityComputed;
}
public override void _PhysicsProcess(double delta)
{
if (_navAgent.IsNavigationFinished()) return;
Vector2 nextPos = _navAgent.GetNextPathPosition();
Vector2 direction = (nextPos - GlobalPosition).Normalized();
Vector2 desiredVelocity = direction * Speed;
if (_navAgent.AvoidanceEnabled)
_navAgent.Velocity = desiredVelocity;
else
{
Velocity = desiredVelocity;
MoveAndSlide();
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
Velocity = safeVelocity;
MoveAndSlide();
}
public void SetTarget(Vector2 targetPos) => _navAgent.TargetPosition = targetPos;
}
```
---
## 3. NavigationAgent3D Basic Usage
A `CharacterBody3D` drives a `NavigationAgent3D` by calling `get_next_path_position()` each physics frame, flattening the direction on Y, and keeping gravity separate. With `avoidance_enabled` the desired velocity goes to `nav_agent.velocity` and movement happens in the `velocity_computed` handler; otherwise `move_and_slide()` is called directly. Targets are set through `target_position` and completion is checked with `is_navigation_finished()`.
> See [references/navigation-agent-3d.md](references/navigation-agent-3d.md) for the full GDScript and C# `NavigationAgent3D` enemy example.
---
## 4. Steering Behaviors
Lightweight per-frame calculations (seek, flee, arrive, wander) that produce natural-looking movement without a navigation mesh. Combine them by summing the returned vectors, or pick one and assign it to `velocity` each `_physics_process` tick.
> See [references/steering-behaviors.md](references/steering-behaviors.md) for the full GDScript and C# implementations of seek, flee, arrive (with deceleration ramp), and wander (with circle-projection jitter).
---
## 5. Patrol Patterns
A `NavigationAgent2D` plus an array of `Marker2D` waypoints and a `Timer` for the pause at each point produces a clean patrol loop. Cycle the index on `wait_timer.timeout`, set `nav_agent.target_position` to the next waypoint, and gate movement on `is_navigation_finished()`.
> See [references/patrol-patterns.md](references/patrol-patterns.md) for the full GDScript and C# waypoint-chain patrol with wait-timer pauses.
---
## 6. Behavior Tree Concept
A behavior tree (BT) is a tree of nodes evaluated every tick. Three core node types:
| Type | Succeeds when | Fails when |
|---|---|---|
| **Sequence** | all children succeed (AND) | any child fails |
| **Selector** | any child succeeds (OR) | all children fail |
| **Action** | the leaf action completes | the leaf reports failure |
Sequences model "do A then B then C". Selectors model "try A, else try B, else try C".
> See [references/behavior-trees.md](references/behavior-trees.md) for the full lightweight BT implementation (BTNode base + Sequence / Selector / Action) and a worked enemy that uses "chase OR patrol", in both GDScript and C#.
---
## 7. Chase + Attack Pattern
Combines NavigationAgent2D with a state machine. See the **state-machine** skill for the full FSM infrastructure.
### States
| State | Entry condition | Exit condition |
|---|---|---|
| PATROL | default / player escaped | player enters detect_range |
| CHASE | player in detect_range | player in attack_range OR player escaped |
| ATTACK | player in attack_range | player left attack_range |
> See [references/chase-attack.md](references/chase-attack.md) for the full GDScript and C# implementation (PATROL → CHASE → ATTACK transitions, attack cooldown timer, patrol-waypoint advancement, escape-range hand-off back to patrol).
> For larger projects, extract each state into its own node class using the **state-machine** skill and inject the `NavigationAgent2D` reference from the parent.
---
## 8. Dedicated 2D Navigation Server (Godot 4.5+)
Prior to Godot 4.5, `NavigationServer2D` was a thin frontend that delegated all work to the 3D navigation server internally. Godot 4.5 splits them into fully independent servers. The change is **transparent** — no API changes and no code migration is required — but it brings two practical benefits:
- **Performance:** 2D pathfinding no longer competes with 3D navigation for server resources. Large 2D scenes with many agents see lower CPU overhead.
- **Smaller exports for 2D-only games:** The 3D navigation server can be stripped from 2D-only export templates, reducing binary size.
```gdscript
# No code change needed — NavigationServer2D calls work identically.
# The split is internal; you continue using NavigationServer2D as before.
# Example: query a path directly via the server (unchanged API).
func get_path_to(target: Vector2) -> PackedVector2Array:
var map: RID = get_world_2d().get_navigation_map()
return NavigationServer2D.map_get_path(
map,
global_position,
target,
true # optimize path
)
```
```csharp
// No code change needed — NavigationServer2D calls work identically.
public PackedVector2Array GetPathTo(Vector2 target)
{
var map = GetWorld2D().GetNavigationMap();
return NavigationServer2D.MapGetPath(map, GlobalPosition, target, true);
}
```
> **2D-only projects:** In **Project Settings → Modules**, you can disable the `NavigationServer3D` module to reduce export size. This is only safe if no 3D navigation nodes (`NavigationRegion3D`, `NavigationAgent3D`) are used anywhere in the project.
---
## 9. Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| **Navigation mesh not baked** | Agent stands still; no path found | Bake the NavigationPolygon/NavigationMesh before running, or call `bake_navigation_polygon()` at runtime after scene loads |
| **`agent_radius` too large** | Agent can't fit through doorways or narrow corridors | Lower `radius` on NavigationAgent to be slightly less than half the passage width |
| **Avoidance jitter** | Agent stutters or oscillates when near other agents | Increase `time_horizon_agents` (try 2–4 s) or slightly lower `max_speed` on the agent |
| **Path recalculation too frequent** | CPU spike each frame; agents lag | Add a `Timer` (0.2–0.5 s) and only set `target_position` when the timer fires, not every physics frame |
| **Wrong navigation layer** | Agent ignores some regions or finds no path | Confirm `navigation_layers` bitmask matches between the NavigationRegion and the NavigationAgent |
| **Target set before NavigationServer is ready** | Path is empty on the first frame | Defer `target_position` assignment to `_ready()` or await `NavigationServer2D.map_changed` |
| **Gravity ignored in 3D** | Agent floats or sinks into the floor | Always accumulate `velocity.y` from gravity separately; only zero out X/Z from the nav direction |
| **Baking causes frame drop** | Synchronous bake on large maps blocks the main thread | Use async baking: `bake_navigation_mesh(true)` (Godot 4.4+); connect `bake_finished` signal |
> ⚠️ **Changed in Godot 4.7:** `NavigationServer3D.map_get_closest_point_normal(map, to_point)` now returns a normalized vector ([GH-119022](https://github.com/godotengine/godot/pull/119022)) — previously the returned surface normal could be unnormalized. Code that compensated by calling `normalized()` on the result keeps working; code that relied on the unnormalized magnitude breaks.
---
## 10. Checklist
- [ ] NavigationRegion2D/3D added to the scene with a NavigationPolygon/NavigationMesh resource
- [ ] Navigation mesh baked (edit-time or at runtime before the agent needs a path)
- [ ] `navigation_layers` bitmask matches between NavigationRegion and NavigationAgent
- [ ] `NavigationAgent2D`/`NavigationAgent3D` is a **child** of the enemy node
- [ ] `get_next_path_position()` called each physics frame, not `get_target_position()`
- [ ] `velocity_computed` signal connected when `avoidance_enabled` is `true`
- [ ] `is_navigation_finished()` checked before moving to avoid jitter at the destination
- [ ] Path target updated via a throttle timer rather than every frame when following a moving player
- [ ] `agent_radius` small enough to fit through the narrowest passage in the level
- [ ] Gravity applied independently of horizontal nav velocity (3D only)
- [ ] Large or dynamic maps use async baking (`bake_navigation_mesh(true)`) to avoid frame drops (Godot 4.4+)
- [ ] 2D-only projects on Godot 4.5+ can disable `NavigationServer3D` in Project Settings to reduce export size