Automated summary
This skill provides GDScript patterns for implementing NPC movement and decision-making in Godot 4 using NavigationAgent2D/3D, NavigationObstacle, and path smoothing.
Strengths
- Complete, directly reusable code snippets for both CharacterBody2D and CharacterBody3D, including waiting for the physics frame before setting a target
- Covers dynamic navigation obstacles (doors, barriers) that can be toggled on and off at runtime
- Demonstrates Catmull-Rom spline path smoothing combined with NavigationServer path optimization flags
- Handles 3D-specific details such as flattening direction to the ground plane and smooth rotation with lerp_angle
Limitations
- The provided document is truncated: the steering behavior and behavior system sections are announced but missing
- No guidance on setting up navigation regions or baking navigation meshes in the editor
- Does not address performance with many simultaneous agents or visual debugging of paths
Use it when writing or reviewing Godot 4 code that moves NPCs along computed paths with dynamic obstacle avoidance.
Avoid it for projects on another engine or on older Godot 3.x, where the NavigationServer APIs differ.
Security analysis
SafeThe skill contains only Godot GDScript code examples and explanations for navigation and AI. It does not involve any shell commands, network operations, file deletion, or data exfiltration. Allowed tools are limited to file reading and editing, which are safe in this context.
No concerns found
How this analysis is madeExamples
Write a Godot 4 3D enemy script that uses NavigationAgent3D to chase the player, stays on the ground plane, and turns smoothly toward its movement direction.In my Godot 4 game, NPCs should path around a closed door. Show me how to set up a NavigationObstacle3D and disable it when the door opens.My Godot 4 NPCs move in jagged lines along the navigation path. Add Catmull-Rom path smoothing using NavigationServer2D.map_get_path to my CharacterBody2D script.name: navigation-ai description: Implements AI navigation and pathfinding using NavigationServer, behavior systems, and steering behaviors. Use when building NPC movement and decision-making. allowed-tools: Read, Write, Edit, Glob, Grep
Godot Navigation & AI
When implementing AI movement and decision-making, use these patterns for intelligent NPC behavior.
Navigation Basics
NavigationAgent2D Setup
extends CharacterBody2D
@export var speed := 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
func _ready() -> void:
# Configure navigation agent
nav_agent.path_desired_distance = 4.0
nav_agent.target_desired_distance = 4.0
nav_agent.avoidance_enabled = true
# Wait for navigation map to be ready
await get_tree().physics_frame
set_movement_target(target_position)
func set_movement_target(target: Vector2) -> void:
nav_agent.target_position = target
func _physics_process(_delta: float) -> void:
if nav_agent.is_navigation_finished():
return
var next_position := nav_agent.get_next_path_position()
var direction := global_position.direction_to(next_position)
velocity = direction * speed
move_and_slide()
NavigationAgent3D Setup
extends CharacterBody3D
@export var speed := 5.0
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
nav_agent.path_desired_distance = 0.5
nav_agent.target_desired_distance = 0.5
nav_agent.avoidance_enabled = true
nav_agent.radius = 0.5
nav_agent.height = 2.0
func set_movement_target(target: Vector3) -> void:
nav_agent.target_position = target
func _physics_process(_delta: float) -> void:
if nav_agent.is_navigation_finished():
return
var next_position := nav_agent.get_next_path_position()
var direction := global_position.direction_to(next_position)
# Keep on ground plane
direction.y = 0
direction = direction.normalized()
velocity = direction * speed
# Rotate to face movement direction
if direction.length() > 0.1:
var target_rotation := atan2(direction.x, direction.z)
rotation.y = lerp_angle(rotation.y, target_rotation, 0.1)
move_and_slide()
Dynamic Navigation Obstacles
extends Node3D
@onready var obstacle: NavigationObstacle3D = $NavigationObstacle3D
func _ready() -> void:
# Configure obstacle
obstacle.radius = 1.0
obstacle.height = 2.0
obstacle.avoidance_enabled = true
func open_door() -> void:
# Disable obstacle when door opens
obstacle.avoidance_enabled = false
func close_door() -> void:
obstacle.avoidance_enabled = true
Advanced Navigation
Path Smoothing
extends CharacterBody2D
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var path: PackedVector2Array
var path_index := 0
func set_target(target: Vector2) -> void:
nav_agent.target_position = target
# Get raw path
path = NavigationServer2D.map_get_path(
nav_agent.get_navigation_map(),
global_position,
target,
true # optimize
)
# Smooth path
path = smooth_path(path)
path_index = 0
func smooth_path(raw_path: PackedVector2Array) -> PackedVector2Array:
if raw_path.size() < 3:
return raw_path
var smoothed := PackedVector2Array()
smoothed.append(raw_path[0])
# Use Catmull-Rom spline interpolation
for i in range(raw_path.size() - 1):
var p0 := raw_path[max(i - 1, 0)]
var p1 := raw_path[i]
var p2 := raw_path[min(i + 1, raw_path.size() - 1)]
var p3 := raw_path[min(i + 2, raw_path.size() - 1)]
for t in range(1, 5): # 4 subdivisions
var point := catmull_rom(p0, p1, p2, p3, t / 5.0)
smoothed.append(point)
smoothed.append(raw_path[-1])
return smoothed
func catmull_rom(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2:
var t2 := t * t
var t3 := t2 * t
return 0.5 * (
(2.0 * p1) +
(-p0 + p2) * t +
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
)
Navigation Layers
extends CharacterBody2D
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
# Navigation layers:
# Layer 1: Ground (all units)
# Layer 2: Flying (flying units only)
# Layer 3: Water (aquatic units only)
enum UnitType { GROUND, FLYING, AQUATIC }
func setup_navigation(unit_type: UnitType) -> void:
match unit_type:
UnitType.GROUND:
nav_agent.navigation_layers = 1 # Only ground
UnitType.FLYING:
nav_agent.navigation_layers = 3 # Ground + Flying (1 + 2)
UnitType.AQUATIC:
nav_agent.navigation_layers = 5 # Ground + Water (1 + 4)
Patrol System
extends CharacterBody2D
@export var patrol_points: Array[Marker2D]
@export var wait_time := 2.0
var current_patrol_index := 0
var is_waiting := false
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
@onready var wait_timer: Timer = $WaitTimer
func _ready() -> void:
wait_timer.wait_time = wait_time
wait_timer.timeout.connect(_on_wait_finished)
if patrol_points.size() > 0:
move_to_patrol_point()
func move_to_patrol_point() -> void:
var target := patrol_points[current_patrol_index].global_position
nav_agent.target_position = target
func _physics_process(_delta: float) -> void:
if is_waiting or nav_agent.is_navigation_finished():
return
var next_pos := nav_agent.get_next_path_position()
velocity = global_position.direction_to(next_pos) * speed
move_and_slide()
# Check if reached patrol point
if nav_agent.is_target_reached():
is_waiting = true
wait_timer.start()
func _on_wait_finished() -> void:
is_waiting = false
current_patrol_index = (current_patrol_index + 1) % patrol_points.size()
move_to_patrol_point()
Steering Behaviors
Steering Behavior Base
class_name SteeringBehavior
extends RefCounted
var agent: CharacterBody2D
var weight := 1.0
func _init(a: CharacterBody2D, w: float = 1.0) -> void:
agent = a
weight = w
func calculate() -> Vector2:
return Vector2.ZERO
Seek and Flee
class_name SeekBehavior
extends SteeringBehavior
var target: Vector2
func calculate() -> Vector2:
var desired := (target - agent.global_position).normalized() * agent.max_speed
return (desired - agent.velocity) * weight
class_name FleeBehavior
extends SteeringBehavior
var threat: Vector2
var panic_distance := 200.0
func calculate() -> Vector2:
var distance := agent.global_position.distance_to(threat)
if distance > panic_distance:
return Vector2.ZERO
var desired := (agent.global_position - threat).normalized() * agent.max_speed
return (desired - agent.velocity) * weight
Arrival
class_name ArrivalBehavior
extends SteeringBehavior
var target: Vector2
var slowing_radius := 100.0
func calculate() -> Vector2:
var to_target := target - agent.global_position
var distance := to_target.length()
if distance < 1:
return -agent.velocity # Stop
var desired_speed: float
if distance < slowing_radius:
desired_speed = agent.max_speed * (distance / slowing_radius)
else:
desired_speed = agent.max_speed
var desired := to_target.normalized() * desired_speed
return (desired - agent.velocity) * weight
Pursuit and Evade
class_name PursuitBehavior
extends SteeringBehavior
var target: CharacterBody2D
func calculate() -> Vector2:
var to_target := target.global_position - agent.global_position
var distance := to_target.length()
# Predict future position
var look_ahead := distance / agent.max_speed
var future_position := target.global_position + target.velocity * look_ahead
var desired := (future_position - agent.global_position).normalized() * agent.max_speed
return (desired - agent.velocity) * weight
class_name EvadeBehavior
extends SteeringBehavior
var threat: CharacterBody2D
var panic_distance := 300.0
func calculate() -> Vector2:
var to_threat := threat.global_position - agent.global_position
var distance := to_threat.length()
if distance > panic_distance:
return Vector2.ZERO
var look_ahead := distance / agent.max_speed
var future_position := threat.global_position + threat.velocity * look_ahead
var desired := (agent.global_position - future_position).normalized() * agent.max_speed
return (desired - agent.velocity) * weight
Obstacle Avoidance
class_name ObstacleAvoidanceBehavior
extends SteeringBehavior
var look_ahead := 100.0
var avoid_force := 50.0
func calculate() -> Vector2:
if agent.velocity.length() < 0.1:
return Vector2.ZERO
var direction := agent.velocity.normalized()
# Cast ray ahead
var space := agent.get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(
agent.global_position,
agent.global_position + direction * look_ahead
)
query.exclude = [agent]
var result := space.intersect_ray(query)
if result.is_empty():
return Vector2.ZERO
# Calculate avoidance
var hit_point: Vector2 = result.position
var hit_normal: Vector2 = result.normal
return hit_normal * avoid_force * weight
Flocking (Separation, Alignment, Cohesion)
class_name FlockingBehavior
extends SteeringBehavior
var neighbors: Array[CharacterBody2D]
var separation_radius := 50.0
var neighbor_radius := 100.0
var separation_weight := 1.5
var alignment_weight := 1.0
var cohesion_weight := 1.0
func set_neighbors(n: Array[CharacterBody2D]) -> void:
neighbors = n
func calculate() -> Vector2:
var separation := calculate_separation()
var alignment := calculate_alignment()
var cohesion := calculate_cohesion()
return (separation * separation_weight +
alignment * alignment_weight +
cohesion * cohesion_weight) * weight
func calculate_separation() -> Vector2:
var steering := Vector2.ZERO
var count := 0
for neighbor in neighbors:
var distance := agent.global_position.distance_to(neighbor.global_position)
if distance < separation_radius and distance > 0:
var diff := agent.global_position - neighbor.global_position
diff = diff.normalized() / distance
steering += diff
count += 1
if count > 0:
steering /= count
steering = steering.normalized() * agent.max_speed
steering -= agent.velocity
return steering
func calculate_alignment() -> Vector2:
var average_velocity := Vector2.ZERO
var count := 0
for neighbor in neighbors:
var distance := agent.global_position.distance_to(neighbor.global_position)
if distance < neighbor_radius:
average_velocity += neighbor.velocity
count += 1
if count > 0:
average_velocity /= count
average_velocity = average_velocity.normalized() * agent.max_speed
return average_velocity - agent.velocity
return Vector2.ZERO
func calculate_cohesion() -> Vector2:
var center := Vector2.ZERO
var count := 0
for neighbor in neighbors:
var distance := agent.global_position.distance_to(neighbor.global_position)
if distance < neighbor_radius:
center += neighbor.global_position
count += 1
if count > 0:
center /= count
var desired := (center - agent.global_position).normalized() * agent.max_speed
return desired - agent.velocity
return Vector2.ZERO
Steering Agent Controller
extends CharacterBody2D
@export var max_speed := 200.0
@export var max_force := 10.0
var behaviors: Array[SteeringBehavior] = []
func add_behavior(behavior: SteeringBehavior) -> void:
behaviors.append(behavior)
func remove_behavior(behavior: SteeringBehavior) -> void:
behaviors.erase(behavior)
func _physics_process(delta: float) -> void:
var steering := Vector2.ZERO
for behavior in behaviors:
steering += behavior.calculate()
# Limit force
if steering.length() > max_force:
steering = steering.normalized() * max_force
# Apply steering
velocity += steering * delta
# Limit speed
if velocity.length() > max_speed:
velocity = velocity.normalized() * max_speed
move_and_slide()
AI Decision Making
Simple FSM AI
extends CharacterBody2D
enum AIState { IDLE, PATROL, CHASE, ATTACK, FLEE }
var current_state: AIState = AIState.IDLE
var target: Node2D
@export var detection_range := 300.0
@export var attack_range := 50.0
@export var flee_health_threshold := 20.0
var health := 100.0
func _physics_process(delta: float) -> void:
update_state()
match current_state:
AIState.IDLE:
process_idle(delta)
AIState.PATROL:
process_patrol(delta)
AIState.CHASE:
process_chase(delta)
AIState.ATTACK:
process_attack(delta)
AIState.FLEE:
process_flee(delta)
func update_state() -> void:
var player := find_player()
if health < flee_health_threshold:
change_state(AIState.FLEE)
return
if player:
var distance := global_position.distance_to(player.global_position)
if distance < attack_range:
change_state(AIState.ATTACK)
elif distance < detection_range:
target = player
change_state(AIState.CHASE)
else:
change_state(AIState.PATROL)
func change_state(new_state: AIState) -> void:
if current_state == new_state:
return
# Exit current state
match current_state:
AIState.ATTACK:
$AttackTimer.stop()
current_state = new_state
# Enter new state
match new_state:
AIState.ATTACK:
$AttackTimer.start()
Utility AI
class_name UtilityAI
extends Node
class Action:
var name: String
var considerations: Array[Consideration]
func get_score() -> float:
if considerations.is_empty():
return 0.0
var score := 1.0
for consideration in considerations:
score *= consideration.get_score()
# Compensation factor for multiple considerations
var mod := 1.0 - (1.0 / considerations.size())
var make_up := (1.0 - score) * mod
return score + (make_up * score)
class Consideration:
var curve: Curve # Response curve
var input_func: Callable
func get_score() -> float:
var input := input_func.call()
return curve.sample(input)
var actions: Array[Action] = []
func get_best_action() -> Action:
var best_action: Action
var best_score := -1.0
for action in actions:
var score := action.get_score()
if score > best_score:
best_score = score
best_action = action
return best_action
# Example setup
func setup_combat_ai(enemy: CharacterBody2D) -> void:
var attack_action := Action.new()
attack_action.name = "attack"
# More likely to attack when:
# - Target is close
# - We have high health
var distance_consideration := Consideration.new()
distance_consideration.curve = preload("res://curves/distance_attack.tres")
distance_consideration.input_func = func():
return enemy.global_position.distance_to(target.global_position) / 500.0
var health_consideration := Consideration.new()
health_consideration.curve = preload("res://curves/health_attack.tres")
health_consideration.input_func = func():
return enemy.health / enemy.max_health
attack_action.considerations = [distance_consideration, health_consideration]
actions.append(attack_action)
Line of Sight
Raycast-Based Vision
extends CharacterBody2D
@export var view_distance := 300.0
@export var view_angle := 90.0 # Degrees
func can_see(target: Node2D) -> bool:
var to_target := target.global_position - global_position
var distance := to_target.length()
# Check distance
if distance > view_distance:
return false
# Check angle
var facing := Vector2.from_angle(rotation)
var angle := rad_to_deg(facing.angle_to(to_target.normalized()))
if abs(angle) > view_angle / 2:
return false
# Check line of sight
var space := get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(
global_position,
target.global_position
)
query.exclude = [self]
query.collision_mask = 1 # Walls layer
var result := space.intersect_ray(query)
if result.is_empty():
return true
# Check if we hit the target
return result.collider == target
func get_visible_targets(group: String) -> Array[Node2D]:
var visible: Array[Node2D] = []
for node in get_tree().get_nodes_in_group(group):
if node is Node2D and can_see(node):
visible.append(node)
return visible
Vision Cone Visualization
extends Node2D
@export var view_distance := 300.0
@export var view_angle := 90.0
@export var cone_color := Color(1, 1, 0, 0.3)
func _draw() -> void:
var half_angle := deg_to_rad(view_angle / 2)
var points := PackedVector2Array()
points.append(Vector2.ZERO)
var segments := 20
for i in range(segments + 1):
var angle := -half_angle + (i / float(segments)) * view_angle * deg_to_rad(1)
var point := Vector2(cos(angle), sin(angle)) * view_distance
points.append(point)
draw_colored_polygon(points, cone_color)
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/godot-navigation-ai-mmbizjxk' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/godot-navigation-ai-mmbizjxk/download' --output '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md.part' && mv '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md.part' '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
Tools declared by the author (check availability in your agent): Read, Write, Edit, Glob, Grep
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.
Claude Code
test -f '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md' && test -s '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/navigation-ai' && cp -R '.skill-downloads/godot-navigation-ai-mmbizjxk' '.claude/skills/navigation-ai'Cursor
test -f '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md' && test -s '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/navigation-ai' && cp -R '.skill-downloads/godot-navigation-ai-mmbizjxk' '.cursor/skills/navigation-ai'Codex
test -f '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md' && test -s '.skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/navigation-ai' && cp -R '.skill-downloads/godot-navigation-ai-mmbizjxk' '.agents/skills/navigation-ai'OpenSkills
OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.
Official OpenSkills installationopenskills install './.skill-downloads/godot-navigation-ai-mmbizjxk'openskills read 'godot-navigation-ai-mmbizjxk'Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.
For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.
openskills syncManual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/godot-navigation-ai-mmbizjxk/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.