[Godot] Pathfinding with NavigationAgent2D - Chasing and Patrolling Enemy AI

Created: 2025-12-08Last updated: 2026-07-08

How to implement chasing and patrolling enemy AI that avoids obstacles using Godot's NavigationAgent2D. Covers the basic processing cycle, what get_next_path_position actually returns, chase and patrol implementations, agent-to-agent avoidance with RVO, and optimizing path recalculation, with code and diagrams.

An enemy chases the player around obstacles. A guard silently walks a fixed route. That kind of smart movement comes from NavigationAgent2D walking on top of a navigation mesh (a map for AI).

In the previous article, we baked a walkable map. This article is about actually moving on it. You only set a destination, and the agent handles path calculation and obstacle avoidance for you, so all you decide is where to go. Let's build the two classics: chasing and patrolling.

Pathfinding illustration on a 2D map where an agent follows a dotted path of waypoints around an obstacle toward a goal flag

What You'll Learn

  • The basic processing cycle of NavigationAgent2D (target, path, next point, move)
  • The key point that get_next_path_position() returns the next waypoint
  • Implementing chase AI and patrol AI
  • Agent-to-agent avoidance (RVO) with velocity_computed
  • Performance optimization for path recalculation, and how AStar2D differs

Sponsored

The Basic NavigationAgent2D Cycle

Place NavigationAgent2D as a child node of the character you want to move (a CharacterBody2D). Using it means running this cycle every frame.

Cycle diagram of NavigationAgent2D processing: set target_position, calculate the path, receive the next point via get_next_path_position(), move with move_and_slide(), repeat
  1. Set the target: assign the destination coordinate to target_position
  2. Calculate the path: navigation automatically finds the shortest route
  3. Get the next point: call get_next_path_position() for the next waypoint on the path
  4. Move there: move_and_slide() toward that point

All the hard parts, pathfinding and obstacle avoidance, are handled internally by the agent. What we write is just "set the target" and "move toward the point it returns."

get_next_path_position Returns the Next Waypoint

The thing beginners trip over is what get_next_path_position() means. It returns the next waypoint to head for along the path, not the goal itself.

Top-down diagram showing that get_next_path_position() returns the next waypoint on a path curving around an obstacle, not the goal at target_position

So you move straight toward the coordinate it returns. Heading directly for the goal (target_position) would run you into obstacles, but following the waypoints one at a time naturally curves you along the route.

var next_point := navigation_agent.get_next_path_position()
var direction := global_position.direction_to(next_point)  # Direction to the next waypoint
velocity = direction * speed
move_and_slide()
Sponsored

Chase AI: Following a Moving Target

First up is chase AI, where an enemy pursues the player. Just keep feeding the player's current position into target_position, and the enemy comes after you while avoiding obstacles.

Comparison of the two main NavigationAgent2D use cases: chasing follows a moving player along a path, while patrolling visits four fixed points in order

The key is not updating target_position every frame. Every target update triggers a path recalculation, so doing it per frame gets expensive. Use a Timer to thin it out to roughly every 0.2 seconds.

extends CharacterBody2D

@export var speed: float = 250.0
@export var player: Node2D

@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
@onready var repath_timer: Timer = $RepathTimer   # 0.2 second interval, autostart

func _ready() -> void:
    repath_timer.timeout.connect(_update_target)

func _update_target() -> void:
    if is_instance_valid(player):
        navigation_agent.target_position = player.global_position  # Updating the target recalculates the path

func _physics_process(_delta: float) -> void:
    if navigation_agent.is_navigation_finished():   # Stop once we've arrived
        return
    var next_point := navigation_agent.get_next_path_position()
    velocity = global_position.direction_to(next_point) * speed
    move_and_slide()

is_navigation_finished() tells you whether the agent has arrived, so you stop moving once it has. Leaving path recalculation to the Timer and keeping _physics_process focused purely on movement is the clean structure.

Patrol AI: Visiting Fixed Points in Order

Next is patrol AI, which cycles through a set of points. When it reaches a target, it switches to the next one, and this is where is_navigation_finished() earns its keep.

extends CharacterBody2D

@export var speed: float = 150.0
@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 index := 0

func _ready() -> void:
    navigation_agent.target_position = patrol_points[index]

