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