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. 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

Edited to show scripts/unit.gd. Also, when I bump into the enemy, it will start jittering around.

    Lousifr seems like your enemy has no movement code..
    How do you move your enemy when target is set?

    https://docs.godotengine.org/en/stable/getting_started/first_3d_game/03.player_movement_code.html

    # Moving the Character
    	velocity = target_velocity
    	move_and_slide()

      kuligs2 the enemy movement is handled by the Unit class, although you've given me the idea refactoring the enemy code.

        I'm using a RigidBody3D, look at func apply_movement_force in Unit. I'm using the same system for player movement.

          Lousifr how about you try the one from the example from docs, clearly its not working for you as it is.

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