godot-genre-stealth
Original:🇺🇸 English
Translated
Expert blueprint for stealth games (Splinter Cell, Hitman, Dishonored, Thief) covering AI detection systems, vision cones, sound propagation, alert states, light/shadow mechanics, and systemic design. Use when building stealth-action, tactical infiltration, or immersive sim games requiring enemy awareness systems. Keywords vision cone, detection, alert state, sound propagation, light level, systemic AI, gradual detection.
22installs
Added on
NPX Install
npx skill4agent add thedivergentai/gd-agentic-skills godot-genre-stealthTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Genre: Stealth
Player choice, systemic AI, and clear communication define stealth games.
Available Scripts
stealth_ai_controller.gd
Expert AI controller with graduated detection, sound response, and alert state management.
Core Loop
Observe → Plan → Execute → Adapt → CompleteNEVER Do in Stealth Games
- NEVER use instant binary detection — Gradual 0-100% detection with visual feedback (filling meter). Binary "seen/not seen" removes player agency and feels unfair.
- NEVER make guards see through walls — Raycast-based vision with collision masks. must check geometry. Wallhacks destroy stealth integrity.
has_line_of_sight() - NEVER use simple distance checks for sound — Sound propagates along paths, NOT straight-line distance. Through-wall hearing breaks immersion. -NEVER make combat as viable as stealth — If guns are easier than sneaking, players ignore stealth. Combat should be risky (outnumbered, limited ammo, loud alerts).
NavigationServer3D - NEVER hide detection reasons from player — Show WHY detected (light level high, made noise, in vision cone). "Gotcha" deaths frustrate, don't teach.
- NEVER use single sample point for player visibility — Sample multiple body parts (head, torso, feet). Hiding behind low cover should hide torso but expose head.
- NEVER forget peripheral vision — Humans have ~180° peripheral (less effective) + 60° focused vision. Single cone = unrealistic. Use composite shapes (Splinter Cell method).
Design Principles
From industry experts (Splinter Cell, Dishonored, Hitman developers):
- Player Choice: Multiple valid approaches to every scenario
- Systemic Design: Rules-based AI that players can learn and exploit
- Clear Communication: Player always understands game state and threats
- 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:
gdscript
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:
gdscript
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
gdscript
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):
gdscript
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!)
gdscript
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 = falsePlayer Abilities
Five categories of stealth tools (per Mark Brown's analysis):
1. Movement Alteration
gdscript
# 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
gdscript
# 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
gdscript
# 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
gdscript
# Shoot out lights, create hiding spots
func shoot_light(light: Light3D) -> void:
light.visible = false
# Update light level for area5. Enemy Elimination
gdscript
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:
gdscript
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 * 100Common 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
- Raycasts for vision: Use with collision masks
PhysicsRayQueryParameters3D - NavigationAgent3D: For patrol routes and pathfinding
- Area3D: For sound propagation zones and trigger areas
- AnimationTree: Blend between alert state animations
Reference
- Master Skill: godot-master