NavigationAgent3D not working

Godot Version

Godot 4.4

Question

I've got a RigidBody3D that I'm using as a character. It's using the same system as the player, as I've created a base class called Unit that handles movement and health. This system worked when I was using movement without proper pathfinding. When I bump into the character, it will start jittering around. I've tried adjusting the cell_height, and that did nothing. Listed below are the source files I am using, along with a picture to show that the path is being calculated.

# scripts/unit.gd
extends RigidBody3D
class_name Unit

# Base Stats
@export var max_health: int = 100
@export var speed: float = 10.0
@export var acceleration: float = 40.0
@export var damage: int = 10
@export var attack_cooldown: float = 1.0

# Current State
var current_health: int
var dead: bool = false
var can_attack: bool = true
var attack_timer: float = 0.0

func _ready() -> void:
    current_health = max_health

    # Configure RigidBody3D properties
    freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
    lock_rotation = true
    gravity_scale = 1.5
    contact_monitor = true
    max_contacts_reported = 4

    body_entered.connect(_on_body_entered)
    _unit_ready() # Virtual method for child classes

func _physics_process(delta: float) -> void:
    if dead:
        return

    # Handle attack cooldown
    if !can_attack:
        attack_timer += delta
        if attack_timer >= attack_cooldown:
            can_attack = true
            attack_timer = 0.0

    _process_movement(delta)
    _process_rotation()

# Virtual method for movement behavior
func _process_movement(_delta: float) -> void:
    pass

# Virtual method for rotation behavior
func _process_rotation() -> void:
    pass

# Apply movement force with speed limiting
func apply_movement_force(direction: Vector3) -> void:
    if direction != Vector3.ZERO:
        apply_central_force(direction * acceleration)

        # Limit maximum speed
        var horizontal_velocity = Vector3(linear_velocity.x, 0, linear_velocity.z)
        if horizontal_velocity.length() > speed:
            horizontal_velocity = horizontal_velocity.normalized() * speed
            linear_velocity.x = horizontal_velocity.x
            linear_velocity.z = horizontal_velocity.z

func take_damage(amount: int) -> void:
    current_health -= amount
    _on_damage_taken()
    if current_health <= 0:
        die()

# Virtual method for damage response
func _on_damage_taken() -> void:
    pass

func die() -> void:
    dead = true
    _on_death()
    queue_free()

# Virtual method for death behavior
func _on_death() -> void:
    pass

func _on_body_entered(body: Node) -> void:
    if can_attack and _can_attack_target(body):
        _attack_target(body)
        can_attack = false
        attack_timer = 0.0

# Virtual method to check if target is valid
func _can_attack_target(_target: Node) -> bool:
    return false

# Virtual method for attack behavior
func _attack_target(_target: Node) -> void:
    pass

# Virtual method for additional setup
func _unit_ready() -> void:
    pass
# scripts/enemy.gd
extends Unit
class_name Enemy

@export var detection_range: float = 15.0
@export var path_update_interval: float = 0.1

@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
var target: Node3D = null
var path_timer: float = 0.0

func _ready() -> void:
    add_to_group("enemies")
    super._ready()

    # Configure NavigationAgent3D
    nav_agent.path_desired_distance = 0.5
    nav_agent.target_desired_distance = 0.5
    nav_agent.path_max_distance = 50.0
    nav_agent.avoidance_enabled = true
    nav_agent.velocity_computed.connect(_on_velocity_computed)

    # Find player target
    target = get_tree().get_first_node_in_group("player")

    # Start pathfinding after a short delay to ensure navigation mesh is ready
    await get_tree().create_timer(0.1).timeout
    if target:
        update_target_position()

func _unit_ready() -> void:
    # Enemy-specific initialization
    max_health = 30
    speed = 9.0
    damage = 10

func _physics_process(delta: float) -> void:
    if !target or dead:
        return

    # Update path to target periodically
    path_timer += delta
    if path_timer >= path_update_interval:
        path_timer = 0.0
        update_target_position()

    # Don't move if we've reached our destination
    if nav_agent.is_navigation_finished():
        return

    # Calculate direction to next path position
    var next_position = nav_agent.get_next_path_position()
    var direction = (next_position - global_position).normalized()

    # Keep movement horizontal
    direction.y = 0

    # Apply movement force
    apply_movement_force(direction * speed)

    # Update rotation 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)

func update_target_position() -> void:
    if !target:
        return

    var distance_to_target = global_position.distance_to(target.global_position)

    # Only pursue target within detection range
    if distance_to_target <= detection_range:
        nav_agent.target_position = target.global_position

# Add this function to handle velocity computation
func _on_velocity_computed(safe_velocity: Vector3) -> void:
    if owner is CharacterBody3D:
        owner.velocity = safe_velocity
        owner.move_and_slide()
    else:
        # For RigidBody3D
        apply_movement_force(safe_velocity)

func _can_attack_target(body: Node) -> bool:
    return body.is_in_group("player")

func _attack_target(body: Node) -> void:
    if body.has_method("take_damage"):
        body.take_damage(damage)

func take_damage(amount: int) -> void:
    super.take_damage(amount)
    # Ensure enemy always knows where player is after taking damage
    if target:
        update_target_position()

func _on_damage_taken() -> void:
    # Optional: Add visual feedback when damaged
    pass

func _on_death() -> void:
    # Optional: Add death effects or drop items
    pass

Figured the problem out. nav_agent.path_desired_distance was set to 0.5. Setting it to 0.6 fixed my issue.