A powerful 3D/2.5D game controller plugin for Bevy Engine.
The Stealth System provides comprehensive mechanics for hiding, cover-based gameplay, and detection avoidance. It enables players to hide from enemies, take cover behind objects, peek around corners, and manage their visibility through multiple states and mechanics. The system seamlessly integrates with the Character Controller, AI System, and Camera System to create immersive stealth gameplay experiences.
The Stealth System follows a state-based approach where the player transitions between various hide states based on input, cover availability, and detection status. Each state has unique camera behaviors, movement restrictions, and detection properties, allowing for varied stealth gameplay styles from static hiding to dynamic cover-to-cover movement.
The main configuration component that defines stealth behavior and capabilities:
Cover Detection Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| cover_detection_distance | f32 | 2.0 | Maximum distance for cover detection raycasts |
| cover_detection_angle | f32 | 90.0 | Field of view angle for cover detection (degrees) |
| cover_layer | u32 | 256 | Physics layer mask for cover objects (layer 8) |
Hide Requirements:
| Field | Type | Default | Description |
|——-|——|———|————-|
| character_need_to_crouch | bool | true | Whether crouching is required to hide |
| character_cant_move | bool | false | Whether movement breaks hiding |
| max_move_amount | f32 | 0.1 | Maximum movement allowed before breaking hide |
Detection Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| hidden_for_a_time | bool | false | Enable time-limited hiding |
| hidden_for_a_time_amount | f32 | 5.0 | Maximum duration for timed hiding (seconds) |
| time_delay_to_hide_again_if_discovered | f32 | 2.0 | Cooldown after being discovered (seconds) |
| check_if_character_can_be_hidden_again_rate | f32 | 0.5 | Rate for checking hide eligibility (seconds) |
| check_if_detected_while_hidden | bool | false | Enable detection checks while hiding |
Camera Rotation Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| camera_can_rotate | bool | true | Enable camera rotation while hidden |
| rotation_speed | f32 | 10.0 | Camera rotation speed multiplier |
| range_angle_x | Vec2 | (-90, 90) | Vertical rotation limits (degrees) |
| range_angle_y | Vec2 | (-90, 90) | Horizontal rotation limits (degrees) |
| use_spring_rotation | bool | false | Enable auto-return camera rotation |
| spring_rotation_delay | f32 | 1.0 | Delay before camera auto-returns (seconds) |
| smooth_camera_rotation_speed | f32 | 5.0 | Speed of smooth camera transitions |
Camera Movement Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| camera_can_move | bool | true | Enable camera translation while hidden |
| move_camera_speed | f32 | 10.0 | Camera movement speed multiplier |
| smooth_move_camera_speed | f32 | 5.0 | Speed of smooth camera position transitions |
| move_camera_limits_x | Vec2 | (-2, 2) | Horizontal movement limits (units) |
| move_camera_limits_y | Vec2 | (-2, 2) | Vertical movement limits (units) |
| use_spring_movement | bool | false | Enable auto-return camera movement |
| spring_movement_delay | f32 | 1.0 | Delay before camera auto-returns (seconds) |
Zoom Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| zoom_enabled | bool | false | Enable zoom functionality |
| zoom_speed | f32 | 10.0 | Zoom in/out speed |
| max_zoom | f32 | 10.0 | Maximum zoom level (minimum FOV) |
| min_zoom | f32 | 90.0 | Minimum zoom level (maximum FOV) |
| set_hidden_fov | bool | false | Override FOV when hidden |
| hidden_fov | f32 | 20.0 | FOV value when hidden (if enabled) |
Camera Reset Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| can_reset_camera_rotation | bool | true | Allow manual camera rotation reset |
| can_reset_camera_position | bool | true | Allow manual camera position reset |
UI Settings:
| Field | Type | Default | Description |
|——-|——|———|————-|
| use_character_state_icon | bool | true | Display state icon for visibility status |
| visible_character_state_name | String | "Visible" | Icon/text when visible |
| not_visible_character_state_name | String | "Not Visible" | Icon/text when hidden |
| use_message_when_unable_to_hide | bool | false | Show message when hiding is blocked |
| unable_to_hide_message | String | "" | Custom message text |
| show_message_time | f32 | 2.0 | Duration to display message (seconds) |
Runtime state component tracking the current stealth status:
Hide State:
| Field | Type | Description |
|——-|——|————-|
| is_hidden | bool | Whether character is currently hidden |
| can_be_hidden | bool | Whether hiding is currently allowed |
| is_detected | bool | Whether character has been detected by AI |
| last_time_hidden | f32 | Timestamp when last entered hiding |
| last_time_discovered | f32 | Timestamp when last discovered |
| last_time_check_if_can_be_hidden_again | f32 | Timestamp for eligibility checks |
| hidden_time | f32 | Duration of current hiding session |
Hide State Enum:
| Field | Type | Description |
|——-|——|————-|
| hide_state | HideState | Current hide state (see Hide States) |
| is_peeking | bool | Whether currently in peek state |
| is_corner_leaning | bool | Whether currently leaning around corner |
Camera State:
| Field | Type | Description |
|——-|——|————-|
| camera_is_free | bool | Whether camera is in free-look mode |
| current_look_angle | Vec2 | Current camera look angles (pitch, yaw) |
| current_camera_rotation | Quat | Current camera rotation quaternion |
| current_pivot_rotation | Quat | Current pivot rotation quaternion |
| current_move_camera_position | Vec3 | Current camera translation offset |
| current_camera_movement_position | Vec3 | Calculated camera movement position |
| current_fov_value | f32 | Current field of view value |
Input State:
| Field | Type | Description |
|——-|——|————-|
| horizontal_mouse | f32 | Horizontal mouse input value |
| vertical_mouse | f32 | Vertical mouse input value |
| horizontal_input | f32 | Horizontal movement input value |
| vertical_input | f32 | Vertical movement input value |
Zoom State:
| Field | Type | Description |
|——-|——|————-|
| increase_zoom | bool | Zoom in flag |
| decrease_zoom | bool | Zoom out flag |
| last_time_mouse_wheel_used | f32 | Timestamp of last zoom input |
| mouse_wheel_used_previously | bool | Whether zoom was recently used |
Spring Camera State:
| Field | Type | Description |
|——-|——|————-|
| last_time_spring_rotation | f32 | Timestamp for spring rotation timer |
| last_time_spring_movement | f32 | Timestamp for spring movement timer |
Component managing cover detection and current cover status:
| Field | Type | Default | Description |
|——-|——|———|————-|
| cover_objects | Vec<CoverObject> | [] | List of detected cover objects |
| is_in_cover | bool | false | Whether character is currently in cover |
| current_cover | Option<Entity> | None | Entity of the current cover object |
| cover_direction | Vec3 | Vec3::ZERO | Direction vector to cover |
| cover_normal | Vec3 | Vec3::ZERO | Surface normal of cover |
| cover_height | f32 | 0.0 | Height of cover relative to character |
| cover_type | CoverType | Low | Type of current cover |
CoverObject Struct:
| Field | Type | Description |
|——-|——|————-|
| entity | Entity | Entity reference |
| position | Vec3 | World position of cover |
| normal | Vec3 | Surface normal |
| height | f32 | Cover height |
| cover_type | CoverType | Type classification |
| is_corner | bool | Whether cover is a corner |
CoverType Enum:
Low - Waist-high cover (height < 0.5)Medium - Chest-high cover (0.5 <= height < 1.5)High - Full cover (height >= 1.5)Corner - Corner cover for leaningFull - Complete concealmentComponent tracking player visibility and detection status:
| Field | Type | Default | Description |
|——-|——|———|————-|
| current_visibility | f32 | 0.0 | Current visibility level (0.0 = hidden, 1.0 = visible) |
| detection_level | f32 | 0.0 | Detection progress (0.0 = undetected, 1.0 = detected) |
| sound_level | f32 | 0.0 | Current noise level (0.0 = silent, 1.0 = loud) |
| light_level | f32 | 0.0 | Current light exposure (0.0 = dark, 1.0 = bright) |
| visibility_decay_rate | f32 | 0.5 | Speed of detection decay per second |
| detection_increase_rate | f32 | 0.3 | Speed of detection increase per second |
| sound_decay_rate | f32 | 0.2 | Speed of sound decay per second |
| is_visible_to_ai | bool | false | Whether AI can see character |
| is_detected_by_ai | bool | false | Whether AI has detected character |
The Stealth System uses a dual-timestep approach with systems running in both Update and FixedUpdate:
System Execution Chain:
handle_stealth_input - Processes player input for hide, peek, lean, zoom, and camera controlsupdate_stealth_state - Updates camera rotation, movement, zoom, and spring behaviorsupdate_visibility_meter - Updates visibility, detection, and sound levelsSystem Execution:
detect_cover_objects - Performs raycasts to detect nearby cover objectscheck_line_of_sight - Checks AI line-of-sight and updates detection statusupdate_hide_states - Validates hide state requirements and enforces rulesThe Stealth System integrates with:
The system supports five distinct hide states, each with unique behaviors:
HideState Enum:
Visible - Default state, not hiding
CrouchHide - Hiding while crouched
ProneHide - Hiding while prone
Peek - Peeking from cover
CornerLean - Leaning around corner
FixedPlaceHide - Hiding at fixed location
State Characteristics:
| State | Movement | Camera Freedom | Detection Risk | Crouch Required | |——-|———-|—————-|—————-|—————–| | Visible | Full | Normal | High | No | | CrouchHide | Limited | Free-look | Low | Yes | | ProneHide | Minimal | Free-look | Very Low | Yes | | Peek | None | Limited | Medium | Yes | | CornerLean | None | Limited | Medium | Optional | | FixedPlaceHide | None | Free-look | Low | Optional |
Transition Rules:
Visible → CrouchHide
- Press Hide action
- character_need_to_crouch must be true
- Not currently detected
- Can be hidden is true
CrouchHide → Peek
- Press Peek action while hiding
- In cover
CrouchHide → CornerLean
- Press Corner Lean action while hiding
- At corner cover
Peek/CornerLean → CrouchHide
- Release Peek/Corner Lean action
Any Hidden State → Visible
- Press Hide action (toggle off)
- Detected by AI
- Move too much (if character_cant_move is true)
- Stand up (if character_need_to_crouch is true)
- Hide time limit exceeded (if hidden_for_a_time is true)
State Transition Diagram:
┌─────────┐
│ Visible │
└────┬────┘
│ Hide pressed
▼
┌──────────────┐
│ CrouchHide │◄────────┐
└──┬───────┬───┘ │
│ │ │
│ Peek │ Corner Lean │ Release/Detected
│ │ │
▼ ▼ │
┌──────┐ ┌──────────────┐│
│ Peek │ │ CornerLean ││
└──────┘ └──────────────┘│
│ │ │
└───────────┴─────────┘
The cover detection system uses raycasting to identify nearby cover objects:
Detection Process:
cover_detection_distancecover_layercover_detection_angleCoverDetection componentRaycast Configuration:
Cover is automatically classified based on height relative to character:
Height-Based Classification:
| Cover Type | Height Range | Description | Typical Objects | |————|————–|————-|—————–| | Low | < 0.5m | Waist-high | Crates, low walls, sandbags | | Medium | 0.5m - 1.5m | Chest-high | Barrels, counters, half-walls | | High | >= 1.5m | Full body | Walls, columns, large objects | | Corner | Variable | Corner edges | Building corners, wall ends | | Full | Variable | Complete concealment | Closets, alcoves | Cover Properties:
Each cover type provides different gameplay characteristics:
Low Cover:
Medium Cover:
High Cover:
Corner Cover:
Entering Cover:
is_in_cover flag set to trueCoverDetectionUsing Cover:
Leaving Cover:
Cover-Based Detection:
The system performs real-time line-of-sight checks between player and AI enemies:
Line of Sight Algorithm:
For each AI enemy:
1. Calculate distance between character and AI
2. Check if distance <= AI detection range
3. Calculate direction vector from character to AI
4. Perform raycast from character to AI
5. If raycast hits nothing (clear path):
- Character is visible to AI
- Set detection flag
- Start detection timer
6. If raycast hits obstacle:
- Character has cover
- No detection from this AI
Detection Validation:
Multiple AI Handling:
The visibility meter tracks gradual detection using multiple factors:
Detection Calculation:
Visibility Level (0.0 - 1.0):
0.0 - Fully hidden, invisible to AI1.0 - Fully visible, easily detectedDetection Progress (0.0 - 1.0):
0.0 - Undetected, safe1.0 - Fully detected, AI alerteddetection_increase_rate when visiblevisibility_decay_rate when hiddenDetection States:
| Detection Range | Status | AI Behavior | Player Feedback | |—————-|——–|————-|—————–| | 0.0 - 0.25 | Safe | Normal patrol | Green indicator | | 0.25 - 0.5 | Suspicious | Investigate | Yellow indicator | | 0.5 - 0.75 | Alert | Search actively | Orange indicator | | 0.75 - 1.0 | Detected | Chase/Attack | Red indicator | Detection Factors:
The system considers multiple factors for detection:
Movement generates noise that affects detection:
Sound Level Calculation:
| Movement State | Sound Level | Detection Modifier | |—————|————-|——————–| | Stationary | 0.0 | No detection bonus | | Walking | 0.3 | +30% detection range | | Running | 0.7 | +70% detection range | | Sprinting | 1.0 | +100% detection range | Sound Decay:
sound_decay_rate per second (default 0.2)Sound-Based Detection:
While hidden, the camera enters a specialized free-look mode:
Free-Look Activation:
Camera Rotation:
Horizontal Rotation (Yaw):
rotation_speed multiplierrange_angle_y (min, max degrees)smooth_camera_rotation_speedVertical Rotation (Pitch):
rotation_speed multiplierrange_angle_x (min, max degrees)smooth_camera_rotation_speedRotation Implementation:
Horizontal angle += mouse_x * rotation_speed
Vertical angle -= mouse_y * rotation_speed
Horizontal clamped to [range_angle_y.min, range_angle_y.max]
Vertical clamped to [range_angle_x.min, range_angle_x.max]
Camera rotation = Quat from Euler angles (vertical, 0, 0)
Pivot rotation = Quat from Euler angles (0, horizontal, 0)
Camera Translation:
Horizontal Movement:
move_camera_speed multipliermove_camera_limits_x (min, max units)Vertical Movement:
move_camera_speed multipliermove_camera_limits_y (min, max units)Camera Reset:
Peeking allows limited exposure to gain visibility:
Peek Activation:
HideState::PeekPeek Camera Behavior:
Peek Detection Risk:
Peek Duration:
Optional zoom functionality for enhanced visibility:
Zoom Controls:
Zoom Parameters:
Zoom Implementation:
If increase_zoom:
current_fov -= delta_time * zoom_speed
If decrease_zoom:
current_fov += delta_time * zoom_speed
current_fov clamped to [max_zoom, min_zoom]
Zoom Features:
Corner leaning provides tactical positioning at corners:
Lean Activation:
HideState::CornerLeanLean Mechanics:
Lean Types:
Tactical Advantages:
Automatic camera return for intuitive controls:
Spring Rotation:
spring_rotation_delay seconds of no inputSpring Movement:
spring_movement_delay seconds of no inputSpring Behavior:
If no rotation input for > spring_rotation_delay:
Interpolate camera rotation to identity
Interpolate pivot rotation to identity
If no movement input for > spring_movement_delay:
Interpolate camera position to zero offset
Use Cases:
Specialized hiding at designated locations:
Concept:
Implementation:
Examples:
Benefits:
The Stealth System tightly integrates with the AI System:
AI Detection Range:
detection_range fieldAI Behavior Triggers:
Faction Integration:
Alert System:
AI State Changes on Detection:
Player Detected:
AI State: Idle/Patrol → Suspect → Chase
AI Speed: patrol_speed → chase_speed
AI Behavior: Passive → Aggressive
AI Memory: Stores last known position
Stealth Recovery:
time_delay_to_hide_again_if_discoveredMinimal Configuration:
use bevy::prelude::*;
use bevy_allinone::prelude::*;
fn spawn_stealth_character(mut commands: Commands, position: Vec3) -> Entity {
commands.spawn((
Name::new("Stealth Character"),
// Stealth components
StealthController::default(),
StealthState::default(),
CoverDetection::default(),
VisibilityMeter::default(),
// Character controller
CharacterController::default(),
CharacterMovementState::default(),
// Input
InputState::default(),
// Transform
Transform::from_translation(position),
GlobalTransform::default(),
// Physics
RigidBody::Dynamic,
Collider::capsule(0.4, 1.0),
LockedAxes::ROTATION_LOCKED,
))
.id()
}
Advanced Setup:
fn spawn_custom_stealth_character(mut commands: Commands) -> Entity {
commands.spawn((
Name::new("Custom Stealth Character"),
StealthController {
// Cover detection
cover_detection_distance: 3.0,
cover_detection_angle: 120.0,
// Hide requirements
character_need_to_crouch: true,
character_cant_move: true,
max_move_amount: 0.05,
// Detection
check_if_detected_while_hidden: true,
time_delay_to_hide_again_if_discovered: 3.0,
// Camera rotation
camera_can_rotate: true,
rotation_speed: 15.0,
range_angle_x: Vec2::new(-60.0, 60.0),
range_angle_y: Vec2::new(-90.0, 90.0),
use_spring_rotation: true,
spring_rotation_delay: 2.0,
// Camera movement
camera_can_move: true,
move_camera_speed: 8.0,
move_camera_limits_x: Vec2::new(-1.5, 1.5),
move_camera_limits_y: Vec2::new(-1.5, 1.5),
use_spring_movement: true,
spring_movement_delay: 2.0,
// Zoom
zoom_enabled: true,
zoom_speed: 20.0,
max_zoom: 15.0,
min_zoom: 75.0,
// UI
use_character_state_icon: true,
use_message_when_unable_to_hide: true,
unable_to_hide_message: "Cannot hide while being watched!".to_string(),
..default()
},
StealthState::default(),
CoverDetection::default(),
VisibilityMeter {
visibility_decay_rate: 0.8,
detection_increase_rate: 0.5,
sound_decay_rate: 0.3,
..default()
},
// ... other components
))
.id()
}
Creating Cover Objects:
fn spawn_cover_object(
mut commands: Commands,
position: Vec3,
cover_type: CoverType,
) -> Entity {
let height = match cover_type {
CoverType::Low => 0.5,
CoverType::Medium => 1.0,
CoverType::High => 2.0,
_ => 1.0,
};
commands.spawn((
Name::new(format!("{:?} Cover", cover_type)),
Transform::from_translation(position),
GlobalTransform::default(),
// Physics - make sure it's on the cover layer
RigidBody::Static,
Collider::cuboid(1.0, height, 1.0),
CollisionLayers::new([Layer::Cover], [Layer::All]),
))
.id()
}
Required Input Actions:
The stealth system requires the following input actions to be mapped:
InputAction::Hide - Toggle hidingInputAction::Peek - Toggle peekingInputAction::CornerLean - Toggle corner leaningInputAction::ResetCamera - Reset camera to neutralInputAction::ZoomIn - Zoom in (decrease FOV)InputAction::ZoomOut - Zoom out (increase FOV)Example Input Mapping:
// This is typically configured in your input system
let input_mappings = vec![
(InputAction::Hide, KeyCode::C),
(InputAction::Peek, KeyCode::Q),
(InputAction::CornerLean, KeyCode::E),
(InputAction::ResetCamera, KeyCode::R),
(InputAction::ZoomIn, MouseScrollUp),
(InputAction::ZoomOut, MouseScrollDown),
];
Raycast Optimization:
cover_detection_distanceLine of Sight Optimization:
check_if_detected_while_hidden enabledUpdate Frequency:
Component Organization:
check_if_detected_while_hidden to false if not neededcheck_if_character_can_be_hidden_again_rateExtending HideState Enum:
// Add new state to enum
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Reflect)]
pub enum HideState {
// ... existing states ...
Crawling, // New: crawling hide state
VentHiding, // New: hiding in vents
WaterHiding, // New: hiding underwater
}
Implementing Custom State Logic:
fn update_custom_hide_states(
mut query: Query
) {
for (stealth, mut state, transform) in query.iter_mut() {
match state.hide_state {
HideState::Crawling => {
// Custom crawling logic
state.camera_is_free = false;
// Implement crawling movement restrictions
}
HideState::VentHiding => {
// Custom vent hiding logic
state.camera_is_free = true;
// Implement vent-specific mechanics
}
HideState::WaterHiding => {
// Custom water hiding logic
// Implement oxygen system, bubble detection, etc.
}
_ => {}
}
}
}
Adding Specialized Cover:
// Extend CoverType enum
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Reflect)]
pub enum CoverType {
// ... existing types ...
Foliage, // Bushes, grass
Water, // Puddles, ponds
Crowd, // Blend into groups
Shadow, // Dark areas
Dynamic, // Moving cover (vehicles, etc.)
}
Custom Cover Detection:
fn detect_custom_cover_types(
mut query: Query,
foliage_query: Query>,
) {
for (stealth, mut cover, transform) in query.iter_mut() {
// Check proximity to foliage
for foliage_transform in foliage_query.iter() {
let distance = (foliage_transform.translation - transform.translation).length();
if distance < 1.0 {
cover.is_in_cover = true;
cover.cover_type = CoverType::Foliage;
cover.cover_height = foliage_transform.scale.y;
break;
}
}
}
}
Environmental Detection Modifiers:
#[derive(Component)]
pub struct EnvironmentalDetection {
pub weather_modifier: f32, // Rain/fog reduces detection
pub time_of_day_modifier: f32, // Night reduces detection
pub noise_level: f32, // Ambient noise masks footsteps
}
fn update_environmental_detection(
mut query: Query,
environment: Res,
) {
for (mut visibility, env_detection, transform) in query.iter_mut() {
// Weather modifier
let weather_factor = match environment.weather {
Weather::Rain | Weather::Fog => 0.5,
Weather::Clear => 1.0,
_ => 0.8,
};
// Time of day modifier
let time_factor = if environment.is_night() { 0.3 } else { 1.0 };
// Calculate final detection modifier
let total_modifier = weather_factor * time_factor * env_detection.noise_level;
// Apply to visibility
visibility.current_visibility *= total_modifier;
}
}
Implementing New Camera Modes:
#[derive(Component)]
pub struct CustomCameraMode {
pub mode: CameraMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CameraMode {
OverShoulder,
FirstPerson,
TopDown,
Isometric,
}
fn update_custom_camera_modes(
mut query: Query,
camera_query: Query, Without)>,
) {
for (state, camera_mode, character_transform) in query.iter() {
if !state.is_hidden {
continue;
}
for mut camera_transform in camera_query.iter_mut() {
match camera_mode.mode {
CameraMode::OverShoulder => {
// Implement over-shoulder camera
let offset = Vec3::new(0.5, 0.5, -2.0);
camera_transform.translation = character_transform.translation + offset;
}
CameraMode::FirstPerson => {
// Implement first-person camera
let offset = Vec3::new(0.0, 1.7, 0.0);
camera_transform.translation = character_transform.translation + offset;
}
CameraMode::TopDown => {
// Implement top-down camera
let offset = Vec3::new(0.0, 10.0, 0.0);
camera_transform.translation = character_transform.translation + offset;
camera_transform.rotation = Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2);
}
_ => {}
}
}
}
}
Stealth Sequences:
// Patrol guards → Find cover → Wait → Move to next cover → Reach objective
Detection Escalation:
// Suspicious → Investigating → Alert → Combat → Search → Return to patrol
Cover Progression:
// Safe area → Open space with cover → Guarded area → Objective
Visualization:
Logging:
Testing:
Character Can’t Hide:
can_be_hidden flagCover Not Detected:
cover_detection_distancecover_layer maskCamera Not Moving:
camera_can_rotate flagcamera_is_free is trueAI Detects Through Walls:
Spring Camera Not Working:
use_spring_rotation/use_spring_movementHigh Raycast Cost:
AI Detection Lag:
Considerations for networked stealth gameplay:
Client-Server Architecture:
Synchronization:
Connecting stealth to animation system:
Animation States:
Animation Blending:
Advanced AI behaviors for stealth:
Alert Sharing:
Smart Searching:
Potential areas for expansion: