godot-genre-stealth

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "godot-genre-stealth" with this command: npx skills add thedivergentai/gd-agentic-skills/thedivergentai-gd-agentic-skills-godot-genre-stealth

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 → Complete

NEVER 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. has_line_of_sight() must check geometry. Wallhacks destroy stealth integrity.
  • NEVER use simple distance checks for sound — Sound propagates along NavigationServer3D 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).
  • 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):

  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.0

Sound 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 length

Player 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.5

2. 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 area

5. 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 * 100

Common Pitfalls

PitfallSolution
Instant detectionUse gradual detection with clear feedback
Guards see through wallsRaycast-based vision with proper collision
Unfair patrol patternsMake patterns learnable, with tells
Two games (stealth + combat)Either commit to stealth or make combat risky
Unclear detectionAlways 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

godot-master

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

godot-shaders-basics

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

godot-ui-theming

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

godot-particles

No summary provided by upstream source.

Repository SourceNeeds Review