func _physics_process(_delta: float) -> void:
    if navigation_agent.is_navigation_finished():
        # Move to the next point (wrap around at the end)
        index = (index + 1) % patrol_points.size()
        navigation_agent.target_position = patrol_points[index]
        return
    var next_point := navigation_agent.get_next_path_position()
    velocity = global_position.direction_to(next_point) * speed
    move_and_slide()

In a patrol, the target only changes when a point is reached, so no thinning is needed like it is for chasing. Combine "when do we switch to chasing (once we spot the player)" with a state machine and you get enemies that move naturally between patrolling and chasing.

Sponsored

Agent-to-Agent Avoidance (RVO)

Once you spawn multiple enemies, a new problem shows up: they clump together and overlap. Avoidance via RVO (Reciprocal Velocity Obstacles) prevents that.

Diagram showing two agents that would collide if they went straight, instead bulging outward to pass each other, with velocity_computed providing the safe velocity

To use it, enable avoidance_enabled on the agent and, instead of moving directly, pass your desired velocity to set_velocity(). The agent then returns a safe velocity that avoids other agents via the velocity_computed signal, and you call move_and_slide() with that.

func _physics_process(_delta: float) -> void:
    if navigation_agent.is_navigation_finished():
        return
    var next_point := navigation_agent.get_next_path_position()
    var desired := global_position.direction_to(next_point) * speed
    navigation_agent.set_velocity(desired)   # Pass the desired velocity (don't move directly)

# The safe velocity, accounting for avoidance, comes back here
func _on_navigation_agent_2d_velocity_computed(safe_velocity: Vector2) -> void:
    velocity = safe_velocity
    move_and_slide()

When avoidance_enabled is on, it's essential to call move_and_slide() only inside velocity_computed. Moving directly in _physics_process as well causes double movement, and avoidance stops working.

Performance and How AStar2D Differs

When you're moving a lot of agents, how often paths are recalculated is the biggest bottleneck.

Comparison of path recalculation cost: recalculating every frame is heavy, while thinning to every 0.2 seconds with a timer is light
  • Thin out recalculation: update target_position on a Timer every 0.2 seconds or so. Give less important enemies an even longer interval
  • Stop off-screen agents: use set_physics_process(false) on distant or off-screen AI
  • Keep NavigationObstacle2D minimal: dynamic obstacles are convenient but expensive, so limit how many you use

Choosing Between AStar2D and NavigationAgent2D

For grid-based games, the lower-level AStar2D is sometimes a better fit.

NavigationAgent2DAStar2D
Abstraction levelHigh level (easy to use)Low level (flexible)
SetupJust bake a meshDefine points and connections manually
Obstacle avoidanceAutomatic (including dynamic obstacles and RVO)Implement it yourself
Good forFree-form movement through spaceGrid movement, turn-based strategy

It's enough to remember this: use NavigationAgent2D for typical enemy AI that moves freely in real time across a map, and AStar2D when the board is a grid and you want tight control over the route.

Sponsored

Bonus: Good to Know for Later

  • Check the map first: 90% of "my agent won't move" problems are on the map (mesh) side. Suspect first that NavigationRegion2D hasn't been baked to cover the area.
  • Jitter or refusal to stop means the tolerance is too tight: if the agent won't settle at its destination, raise target_desired_distance a bit (for example, 10.0).
  • Smooth out the velocity: interpolating with something like velocity = velocity.lerp(desired, 0.1) gives natural acceleration instead of an overly snappy start.
  • Keep decisions on the AI side: whether to patrol, chase, or flee belongs to a state machine or behavior tree. Letting navigation stick to "move toward the given target" keeps your design clean.

Summary

  • Basic cycle: set target_position, then move_and_slide() toward the waypoint from get_next_path_position()
  • Following waypoints one at a time is what makes obstacle avoidance look natural
  • Chasing updates target_position on a thinned Timer; patrolling advances to the next point on arrival
  • RVO with avoidance_enabled plus set_velocity() plus velocity_computed stops agents from overlapping
  • Optimization starts with thinning recalculation and stopping off-screen agents

Start by placing one enemy on your baked map and setting the player as its target_position. Once you see it curving around obstacles to reach you, the foundation of navigation is done. From there, add patrolling, RVO, and state management to get enemy AI that really feels like a game.

Further Reading