Introduction: Why Game AI Needs Pathfinding
In game development, enemy and NPC movement significantly affects player experience quality. When walls, obstacles, and complex maps exist, AI needs to find the optimal path to destinations. This technique of "finding optimal paths" is pathfinding.
Godot Engine provides the NavigationAgent2D node to easily and efficiently implement this complex pathfinding processing.
How NavigationAgent2D Works and Basic Setup
Godot's navigation system consists of three main components:
| Element | Node Name | Role |
|---|---|---|
| Navigation Map | NavigationServer2D | Backend server managing navigable area data for the entire scene. |
| Navigable Area | NavigationRegion2D | Defines areas (navigation mesh) where AI can move on the map. |
| Pathfinding Agent | NavigationAgent2D | Requests path calculations and controls parent node (AI character) movement. |
| Dynamic Obstacles | NavigationObstacle2D | Defines dynamic obstacles like moving doors or other characters for agents to avoid. |
Basic Setup
To make NavigationAgent2D functional, you first need to define navigable areas on the map.
- Place
NavigationRegion2D: Add aNavigationRegion2Dnode to the scene tree. - Create
NavigationPolygon: In theNavigationRegion2Dinspector, create a newNavigationPolygonresource and edit it to cover the navigable parts of the map. - Prepare AI Character: Add a
NavigationAgent2Dnode as a child of the enemy AI's root node (e.g.,CharacterBody2D).
Practice 1: Player Pursuit by Enemy AI
Enemy AI chasing the player is the most fundamental pathfinding use case. With NavigationAgent2D, you can easily create AI that follows the shortest path while avoiding obstacles.
# EnemyAI.gd (Attached to CharacterBody2D)
extends CharacterBody2D
@export var speed: float = 250.0
# Reference to player node (set from inspector)
@export var player: Node2D
@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
func _ready() -> void:
# max_speed is a parameter for RVO (Reciprocal Velocity Obstacle) avoidance
# Actual movement speed is controlled by the speed variable in velocity calculation
navigation_agent.max_speed = speed
# Do nothing if player doesn't exist
if not is_instance_valid(player):
set_physics_process(false)
func _physics_process(delta: float) -> void:
# Do nothing more if destination is reached
if navigation_agent.is_navigation_finished():
velocity = Vector2.ZERO
move_and_slide()
return
# Set player position as target
# For performance, updating via Timer rather than every frame is preferable (see below)
navigation_agent.target_position = player.global_position
# Get the next point to move toward
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
# Calculate direction to that point
var direction: Vector2 = global_position.direction_to(next_path_position)
# Move along calculated direction
velocity = direction * speed
move_and_slide()
The core of this code is its simplicity: set navigation_agent.target_position to the player's coordinates, then move_and_slide() toward the coordinates returned by navigation_agent.get_next_path_position(). All complex path calculation and obstacle avoidance logic is handled internally by NavigationAgent2D.
Practice 2: Guard AI Patrolling a Fixed Route
Patrol behavior where enemy AI cycles through multiple points is also easily implemented with NavigationAgent2D. The is_navigation_finished() function for determining arrival plays a key role.
# PatrolAI.gd (Attached to CharacterBody2D)
extends CharacterBody2D
@export var speed: float = 150.0
# Array of patrol points
@export var patrol_points: Array[Vector2] = [
Vector2(100, 100), Vector2(800, 100), Vector2(800, 500), Vector2(100, 500)
]
@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
var current_point_index: int = 0
func _ready() -> void:
navigation_agent.max_speed = speed
# Set first patrol point as target
_set_next_patrol_point()
func _physics_process(delta: float) -> void:
# is_navigation_finished() is a reliable way to determine target arrival
if navigation_agent.is_navigation_finished():
# Move to next point
current_point_index = (current_point_index + 1) % patrol_points.size()
_set_next_patrol_point()
# Move toward next point, similar to pursuit logic
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var direction: Vector2 = global_position.direction_to(next_path_position)
velocity = direction * speed
move_and_slide()
func _set_next_patrol_point() -> void:
if patrol_points.is_empty():
return
navigation_agent.target_position = patrol_points[current_point_index]
Common Mistakes and Best Practices
NavigationAgent2D is powerful, but misuse can cause unexpected behavior and performance degradation.
| Common Mistake | Best Practice |
|---|---|
Updating target_position every frame | Use a Timer node to update target position every 0.1–0.3 seconds. Or incorporate logic to only update when distance from target exceeds a threshold. |
Using only is_navigation_finished() for arrival detection | Also use navigation_agent.distance_to_target() to consider arrival when within a certain tolerance from the target for more reliable detection. |
Setting velocity directly | Use velocity = velocity.lerp(direction * speed, weight) to smoothly interpolate from current to target velocity for natural acceleration/deceleration. |
| Manually creating complex shapes | Use TileMap node's layer feature to create scripts that automatically generate navigation meshes from specific tiles. |
Performance Considerations and Alternative Patterns
Performance Optimization
- Path Recalculation Frequency: Updating
target_positioncan become the biggest bottleneck. Set update frequency appropriately based on AI importance and count. - Dynamic Obstacles (
NavigationObstacle2D): UsingNavigationObstacle2Dfor moving floors or doors is convenient but costly. Keep usage to a minimum. - Number of Agents: Simplifying or pausing processing (
set_physics_process(false)) for distant or off-screen AI is effective optimization.
Alternative Pattern: Comparison with AStar2D
| Feature | NavigationAgent2D | AStar2D |
|---|---|---|
| Abstraction Level | High-level (easy to use) | Low-level (highly flexible) |
| Setup | Just bake NavigationRegion2D | Must manually define points and connections |
| Obstacle Avoidance | Automatic (supports dynamic obstacles) | Must implement yourself |
| Use Cases | Character spatial movement | Grid-based games, turn-based strategy |
For typical cases where characters move around maps in real-time, NavigationAgent2D is convenient and efficient.
Summary
Godot Engine's NavigationAgent2D is a powerful tool that dramatically simplifies AI movement logic in 2D games.
- Define navigable areas with NavigationRegion2D
- Set target position on NavigationAgent2D
- Move the character toward the next point obtained from get_next_path_position()
With this simple procedure, you can immediately add practical, natural AI to your game that intelligently avoids obstacles, pursues players, or patrols fixed routes.