Starter-Kit-City-Builder/scripts/NavigationNPC.gd

168 lines
5.5 KiB
GDScript

extends CharacterBody3D
@onready var navigation_agent_3d: NavigationAgent3D = $NavigationAgent3D
@onready var animation_player: AnimationPlayer = $"character-female-d2/AnimationPlayer"
@onready var character_model: Node3D = $"character-female-d2"
var is_moving: bool = false
var wait_timer: float = 0.0
var waiting_time: float = 3.0
var last_position: Vector3 = Vector3.ZERO
var auto_patrol: bool = true # Set to true to automatically patrol between points
var movement_speed: float = 2.5 # Walking speed
var stuck_timer: float = 0.0 # Timer to detect if character is stuck
var stuck_threshold: float = 5.0 # Time before considering character stuck
func _ready() -> void:
# Set navigation parameters
navigation_agent_3d.path_desired_distance = 0.5
navigation_agent_3d.target_desired_distance = 0.5
# Connect navigation signal if using Godot's navigation velocity system
if navigation_agent_3d.has_signal("velocity_computed"):
navigation_agent_3d.velocity_computed.connect(_on_velocity_computed)
# Store initial position
last_position = global_position
if animation_player:
# Start with idle animation
animation_player.play("idle")
# Force an immediate target set to get the character moving
call_deferred("_start_initial_movement")
# Called after scene is ready to start movement
func _start_initial_movement():
await get_tree().process_frame
pick_random_target()
# Force movement to a specific target
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
var random_position := Vector3.ZERO
random_position.x = randf_range(-5,5)
random_position.z = randf_range(-5,5) # Changed to z-axis since we're in 3D space
set_movement_target(random_position)
print("User forced new movement target: ", random_position)
# Set the target position for navigation
func set_movement_target(target: Vector3) -> void:
navigation_agent_3d.set_target_position(target)
is_moving = true
stuck_timer = 0.0
if animation_player and animation_player.current_animation != "walk":
animation_player.play("walk")
func _physics_process(delta:float)->void:
# Check if character is stuck
var moved = (global_position - last_position).length() > 0.01
if is_moving and !moved:
stuck_timer += delta
if stuck_timer > stuck_threshold:
# Character is stuck, pick a new random target
pick_random_target()
stuck_timer = 0.0
else:
stuck_timer = 0.0
# Always update the last position
last_position = global_position
# Check for auto patrol behavior
if auto_patrol:
if !is_moving:
# Count down the wait timer
wait_timer += delta
if wait_timer >= waiting_time:
wait_timer = 0.0
pick_random_target()
# Handle navigation logic
if navigation_agent_3d.is_navigation_finished():
# Character reached destination, switch to idle
if is_moving:
is_moving = false
if animation_player:
animation_player.play("idle")
# Reset wait timer for next automatic movement
wait_timer = 0.0
waiting_time = randf_range(2.0, 5.0)
# print("Character reached destination, waiting for ", waiting_time, " seconds")
return
# If we're still moving, proceed with navigation
if is_moving:
# Get movement data
var destination = navigation_agent_3d.get_next_path_position()
var local_destination = destination - global_position
var direction = local_destination.normalized()
# Make character face the direction of movement
if direction.length() > 0.01:
# Look at the destination with a 180-degree rotation to face forward
var look_target = global_position + Vector3(direction.x, 0, direction.z)
# First make the character model look at the target
character_model.look_at(look_target, Vector3.UP)
# Then rotate it 180 degrees around the Y axis to fix backward facing
character_model.rotate_y(PI)
# Play walking animation when moving
if not animation_player.current_animation == "walk":
animation_player.play("walk")
# Calculate velocity - try both methods of movement
if navigation_agent_3d.avoidance_enabled and has_method("_on_velocity_computed"):
# Use Godot's navigation velocity system
var desired_velocity = direction * movement_speed
navigation_agent_3d.set_velocity(desired_velocity)
else:
# Direct movement
velocity = direction * movement_speed
move_and_slide()
# Called when the navigation system has computed the velocity
func _on_velocity_computed(safe_velocity: Vector3) -> void:
# Apply the computed velocity to the character
velocity = safe_velocity
move_and_slide()
# Picks a random target position that should be navigable
func pick_random_target() -> void:
# Try to find a valid target position
var max_attempts = 10
var attempt = 0
var found_valid_point = false
var target_pos = Vector3.ZERO
while attempt < max_attempts and !found_valid_point:
# Generate a random direction and distance
var random_direction = Vector3(
randf_range(-1.0, 1.0),
0.0,
randf_range(-1.0, 1.0)
).normalized()
var random_distance = randf_range(3.0, 10.0)
target_pos = global_position + random_direction * random_distance
# Try to get a position on the navigation mesh
if navigation_agent_3d.get_navigation_map() != RID():
# If we have a valid navigation map, try to get a position on it
found_valid_point = true
attempt += 1
# If we couldn't find a valid point, just pick a close one
if !found_valid_point:
target_pos = global_position + Vector3(randf_range(-3, 3), 0, randf_range(-3, 3))
print("Couldn't find valid navigation point, choosing nearby point: ", target_pos)
set_movement_target(target_pos)