
Godot Genre Stealth
- 131 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-stealth for development tasks
About
godot-genre-stealth: A skill for development. This provides functionality for development workflows.
- godot-genre-stealth
Godot Genre Stealth by the numbers
- 131 all-time installs (skills.sh)
- +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,707 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-stealthAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 131 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-stealth for development tasks
Files
Genre: Stealth
Player choice, systemic AI, and clear communication define stealth games.
NEVER Do (Expert Anti-Patterns)
Detection & Awareness
- NEVER use binary "Seen/Not Seen" detection; strictly use a Gradual Detection Meter (0-100%) that builds based on distance, light level, and speed.
- NEVER use standard
RayCast3Dnodes for massive amounts of vision checks; strictly use `PhysicsDirectSpaceState3D.intersect_ray()` to query the PhysicsServer instantly and nodelessly. - NEVER allow AI to see through solid geometry; strictly use raycasts between AI eyes and player sample points (Head/Torso/Feet).
- NEVER use a single sample point for visibility; strictly sample at least 3 points (Head, Torso, Feet) to prevent detection bugs when partially in cover.
- NEVER use static "Guard Paths"; strictly implement Dynamic Investigating where guards leave their route to check on suspicious sounds/activities.
- NEVER trigger "Detection" immediately upon line-of-sight; strictly use a Detection Meter with a decay rate to provide a "forgiveness window" for the player to recover.
- NEVER assume a random navmesh point is safe; strictly verify cover points by Raycasting toward the Threat to ensure geometry successfully breaks the line of sight.
- NEVER forget to pass the guard's own RID into the raycast exclude array; if omitted, the ray will hit the guard's own body, causing false blocking.
- NEVER run complex AI detection for off-screen guards; strictly use
VisibleOnScreenNotifier3Dto pause heavy logic for distant enemies.
Systemic & World Logic
- NEVER use a simple
distance_to()check for hearing; strictly calculate sound travel along the Navigation Path to determine if a wall blocks noise. - NEVER make combat as viable as stealth; strictly ensure "going loud" triggers intense reinforcements or high-lethality states to preserve the stealth loop.
- NEVER hide the "Why" of detection; strictly provide immediate feedback via UI icons (?, !) or audio barks ("What was that?").
- NEVER ignore the return value of
intersect_ray(); strictly checkis_empty()first to prevent runtime crashes. - NEVER assume a raycast won't hit the guard itself; strictly exclude the guard's RID from Query Parameters.
Optimization & Performance
- NEVER tightly couple AI to player scripts; strictly use duck-typing (e.g.,
if body.has_method("get_detected")) so guards can spot decoys or dead bodies without brittle dependencies. - NEVER maintain hardcoded arrays to trigger base-wide alarms; strictly add guards to a "guards" group and use
get_tree().call_group()for dynamic notification. - NEVER use standard Strings for AI state; strictly use
StringName(&"alert") for O(1) pointer-level comparisons in high-frequency loops. - NEVER bake massive NavigationMeshes synchronously; strictly use
use_async_iterationsto prevent main thread stalls during runtime bakes. - NEVER rely on
Node.find_child()during gameplay; strictly use Groups or exported references for O(1) player tracking. - NEVER leave CollisionShapes enabled on incapacitated bodies; strictly disable them or move them to a "corpse" layer to prevent pathing interference.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- stealth_ai_controller.gd - Professional-grade NPC controller with composite vision, sound paths, and alert state logic.
Modular Components
- stealth_patterns.gd - Collection of patterns for PhysicsServer raycasting, noise bus routing, and avoidance masking.
---
Design Principles
From industry experts (Splinter Cell, Dishonored, Hitman developers):
1. Player Choice: Multiple valid approaches to every scenario 2. Systemic Design: Rules-based AI that players can learn and exploit 3. Clear Communication: Player always understands game state and threats 4. Fair Detection: No "gotcha" moments - threats visible before dangerous
---
AI Detection System
Vision Cone Implementation
Based on Splinter Cell Blacklist GDC talk - realistic vision uses composite shapes:
class_name EnemyVision
extends Node3D
@export var forward_vision_range := 20.0 # Main vision cone
@export var peripheral_range := 10.0 # Side vision
@export var forward_fov := 60.0 # Degrees
@export var peripheral_fov := 120.0 # Degrees
@export var detection_speed := 1.0 # How fast detection builds
var detection_level := 0.0 # 0-100
var target: Node3D = null
func _physics_process(delta: float) -> void:
var player := get_player_if_visible()
if player:
# Detection rate varies by:
# - Distance (closer = faster)
# - Lighting on player
# - Player movement (moving = more visible)
# - In peripheral vs direct vision
var rate := calculate_detection_rate(player)
detection_level = min(100, detection_level + rate * delta)
else:
detection_level = max(0, detection_level - detection_speed * 0.5 * delta)
func get_player_if_visible() -> Player:
var player := get_tree().get_first_node_in_group("player")
if not player:
return null
var to_player := player.global_position - global_position
var distance := to_player.length()
var angle := rad_to_deg(global_basis.z.angle_to(-to_player.normalized()))
# Check forward cone
if angle < forward_fov / 2.0 and distance < forward_vision_range:
if has_line_of_sight(player):
return player
# Check peripheral (less effective)
elif angle < peripheral_fov / 2.0 and distance < peripheral_range:
if has_line_of_sight(player):
return player
return null
func calculate_detection_rate(player: Player) -> float:
var distance := global_position.distance_to(player.global_position)
var distance_factor := 1.0 - (distance / forward_vision_range)
var light_factor := player.get_light_level() # 0.0 = dark, 1.0 = lit
var movement_factor := 1.0 if player.velocity.length() > 0.5 else 0.3
return detection_speed * distance_factor * light_factor * movement_factor * 50.0Sound Detection System
Based on Thief/Hitman implementation - sounds propagate along navigation paths:
class_name SoundPropagation
extends Node
# Sound travels through connected navigation points, not through walls
func propagate_sound(origin: Vector3, loudness: float, sound_type: String) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
var path := NavigationServer3D.map_get_path(
get_world_3d().navigation_map,
origin,
enemy.global_position,
true
)
if path.is_empty():
continue # No path = sound blocked
var path_distance := calculate_path_length(path)
var heard_loudness := loudness - (path_distance * 0.5) # Falloff
if heard_loudness > enemy.hearing_threshold:
enemy.hear_sound(origin, sound_type, heard_loudness)
func calculate_path_length(path: PackedVector3Array) -> float:
var length := 0.0
for i in range(1, path.size()):
length += path[i].distance_to(path[i - 1])
return lengthPlayer Light Level
class_name LightDetector
extends Node3D
@export var sample_points: Array[Marker3D] # Multiple points on player body
func get_light_level() -> float:
var total := 0.0
var space := get_world_3d().direct_space_state
for point in sample_points:
for light in get_tree().get_nodes_in_group("lights"):
var dir := light.global_position - point.global_position
var query := PhysicsRayQueryParameters3D.create(
point.global_position,
light.global_position
)
var result := space.intersect_ray(query)
if result.is_empty(): # Not blocked
total += light.light_energy / dir.length_squared()
return clamp(total / sample_points.size(), 0.0, 1.0)---
AI Alert States
Three-phase system (industry standard):
enum AlertState { IDLE, SUSPICIOUS, ALERTED, COMBAT }
class_name EnemyAI
extends CharacterBody3D
var alert_state := AlertState.IDLE
var suspicion_point: Vector3
var search_timer := 0.0
signal alert_state_changed(new_state: AlertState)
func transition_to(new_state: AlertState) -> void:
alert_state = new_state
alert_state_changed.emit(new_state)
match new_state:
AlertState.SUSPICIOUS:
play_animation("suspicious")
speak_dialogue("what_was_that")
AlertState.ALERTED:
speak_dialogue("who_goes_there")
# Other guards in range hear and become suspicious
alert_nearby_guards()
AlertState.COMBAT:
speak_dialogue("intruder")
trigger_alarm()Visual Feedback (Critical!)
class_name AlertIndicator
extends Node3D
@export var idle_icon: Texture2D
@export var suspicious_icon: Texture2D # "?"
@export var alerted_icon: Texture2D # "!"
@export var detection_meter: ProgressBar # Shows filling detection
func update_indicator(state: AlertState, detection: float) -> void:
detection_meter.value = detection
match state:
AlertState.IDLE:
icon.texture = idle_icon
detection_meter.visible = false
AlertState.SUSPICIOUS:
icon.texture = suspicious_icon
detection_meter.visible = true
AlertState.ALERTED:
icon.texture = alerted_icon
detection_meter.visible = false---
Player Abilities
Five categories of stealth tools (per Mark Brown's analysis):
1. Movement Alteration
# Crouch, crawl, run (noisy vs quiet)
func calculate_noise_level() -> float:
if is_crouching:
return 0.2
elif is_running:
return 1.0
else:
return 0.52. Information Gathering
# Peek, scout, mark enemies
func activate_detective_vision() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.show_outline()
enemy.show_vision_cone()3. AI Manipulation
# Throw distractions
func throw_distraction(target_position: Vector3) -> void:
var rock := distraction_scene.instantiate()
rock.global_position = target_position
add_child(rock)
SoundPropagation.propagate_sound(target_position, 30.0, "impact")4. Space Control
# Shoot out lights, create hiding spots
func shoot_light(light: Light3D) -> void:
light.visible = false
# Update light level for area5. Enemy Elimination
func perform_takedown(enemy: EnemyAI, lethal: bool) -> void:
if enemy.alert_state == AlertState.COMBAT:
return # Can't stealth kill alert enemy
if lethal:
enemy.die()
else:
enemy.knockout()
# Body becomes interactable
spawn_body(enemy)---
Level Design
Outpost Design (Open Areas)
[Safe perimeter for observation]
|
[Sparse guards at edges - isolatable]
|
[Dense center with objective]
|
[Multiple entry points/routes]Limited Encounter Design (Corridors)
- Enemies visible 8+ meters before engagement
- Multiple paths through
- Cover objects and hiding spots
- Emergency escape routes
---
UI Communication
Based on Thief's "light gem" innovation:
class_name StealthHUD
extends Control
@onready var visibility_meter: TextureProgressBar
@onready var sound_meter: TextureProgressBar
@onready var minimap: Control
func _process(_delta: float) -> void:
visibility_meter.value = player.get_light_level() * 100
sound_meter.value = player.current_noise_level * 1004. Reaction-Delay Window (Meter)
Decoupled detection tracking with a forgiveness period to prevent instant "gotcha" moments.
# detection_meter.gd
func process_vision(is_seeing: bool, delta: float):
if is_seeing:
current_awareness += delta
if current_awareness >= threshold:
player_detected.emit()
else:
# Gradually decay awareness when out of sight
current_awareness = max(0, current_awareness - delta * 0.5)5. Cover-Point Finder (Raycast)
Find and verify valid cover points using Navigation and Physics queries.
# cover_finder.gd
func find_cover(threat_pos: Vector3):
var map = get_world_3d().get_navigation_map()
for i in 10:
# Get a random point on the navmesh
var p = NavigationServer3D.map_get_random_point(map, 1, false)
# Verify LOS is broken from the point to the threat
var query = PhysicsRayQueryParameters3D.create(p + Vector3.UP, threat_pos + Vector3.UP)
var hit = get_world_3d().direct_space_state.intersect_ray(query)
if hit:
return p # Hit environmental geometry, point is valid cover
return global_position6. Detection Cone Shader (Post-Process)
Highlight detection areas using a screen-space shader for clear player feedback.
// vision.gdshader
shader_type canvas_item;
uniform sampler2D screen : hint_screen_texture;
uniform vec2 enemy_pos; // Normalized screen coordinates (0.0-1.0)
void fragment() {
vec4 base = texture(screen, SCREEN_UV);
float dist = distance(SCREEN_UV, enemy_pos);
if (dist < 0.2) {
// Tint detected area red
COLOR = mix(base, vec4(1, 0, 0, 1), 0.3);
} else {
COLOR = base;
}
}
---
## Common Pitfalls
| Pitfall | Solution |
|---------|----------|
| Instant detection | Use gradual detection with clear feedback |
| Guards see through walls | Raycast-based vision with proper collision |
| Unfair patrol patterns | Make patterns learnable, with tells |
| Two games (stealth + combat) | Either commit to stealth or make combat risky |
| Unclear detection | Always show WHY player was detected |
---
## Godot-Specific Tips
1. **Raycasts for vision**: Use `PhysicsRayQueryParameters3D` with collision masks
2. **NavigationAgent3D**: For patrol routes and pathfinding
3. **Area3D**: For sound propagation zones and trigger areas
4. **AnimationTree**: Blend between alert state animations
## Reference
- Master Skill: [godot-master](../godot-master/SKILL.md)
# skills/genre-stealth/scripts/light_detector.gd
extends Node3D
## Light Detector (Expert Pattern)
## Estimates how lit the player is by sampling light sources.
## More performance-friendly than rendering viewport textures.
class_name LightDetector
@export var body_mesh: MeshInstance3D # To get bounds
@export var active: bool = True
func get_light_level() -> float:
if not active: return 0.0
var total_light = 0.0
var ambient = get_world_3d().environment.ambient_light_color.v if get_world_3d().environment else 0.0
total_light += ambient
# Find nearby lights
# In a real system, use an Area3D to track lights entering/exiting range
# For this snippet, we iterate group "lights" (Optimization warning: Don't do this every frame for 100 lights)
for light in get_tree().get_nodes_in_group("lights"):
if light is OmniLight3D:
var dist_sq = global_position.distance_squared_to(light.global_position)
var range_sq = light.omni_range * light.omni_range
if dist_sq < range_sq:
# Check occlusion
if _is_occluded(light.global_position):
continue
var attenuation = 1.0 - (dist_sq / range_sq) # Simplified
total_light += light.light_energy * attenuation
return clamp(total_light, 0.0, 1.0)
func _is_occluded(light_pos: Vector3) -> bool:
var query = PhysicsRayQueryParameters3D.create(global_position + Vector3.UP, light_pos)
var result = get_world_3d().direct_space_state.intersect_ray(query)
# If hit something that isn't the light (lights don't have collision usually)
return not result.is_empty()
## EXPERT USAGE:
## Add lights to group "lights".
## Call get_light_level() from Stealth AI.
extends Node
class_name SoundOcclusionManager
## Expert Sound Propagation (Godot 4.6).
## Emits noise events and checks for physical occlusion (walls).
func emit_noise(origin: Vector3, radius: float) -> void:
var npcs = get_tree().get_nodes_in_group("guards")
var space = get_viewport().get_world_3d().direct_space_state
for npc in npcs:
var dist = origin.distance_to(npc.global_position)
if dist > radius: continue
# Check for physical occlusion
var query = PhysicsRayQueryParameters3D.create(origin, npc.global_position)
query.collision_mask = 1 # World/Geometry layer
var result = space.intersect_ray(query)
# Expert Pattern: Muffle radius if blocked by geometry
var final_radius = radius * 0.4 if result else radius
if dist <= final_radius:
npc.on_noise_heard(origin)
## [SKILL NOTICE]: Use raycasts to 'muffle' sounds when blocked
## by static geometry, creating realistic acoustic occlusion.
# skills/genre-stealth/code/stealth_ai_controller.gd
extends CharacterBody3D
## Stealth AI Expert Pattern
## Implements Vision (Light-dependent), Hearing, and Alertness Meter.
enum State { IDLE, SUSPICIOUS, ALERT, COMBAT }
var current_state: State = State.IDLE
@export var detection_threshold: float = 100.0
var alertness_meter: float = 0.0
@onready var vision_cast: RayCast3D = $VisionCast
@onready var player: Node3D = get_tree().get_first_node_in_group("player")
func _physics_process(delta: float) -> void:
var can_see = _check_vision()
if can_see:
# 1. Light-Dependent Detection Speed
var light_level = _get_player_light_level()
alertness_meter += delta * 50.0 * light_level
else:
# Cool down alertness
alertness_meter -= delta * 10.0
alertness_meter = clamp(alertness_meter, 0, detection_threshold)
_update_state()
func _check_vision() -> bool:
if not player: return false
# 2. Field of View & Line of Sight
var dir_to_player = player.global_position - global_position
var angle = global_transform.basis.z.angle_to(dir_to_player)
if angle < deg_to_rad(45.0): # 90 degree FOV
vision_cast.target_position = vision_cast.to_local(player.global_position)
vision_cast.force_raycast_update()
return not vision_cast.is_colliding() # Assuming layer mask only hits world
return false
func _get_player_light_level() -> float:
# 3. Light Gem Logic
# In an expert setup, the player calculates their own 'exposure'
# based on light probes or specific light overlaps.
if player.has_method("get_light_exposure"):
return player.get_light_exposure()
return 1.0 # Default to fully visible
func on_sound_heard(sound_pos: Vector3, intensity: float) -> void:
# 4. Hearing Implementation
var dist = global_position.distance_to(sound_pos)
if dist < intensity * 10.0:
alertness_meter += intensity * 20.0
# Investigate sound source
# navigation_agent.target_position = sound_pos
func _update_state() -> void:
if alertness_meter >= detection_threshold:
current_state = State.ALERT
elif alertness_meter > 10.0:
current_state = State.SUSPICIOUS
else:
current_state = State.IDLE
## EXPERT NOTE:
## Use 'Composite Vision Cones'. A short, wide cone for immediate detection
## and a long, narrow cone for peripheral SUSPICION.
## Use the 'Reaction Delay' pattern: AI should pause for 0.5s when first
## spotting the player to give the player time to duck back into cover.
# stealth_patterns.gd
extends Node
# 1. Physics Server Raycasting (Nodeless LOS)
# EXPERT NOTE: Direct raycasting via the space state is drastically faster for many AI agents.
func has_line_of_sight(from: Vector3, to: Vector3, exclude: Array[RID]) -> bool:
var space := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.exclude = exclude
return space.intersect_ray(query).is_empty()
# 2. Vision Cone DOT Product
# EXPERT NOTE: Efficiently check if a player is within an AI's forward-facing view cone.
func target_in_cone(forward: Vector3, to_target: Vector3, threshold_dot: float) -> bool:
return forward.dot(to_target.normalized()) > threshold_dot
# 3. Spatial Audio Routing for AI Hearing
# EXPERT NOTE: Use specific buses for "noise" sounds so AI can listen to and prioritize them.
func play_stealth_noise(player: AudioStreamPlayer3D, is_loud: bool) -> void:
player.bus = &"AI_Audible" if is_loud else &"Default"
player.play()
# 4. Group Alert State Broadcasting
# EXPERT NOTE: Notify all guards in range without needing direct references to each.
func broadcast_alert() -> void:
get_tree().call_group(&"guards", &"enter_alert_mode")
# 5. Asynchronous Navigation Queries
# EXPERT NOTE: Query complex investigation paths without stalling the main thread.
func request_investigation(nav: NavigationAgent3D, target: Vector3) -> void:
var params := NavigationPathQueryParameters3D.new()
params.start_position = nav.global_position
params.target_position = target
# NavigationServer3D.query_path(...) can be used for advanced multi-pathing
# 6. Suspending Off-Screen AI Processors
# EXPERT NOTE: Use visibility notifiers to stop heavy stealth logic when the guard is off-screen.
func _on_visibility_notifier_screen_exited() -> void:
set_physics_process(false)
# 7. Light/Shadow Masking Check
# EXPERT NOTE: Logic for detecting if a player belongs to a specific "shadow" render layer.
func is_player_in_shadow(player: VisualInstance3D, shadow_layer: int) -> bool:
return bool(player.layers & (1 << shadow_layer))
# 8. Optimized AI State Matching (StringName)
# EXPERT NOTE: Use StringName for 2x faster hash comparisons in high-frequency AI state machines.
func process_stealth_state(state: StringName) -> void:
match state:
&"patrol": pass
&"alert": pass
&"combat": pass
# 9. ShapeCast3D for Noise Radius (Hearing)
# EXPERT NOTE: Get all entities within a physical "hearing" sphere instantly.
func get_entities_in_radius(pos: Transform3D, shape_rid: RID) -> Array:
var space := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.transform = pos
query.shape_rid = shape_rid
return space.intersect_shape(query)
# 10. Dynamic Avoidance Masking
# EXPERT NOTE: Enable RVO avoidance only for guards to prevent them from bumping into each other.
func setup_ai_avoidance(agent_rid: RID) -> void:
NavigationServer3D.agent_set_avoidance_enabled(agent_rid, true)
NavigationServer3D.agent_set_avoidance_mask(agent_rid, 1)
# skills/genre-stealth/scripts/stealth_vision_cone.gd
extends Area3D
## Stealth Vision Cone (Expert Pattern)
## Implements realistic vision with composite FOV (focused vs peripheral).
## Uses Dot Product and Raycasts for performance (avoids huge Area3D checks).
class_name StealthVisionCone
signal playback_alert(level: float) # 0.0 to 1.0
@export var head: Node3D # Eyes position
@export var vision_range: float = 20.0
@export var peripheral_angle: float = 120.0 # Degrees
@export var focused_angle: float = 45.0
@export var detection_speed: float = 0.5 # Per second
var detection_level: float = 0.0
var target: Node3D # The player
func _ready() -> void:
# Optimization: Only check player
target = get_tree().get_first_node_in_group("player")
func _physics_process(delta: float) -> void:
if not target: return
var player_visible = false
var detection_rate = 0.0
var to_target = target.global_position - head.global_position
var dist_sq = to_target.length_squared()
if dist_sq < vision_range * vision_range:
var forward = head.global_transform.basis.z
var dir_to_target = to_target.normalized()
var dot = forward.dot(dir_to_target)
var angle = rad_to_deg(acos(dot))
# 1. Check Focused Vision (Fast detection)
if angle < focused_angle / 2.0:
if _has_line_of_sight(target):
detection_rate = 1.5
player_visible = true
# 2. Check Peripheral Vision (Slow detection)
elif angle < peripheral_angle / 2.0:
if _has_line_of_sight(target):
detection_rate = 0.5
player_visible = true
# 3. Update Meter
if player_visible:
# Distance scaling: faster if closer
var dist_factor = 1.0 - (sqrt(dist_sq) / vision_range)
detection_level += detection_speed * detection_rate * dist_factor * delta * 5.0
else:
detection_level -= delta * 0.5 # Cool down
detection_level = clamp(detection_level, 0.0, 100.0)
playback_alert.emit(detection_level)
func _has_line_of_sight(obj: Node3D) -> bool:
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(head.global_position, obj.global_position + Vector3(0, 1, 0)) # Chest height
# Mask should exclude self and triggers, include world and player
query.collision_mask = 1 | 2 # Example layers
var result = space.intersect_ray(query)
if result and result.collider == obj:
return true
return false
## EXPERT USAGE:
## Attach to Enemy Head. Assign 'Target' via group or inspector.
extends Node
class_name VisibilityManager
## Expert Visibility Logic (Godot 4.6).
## Manages environmental stealth modifiers (shadows, bushes, lockers).
var global_visibility_mult: float = 1.0
var active_modifiers: Dictionary = {}
func set_modifier(id: String, multiplier: float) -> void:
active_modifiers[id] = multiplier
_update_global_mult()
func remove_modifier(id: String) -> void:
active_modifiers.erase(id)
_update_global_mult()
func _update_global_mult() -> void:
global_visibility_mult = 1.0
# Expert Pattern: Take the strongest hiding modifier (lowest mult)
for mult in active_modifiers.values():
global_visibility_mult = min(global_visibility_mult, mult)
## [SKILL NOTICE]: Use a centralized manager to calculate
## the player's detection susceptibility based on their environment.
extends Node3D
class_name VisionCone3D
## Expert Vision Cone (Godot 4.6).
## Combines Dot Product angle checks with Physics Raycasts for LoS.
@export var fov: float = 90.0
@export var view_distance: float = 20.0
@export var detection_speed: float = 0.5
var detection_level: float = 0.0
func _physics_process(delta: float) -> void:
var player = get_tree().get_first_node_in_group("player")
if not player: return
if _has_line_of_sight(player):
var dist_factor = 1.0 - (global_position.distance_to(player.global_position) / view_distance)
detection_level += delta * detection_speed * max(0.1, dist_factor)
else:
detection_level = max(0.0, detection_level - delta * 0.2)
func _has_line_of_sight(target: Node3D) -> bool:
var to_target = global_position.direction_to(target.global_position)
var forward = -global_transform.basis.z
# 1. Angle Check (Dot Product)
if forward.dot(to_target) < cos(deg_to_rad(fov/2)): return false
# 2. Physics Check (Raycast)
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(global_position, target.global_position)
var result = space.intersect_ray(query)
return result and result.collider == target
## [SKILL NOTICE]: Use 'intersect_ray' instead of multiple RayCast3D
## nodes for dynamic, high-performance line-of-sight checks.