feat: add navigation_failed signal with time-based stuck detection and simulation pipeline
NpcVisual: - Add navigation_failed(sim_id) signal distinct from arrived_at_target so the simulation layer can distinguish genuine arrival from failure - Replace integer stuck_counter with delta-based stuck_time and exported stuck_timeout (1.5s) for frame-rate-independent detection - Add path_request_id / path_pending mechanism to reject stale async path results when a new target is set or NPC dies - On empty path or stuck movement, emit navigation_failed instead of arriving; only emit arrived_at_target when truly close to target - Reduce arrival_distance back to 0.6 now that navigation_failure is properly signalled (no longer needed as a workaround) - clamp arrival_distance reference in apply_dead_visual_state SimNPC: - Remove retry_count (replaced by navigation_failed signal pathway) - Move last_task assignment from set_task() to task completion in SimulationManager so it reflects the task that actually finished SimulationManager: - Add notify_npc_navigation_failed(): release reservation, record last_task, send NPC to wander for 2s, emit task_changed signal - Add notify_npc_target_unavailable() as public alias of above - Guard extraction with node.reserved_by == npc.id to prevent stale/illegitimate extraction when reservation is lost - Guard fallback zone task completion (apply_npc_task) so it only fires for non-resource tasks (gather_food/wood with no target silently skip instead of applying zone values) - Remove retry_count safety net (fully replaced by signal pipeline) WorldViewManager: - Connect navigation_failed signal in spawn_npc_visual() - Skip target assignment for empty/idle/dead task states - When no resource node available for gather_food/wood, call notify_npc_target_unavailable() so NPC wanders instead of idling - Add _on_npc_visual_navigation_failed handler delegating to SimulationManager.notify_npc_navigation_failed() - Remove retry_count references
This commit is contained in:
+46
-14
@@ -1,12 +1,14 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
signal arrived_at_target(sim_id: int)
|
||||
signal navigation_failed(sim_id: int)
|
||||
|
||||
const DEBUG_LOGS := true
|
||||
|
||||
@export var move_speed := 3.0
|
||||
@export var rotation_speed := 8.0
|
||||
@export var waypoint_reached_distance := 0.35
|
||||
@export var stuck_timeout := 1.5
|
||||
|
||||
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||
|
||||
@@ -18,8 +20,10 @@ var profession: String = ""
|
||||
var current_path: PackedVector3Array = []
|
||||
var path_index := 0
|
||||
var current_target := Vector3.INF
|
||||
var arrival_distance := 1.5
|
||||
var stuck_counter := 0
|
||||
var arrival_distance := 0.6
|
||||
var stuck_time := 0.0
|
||||
var path_request_id := 0
|
||||
var path_pending := false
|
||||
|
||||
var is_dead_visual := false
|
||||
var dead_material := StandardMaterial3D.new()
|
||||
@@ -38,8 +42,12 @@ func setup_from_sim(npc: SimNPC) -> void:
|
||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||
|
||||
func set_target_position(pos: Vector3) -> void:
|
||||
path_request_id += 1
|
||||
var request_id := path_request_id
|
||||
current_target = pos
|
||||
has_reported_arrival = false
|
||||
stuck_time = 0.0
|
||||
path_pending = false
|
||||
|
||||
if global_position.distance_to(pos) <= arrival_distance:
|
||||
current_path = PackedVector3Array()
|
||||
@@ -48,9 +56,13 @@ func set_target_position(pos: Vector3) -> void:
|
||||
return
|
||||
|
||||
nav_agent.target_position = pos
|
||||
path_pending = true
|
||||
|
||||
await get_tree().physics_frame
|
||||
|
||||
if request_id != path_request_id or is_dead_visual:
|
||||
return
|
||||
|
||||
current_path = NavigationServer3D.map_get_path(
|
||||
get_world_3d().navigation_map,
|
||||
global_position,
|
||||
@@ -58,11 +70,12 @@ func set_target_position(pos: Vector3) -> void:
|
||||
true
|
||||
)
|
||||
|
||||
path_pending = false
|
||||
path_index = 0
|
||||
|
||||
if current_path.is_empty():
|
||||
debug_log("Path is empty, stuck counter will handle fallback.")
|
||||
nav_agent.target_position = global_position
|
||||
debug_log("Path is empty")
|
||||
_report_navigation_failure_once()
|
||||
else:
|
||||
debug_log("New target: %s Path points: %s" % [pos, current_path.size()])
|
||||
|
||||
@@ -77,6 +90,11 @@ func _physics_process(delta: float) -> void:
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
if path_pending:
|
||||
velocity = Vector3.ZERO
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
if global_position.distance_to(current_target) <= arrival_distance:
|
||||
_report_arrival_once()
|
||||
velocity = Vector3.ZERO
|
||||
@@ -86,10 +104,7 @@ func _physics_process(delta: float) -> void:
|
||||
if current_path.is_empty() or path_index >= current_path.size():
|
||||
velocity = Vector3.ZERO
|
||||
move_and_slide()
|
||||
stuck_counter += 1
|
||||
if stuck_counter > 90:
|
||||
debug_log("Stuck with no valid path, reporting arrival")
|
||||
_report_arrival_once()
|
||||
_report_navigation_failure_once()
|
||||
return
|
||||
|
||||
var next_pos := current_path[path_index]
|
||||
@@ -109,14 +124,15 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
move_and_slide()
|
||||
|
||||
if global_position.distance_squared_to(prev_pos) < 0.001:
|
||||
stuck_counter += 1
|
||||
if stuck_counter > 90:
|
||||
debug_log("Stuck while navigating, reporting arrival")
|
||||
_report_arrival_once()
|
||||
var minimum_progress := move_speed * delta * 0.1
|
||||
if global_position.distance_squared_to(prev_pos) < minimum_progress * minimum_progress:
|
||||
stuck_time += delta
|
||||
if stuck_time >= stuck_timeout:
|
||||
debug_log("Movement remained blocked")
|
||||
_report_navigation_failure_once()
|
||||
return
|
||||
else:
|
||||
stuck_counter = 0
|
||||
stuck_time = 0.0
|
||||
|
||||
var target_angle := atan2(direction.x, direction.z)
|
||||
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
|
||||
@@ -132,12 +148,28 @@ func _report_arrival_once() -> void:
|
||||
debug_log("Arrived at target")
|
||||
arrived_at_target.emit(sim_id)
|
||||
|
||||
func _report_navigation_failure_once() -> void:
|
||||
if has_reported_arrival:
|
||||
return
|
||||
|
||||
has_reported_arrival = true
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
current_target = Vector3.INF
|
||||
path_pending = false
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
debug_log("Navigation failed")
|
||||
navigation_failed.emit(sim_id)
|
||||
|
||||
func apply_dead_visual_state() -> void:
|
||||
is_dead_visual = true
|
||||
path_request_id += 1
|
||||
velocity = Vector3.ZERO
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
current_target = Vector3.INF
|
||||
path_pending = false
|
||||
has_reported_arrival = true
|
||||
|
||||
scale = Vector3(0.8, 0.25, 1.2)
|
||||
|
||||
Reference in New Issue
Block a user