Claude
Skills
Sign in
โ† Back

godot-genre-stealth

Included with Lifetime
$97 forever

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.

Designscripts

What this skill does


# 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 `RayCast3D` nodes 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 `VisibleOnScreenNotifier3D` to 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 check `is_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_iterations` to 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](scripts/stealth_ai_controller.gd) - Professional-grade NPC controller with composite vision, sound paths, and alert state logic.

### Modular Components
- [stealth_patterns.gd](scripts/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**:

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

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

### Player 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)
            
        

Related in Design