Object Avoidance

🛡️ Navigation Object Avoidance in Godot (RVO2)

In Intro Into Navigation In Godot, we looked at how an agent calculates a path from A to B across a static Navigation Region.

However, pathfinding alone only knows about baked, static level geometry. If five enemies all chase the player down the same narrow hallway, pathfinding will cause them to bunch up into a single point and clip inside each other's models.

Obstacle Avoidance solves this. Godot uses the RVO2 (Reciprocal Velocity Obstacles) algorithm on the NavigationServer to calculate collision-free movement between dynamic agents in real time.


⚖️ Pathfinding vs Avoidance: The Core Gap

Feature Pathfinding (NavigationMesh) Avoidance (RVO2)
Scope Global (entire map) Local (immediate surroundings)
Geometry Static walls, floors, ramps Moving agents, temporary barricades
Frequency Computed when target changes Recalculated every physics frame
Cost A* search on polygon graph Geometric velocity cone clipping

⚙️ The Avoidance Workflow (The Callback Loop)

Avoidance cannot be evaluated instantly on the same line of code because calculations are dispatched to the NavigationServer3D / NavigationServer2D.

Instead, avoidance requires a safe velocity callback loop:

  1. Calculate Intended Direction: Use the agent's pathfinding to find where it wants to go.
  2. Feed Intended Velocity: Call nav_agent.set_velocity(intended_velocity).
  3. Wait for Server Signal: The NavigationServer processes all agents and emits the signal velocity_computed(safe_velocity).
  4. Move the Character: Apply safe_velocity to move_and_slide() inside that signal handler!

💻 GDScript Implementation Example

Here is a clean implementation for a CharacterBody3D enemy with avoidance enabled:

extends CharacterBody3D
class_name AvoidanceEnemy

@export var move_speed: float = 5.0
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D

func _ready() -> void:
    # 1. Enable avoidance on the agent
    nav_agent.avoidance_enabled = true
    
    # 2. Connect the safe velocity callback
    nav_agent.velocity_computed.connect(_on_velocity_computed)

func set_destination(target_pos: Vector3) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    # Get the next waypoint on the baked navigation mesh
    var next_path_pos: Vector3 = nav_agent.get_next_path_position()
    var move_dir: Vector3 = global_position.direction_to(next_path_pos)
    var intended_velocity: Vector3 = move_dir * move_speed

    if nav_agent.avoidance_enabled:
        # Send intended velocity to NavigationServer for RVO avoidance
        nav_agent.set_velocity(intended_velocity)
    else:
        # Direct movement if avoidance is disabled
        _on_velocity_computed(intended_velocity)

# 3. Apply the calculated safe velocity here!
func _on_velocity_computed(safe_velocity: Vector3) -> void:
    velocity = safe_velocity
    move_and_slide()
The Common Pitfall

If you call move_and_slide() directly inside _physics_process() using your intended velocity while avoidance_enabled = true, avoidance will fail because you are bypassing the server's computed safe_velocity!


🚧 Dynamic Obstacles (NavigationObstacle3D)

What if you have a wooden crate that players can push, or a blast door that closes?
You don't want to rebake the entire 3D NavigationMesh in real-time (baking is slow and CPU-heavy).

Instead, place a NavigationObstacle3D node on the dynamic object:


🎛️ Avoidance Layers & Masks

Just like physics layers, avoidance uses Layers and Masks:

Tip: A heavy boss enemy can set its mask to ignore light foot soldiers, forcing the foot soldiers to swerve around the boss while the boss marches straight forward unhindered.