Ejemplos de Bevy Game Engine

Ejemplos del motor de juego Bevy Rust con ECS, gráficos 2D/3D, audio y mecánicas de juego

💻 Bevy Hello World rust

🟢 simple ⭐⭐

Configuración básica de aplicación Bevy con creación de ventana e introducción a arquitectura ECS

⏱️ 15 min 🏷️ bevy, rust, gamedev, ecs
Prerequisites: Basic Rust knowledge, ECS (Entity Component System) concepts
// Bevy Hello World - Basic Game Engine Setup
// Cargo.toml dependencies:
// bevy = "0.13"

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, hello_world_system)
        .run();
}

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Name(String);

#[derive(Resource)]
struct GameState {
    score: u32,
    level: u32,
}

fn setup(mut commands: Commands) {
    // Spawn 2D camera
    commands.spawn(Camera2dBundle::default());

    // Initialize game state resource
    commands.insert_resource(GameState {
        score: 0,
        level: 1,
    });

    // Create player entity
    commands.spawn((
        SpriteBundle {
            sprite: Sprite {
                color: Color::rgb(0.25, 0.25, 0.75),
                custom_size: Some(Vec2::new(50.0, 50.0)),
                ..default()
            },
            transform: Transform::from_xyz(0.0, 0.0, 0.0),
            ..default()
        },
        Player,
        Name("Player".to_string()),
    ));

    // Create some sprites
    for i in 0..5 {
        commands.spawn((
            SpriteBundle {
                sprite: Sprite {
                    color: Color::hsl(i as f32 * 72.0, 0.7, 0.5),
                    custom_size: Some(Vec2::new(30.0, 30.0)),
                    ..default()
                },
                transform: Transform::from_xyz(
                    (i as f32 - 2.0) * 100.0,
                    150.0,
                    0.0
                ),
                ..default()
            },
            Name(format!("Sprite {}", i + 1)),
        ));
    }

    println!("Game initialized! Bevy ECS systems are running.");
}

fn hello_world_system(
    time: Res<Time>,
    mut game_state: ResMut<GameState>,
    query: Query<&Name>,
) {
    // Update game state
    game_state.score += 1;

    // Print hello world with game info
    if time.elapsed_seconds() as u32 % 2 == 0 {
        println!("Hello from Bevy Game Engine!");
        println!("Score: {}, Level: {}", game_state.score, game_state.level);
        println!("Active entities: {}", query.iter().count());
    }
}

💻 Juego de Plataformas 2D rust

🟡 intermediate ⭐⭐⭐

Juego de plataformas 2D completo con movimiento de jugador, saltos, detección de colisiones y diseño de niveles

⏱️ 30 min 🏷️ bevy, rust, gamedev, platformer, 2d
Prerequisites: Rust basics, Bevy ECS fundamentals, Basic game physics
// Bevy 2D Platformer Game
// Features: Player movement, jumping, physics, collision detection

use bevy::prelude::*;

const GRAVITY: f32 = -980.0;
const PLAYER_SPEED: f32 = 200.0;
const JUMP_FORCE: f32 = 400.0;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(ClearColor(Color::rgb(0.53, 0.81, 0.98)))
        .add_event::<JumpEvent>()
        .add_systems(Startup, setup)
        .add_systems(
            Update,
            (
                player_movement,
                player_jump,
                apply_gravity,
                check_ground_collision,
                camera_follow_player,
            ).chain(),
        )
        .run();
}

#[derive(Component)]
struct Player {
    velocity: Vec2,
    is_grounded: bool,
}

#[derive(Component)]
struct Ground;

#[derive(Component)]
struct Platform;

#[derive(Event)]
struct JumpEvent;

#[derive(Bundle)]
struct PlayerBundle {
    player: Player,
    sprite_bundle: SpriteBundle,
}

impl PlayerBundle {
    fn new(x: f32, y: f32) -> Self {
        Self {
            player: Player {
                velocity: Vec2::ZERO,
                is_grounded: false,
            },
            sprite_bundle: SpriteBundle {
                sprite: Sprite {
                    color: Color::rgb(0.3, 0.3, 0.8),
                    custom_size: Some(Vec2::new(40.0, 60.0)),
                    ..default()
                },
                transform: Transform::from_xyz(x, y, 1.0),
                ..default()
            },
        }
    }
}

fn setup(mut commands: Commands) {
    // Spawn camera
    commands.spawn(Camera2dBundle::default());

    // Spawn player
    commands.spawn(PlayerBundle::new(0.0, 100.0));

    // Create ground
    commands.spawn((
        SpriteBundle {
            sprite: Sprite {
                color: Color::rgb(0.2, 0.7, 0.2),
                custom_size: Some(Vec2::new(800.0, 50.0)),
                ..default()
            },
            transform: Transform::from_xyz(0.0, -250.0, 0.0),
            ..default()
        },
        Ground,
    ));

    // Create platforms
    let platforms = [
        (-200.0, 0.0, 120.0, 20.0),
        (200.0, 50.0, 120.0, 20.0),
        (0.0, 150.0, 100.0, 20.0),
        (-300.0, 200.0, 80.0, 20.0),
        (300.0, 250.0, 100.0, 20.0),
    ];

    for (x, y, width, height) in platforms {
        commands.spawn((
            SpriteBundle {
                sprite: Sprite {
                    color: Color::rgb(0.6, 0.4, 0.2),
                    custom_size: Some(Vec2::new(width, height)),
                    ..default()
                },
                transform: Transform::from_xyz(x, y, 0.0),
                ..default()
            },
            Platform,
        ));
    }
}

fn player_movement(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut player_query: Query<&mut Player, With<Player>>,
    time: Res<Time>,
) {
    for mut player in player_query.iter_mut() {
        let mut direction = 0.0;

        if keyboard_input.pressed(KeyCode::ArrowLeft) {
            direction -= 1.0;
        }
        if keyboard_input.pressed(KeyCode::ArrowRight) {
            direction += 1.0;
        }

        player.velocity.x = direction * PLAYER_SPEED;
    }
}

fn player_jump(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut jump_events: EventWriter<JumpEvent>,
    player_query: Query<&Player, With<Player>>,
) {
    if let Ok(player) = player_query.get_single() {
        if keyboard_input.just_pressed(KeyCode::Space) && player.is_grounded {
            jump_events.send(JumpEvent);
        }
    }
}

fn apply_gravity(
    mut player_query: Query<(&mut Player, &mut Transform)>,
    time: Res<Time>,
) {
    for (mut player, mut transform) in player_query.iter_mut() {
        player.velocity.y += GRAVITY * time.delta_seconds();
        transform.translation.y += player.velocity.y * time.delta_seconds();
        transform.translation.x += player.velocity.x * time.delta_seconds();
    }
}

fn check_ground_collision(
    mut jump_events: EventReader<JumpEvent>,
    mut player_query: Query<(&mut Player, &Transform), Without<Ground>>,
    ground_query: Query<(Entity, &Transform), With<Ground>>,
    platform_query: Query<(Entity, &Transform), With<Platform>>,
) {
    for (mut player, player_transform) in player_query.iter_mut() {
        let player_bottom = player_transform.translation.y - 30.0;

        // Check ground collision
        for (_, ground_transform) in ground_query.iter() {
            let ground_top = ground_transform.translation.y + 25.0;

            if player_bottom <= ground_top
                && player_bottom >= ground_top - 10.0
                && player.velocity.y <= 0.0 {
                player.velocity.y = 0.0;
                player.is_grounded = true;
                return;
            }
        }

        // Check platform collision
        for (_, platform_transform) in platform_query.iter() {
            let platform_top = platform_transform.translation.y + 10.0;
            let platform_left = platform_transform.translation.x - platform_transform.scale.x / 2.0;
            let platform_right = platform_transform.translation.x + platform_transform.scale.x / 2.0;
            let player_x = player_transform.translation.x;

            if player_bottom <= platform_top
                && player_bottom >= platform_top - 10.0
                && player_x >= platform_left
                && player_x <= platform_right
                && player.velocity.y <= 0.0 {
                player.velocity.y = 0.0;
                player.is_grounded = true;
                return;
            }
        }

        player.is_grounded = false;
    }

    // Handle jump events
    for _event in jump_events.read() {
        for (mut player, _) in player_query.iter_mut() {
            if player.is_grounded {
                player.velocity.y = JUMP_FORCE;
                player.is_grounded = false;
            }
        }
    }
}

fn camera_follow_player(
    player_query: Query<&Transform, (With<Player>, Without<Camera>)>,
    mut camera_query: Query<&mut Transform, (With<Camera>, Without<Player>)>,
) {
    if let (Ok(player_transform), Ok(mut camera_transform)) =
        (player_query.get_single(), camera_query.get_single_mut()) {
        let target_x = player_transform.translation.x;
        let current_x = camera_transform.translation.x;

        // Smooth camera follow
        let new_x = current_x + (target_x - current_x) * 0.1;
        camera_transform.translation.x = new_x;
    }
}

💻 Escena 3D con Iluminación rust

🟡 intermediate ⭐⭐⭐⭐

Creación de escenas 3D con modelos, iluminación, materiales y controles de cámara

⏱️ 25 min 🏷️ bevy, rust, gamedev, 3d, graphics
Prerequisites: 3D graphics basics, Bevy 3D concepts, Linear algebra
// Bevy 3D Scene with Lighting and Camera Controls
// Features: 3D primitives, lighting, materials, camera orbit

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(ClearColor(Color::rgb(0.1, 0.1, 0.15)))
        .add_systems(Startup, setup)
        .add_systems(Update, (camera_orbit, animate_objects))
        .run();
}

#[derive(Component)]
struct RotatingObject {
    speed: f32,
}

#[derive(Component)]
struct MainCamera {
    focus: Vec3,
    distance: f32,
    angle: f32,
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // Spawn camera
    commands.spawn((
        Camera3dBundle {
            transform: Transform::from_xyz(0.0, 5.0, 8.0)
                .looking_at(Vec3::ZERO, Vec3::Y),
            ..default()
        },
        MainCamera {
            focus: Vec3::ZERO,
            distance: 8.0,
            angle: 0.0,
        },
    ));

    // Add directional light (sun)
    commands.spawn(DirectionalLightBundle {
        directional_light: DirectionalLight {
            illuminance: 10000.0,
            shadows_enabled: true,
            ..default()
        },
        transform: Transform::from_rotation(Quat::from_euler(
            EulerRot::XYZ,
            -0.5,
            0.5,
            0.0,
        )),
        ..default()
    });

    // Add ambient light
    commands.insert_resource(AmbientLight {
        color: Color::rgb(0.2, 0.2, 0.3),
        brightness: 0.3,
    });

    // Create ground plane
    commands.spawn(PbrBundle {
        mesh: meshes.add(Circle::new(7.0)),
        material: materials.add(StandardMaterial {
            base_color: Color::rgb(0.1, 0.3, 0.1),
            perceptual_roughness: 0.8,
            metallic: 0.0,
            ..default()
        }),
        transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
        ..default()
    });

    // Create central cube
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Cuboid::new(2.0, 2.0, 2.0)),
            material: materials.add(StandardMaterial {
                base_color: Color::rgb(0.8, 0.7, 0.6),
                metallic: 0.1,
                perceptual_roughness: 0.4,
                ..default()
            }),
            transform: Transform::from_xyz(0.0, 1.0, 0.0),
            ..default()
        },
        RotatingObject { speed: 0.5 },
    ));

    // Create spheres in circle
    for i in 0..8 {
        let angle = i as f32 * std::f32::consts::TAU / 8.0;
        let x = angle.cos() * 4.0;
        let z = angle.sin() * 4.0;

        commands.spawn((
            PbrBundle {
                mesh: meshes.add(Sphere::new(0.5)),
                material: materials.add(StandardMaterial {
                    base_color: Color::hsl(i as f32 * 45.0, 0.7, 0.5),
                    metallic: 0.2,
                    perceptual_roughness: 0.3,
                    ..default()
                }),
                transform: Transform::from_xyz(x, 0.5, z),
                ..default()
            },
            RotatingObject { speed: 0.3 + (i as f32 * 0.1) },
        ));
    }

    // Create torus
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Torus {
                major_radius: 1.5,
                minor_radius: 0.3,
            }),
            material: materials.add(StandardMaterial {
                base_color: Color::rgb(0.9, 0.4, 0.4),
                metallic: 0.6,
                perceptual_roughness: 0.2,
                ..default()
            }),
            transform: Transform::from_xyz(-3.0, 2.0, 0.0),
            ..default()
        },
        RotatingObject { speed: 0.8 },
    ));

    // Create cylinder
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Cylinder::new(0.5, 3.0)),
            material: materials.add(StandardMaterial {
                base_color: Color::rgb(0.4, 0.4, 0.9),
                metallic: 0.3,
                perceptual_roughness: 0.5,
                ..default()
            }),
            transform: Transform::from_xyz(3.0, 1.5, 0.0),
            ..default()
        },
        RotatingObject { speed: 0.4 },
    ));

    // Create icosphere
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Icosphere::new(0.8, 5)),
            material: materials.add(StandardMaterial {
                base_color: Color::rgb(0.9, 0.8, 0.3),
                metallic: 0.7,
                perceptual_roughness: 0.1,
                ..default()
            }),
            transform: Transform::from_xyz(0.0, 3.0, -2.0),
            ..default()
        ),
        RotatingObject { speed: 0.6 },
    ));
}

fn camera_orbit(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut camera_query: Query<&mut Transform, With<MainCamera>>,
    mut camera: ResMut<MainCamera>,
    time: Res<Time>,
) {
    if let Ok(mut transform) = camera_query.get_single_mut() {
        // Rotate camera around focus point
        if keyboard_input.pressed(KeyCode::ArrowLeft) {
            camera.angle -= 1.5 * time.delta_seconds();
        }
        if keyboard_input.pressed(KeyCode::ArrowRight) {
            camera.angle += 1.5 * time.delta_seconds();
        }

        // Zoom in/out
        if keyboard_input.pressed(KeyCode::ArrowUp) {
            camera.distance = (camera.distance - 2.0 * time.delta_seconds()).max(3.0);
        }
        if keyboard_input.pressed(KeyCode::ArrowDown) {
            camera.distance = (camera.distance + 2.0 * time.delta_seconds()).min(20.0);
        }

        // Move focus point
        let mut focus_change = Vec3::ZERO;
        if keyboard_input.pressed(KeyCode::KeyW) {
            focus_change.z -= 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyS) {
            focus_change.z += 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyA) {
            focus_change.x -= 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyD) {
            focus_change.x += 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyQ) {
            focus_change.y -= 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyE) {
            focus_change.y += 1.0;
        }

        camera.focus += focus_change * 2.0 * time.delta_seconds();

        // Update camera position
        let x = camera.angle.cos() * camera.distance;
        let z = camera.angle.sin() * camera.distance;

        transform.translation = camera.focus + Vec3::new(x, 5.0, z);
        transform.look_at(camera.focus, Vec3::Y);
    }
}

fn animate_objects(
    mut objects_query: Query<(&mut Transform, &RotatingObject)>,
    time: Res<Time>,
) {
    for (mut transform, rotating) in objects_query.iter_mut() {
        transform.rotate_y(rotating.speed * time.delta_seconds());
        transform.rotate_x(rotating.speed * 0.7 * time.delta_seconds());

        // Add floating motion
        let float_offset = time.elapsed_seconds() * rotating.speed;
        transform.translation.y += (float_offset).sin() * 0.01;
    }
}

💻 Sistema de Audio rust

🟡 intermediate ⭐⭐⭐

Sistema completo de gestión de audio con efectos de sonido, música de fondo y audio espacial

⏱️ 20 min 🏷️ bevy, rust, gamedev, audio, sound
Prerequisites: Bevy basics, Audio fundamentals, Event systems
// Bevy Audio System with Sound Effects and Music
// Features: Sound manager, spatial audio, audio mixing

use bevy::prelude::*;

// Asset paths for audio files
const JUMP_SOUND: &str = "sounds/jump.wav";
const COIN_SOUND: &str = "sounds/coin.wav";
const BACKGROUND_MUSIC: &str = "music/background.ogg";
const EXPLOSION_SOUND: &str = "sounds/explosion.wav";

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Bevy Audio System".into(),
                resolution: (800.0, 600.0).into(),
                ..default()
            }),
            ..default()
        }))
        .add_event::<PlaySoundEvent>()
        .add_event::<ChangeMusicEvent>()
        .insert_resource(AudioState {
            music_volume: 0.7,
            sfx_volume: 0.8,
            is_muted: false,
            current_music: None,
        })
        .add_systems(Startup, setup)
        .add_systems(Update, (
            handle_sound_events,
            handle_music_events,
            handle_keyboard_input,
            update_audio_state,
            spawn_sound_effects,
        ))
        .run();
}

#[derive(Resource)]
struct AudioState {
    music_volume: f32,
    sfx_volume: f32,
    is_muted: bool,
    current_music: Option<Handle<AudioSink>>,
}

#[derive(Event)]
struct PlaySoundEvent {
    sound: SoundType,
    position: Option<Vec3>,
}

#[derive(Event)]
struct ChangeMusicEvent {
    music: MusicType,
    fade_duration: Option<f32>,
}

#[derive(Component)]
struct AudioSource {
    sound_type: SoundType,
    cooldown: Timer,
}

#[derive(Component)]
struct MusicController;

#[derive(Clone, Copy, PartialEq)]
enum SoundType {
    Jump,
    Coin,
    Explosion,
    Footstep,
}

#[derive(Clone, Copy, PartialEq)]
enum MusicType {
    MainMenu,
    Gameplay,
    Victory,
    BossBattle,
}

#[derive(Component)]
struct SpatialSound {
    max_distance: f32,
    reference_distance: f32,
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    // Spawn 2D camera
    commands.spawn(Camera2dBundle::default());

    // Create UI for audio controls
    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                flex_direction: FlexDirection::Column,
                align_items: AlignItems::Center,
                padding: UiRect::all(Val::Px(20.0)),
                ..default()
            },
            ..default()
        })
        .with_children(|parent| {
            // Title
            parent.spawn(TextBundle::from_section(
                "Audio System Demo",
                TextStyle {
                    font_size: 40.0,
                    color: Color::WHITE,
                    ..default()
                },
            ));

            // Instructions
            parent.spawn(TextBundle::from_section(
                "Controls:
                Space - Play Jump Sound
                C - Play Coin Sound
                E - Play Explosion Sound
                M - Toggle Music
                Arrow Up/Down - Music Volume
                Arrow Left/Right - SFX Volume
                P - Mute/Unmute",
                TextStyle {
                    font_size: 20.0,
                    color: Color::rgb(0.8, 0.8, 0.8),
                    ..default()
                },
            ));

            // Status display will be added here
        });

    // Load audio assets
    asset_server.load(JUMP_SOUND);
    asset_server.load(COIN_SOUND);
    asset_server.load(EXPLOSION_SOUND);
    asset_server.load(BACKGROUND_MUSIC);

    // Spawn some sound source objects
    spawn_sound_objects(&mut commands);
}

fn spawn_sound_objects(mut commands: Commands) {
    // Create objects that can emit sounds
    let objects = [
        (Vec3::new(-200.0, 0.0, 0.0), Color::RED),
        (Vec3::new(0.0, 0.0, 0.0), Color::GREEN),
        (Vec3::new(200.0, 0.0, 0.0), Color::BLUE),
    ];

    for (pos, color) in objects {
        commands.spawn((
            SpriteBundle {
                sprite: Sprite {
                    color,
                    custom_size: Some(Vec2::new(50.0, 50.0)),
                    ..default()
                },
                transform: Transform::from_xyz(pos.x, pos.y, pos.z),
                ..default()
            },
            AudioSource {
                sound_type: SoundType::Coin,
                cooldown: Timer::from_seconds(1.0, TimerMode::Once),
            },
            SpatialSound {
                max_distance: 300.0,
                reference_distance: 50.0,
            },
        ));
    }
}

fn handle_sound_events(
    mut commands: Commands,
    mut sound_events: EventReader<PlaySoundEvent>,
    asset_server: Res<AssetServer>,
    audio_state: Res<AudioState>,
) {
    for event in sound_events.read() {
        if audio_state.is_muted {
            continue;
        }

        let sound_path = match event.sound {
            SoundType::Jump => JUMP_SOUND,
            SoundType::Coin => COIN_SOUND,
            SoundType::Explosion => EXPLOSION_SOUND,
            SoundType::Footstep => "sounds/footstep.wav",
        };

        let sound_handle: Handle<AudioSource> = asset_server.load(sound_path);

        match event.position {
            Some(pos) => {
                // Spatial audio
                commands.spawn(SpatialAudioBundle {
                    source: sound_handle,
                    settings: PlaybackSettings::DESPAWN,
                    spatial: SpatialSettings {
                        gain: 1.0,
                        ..default()
                    },
                    transform: Transform::from_translation(pos),
                });
            }
            None => {
                // 2D audio (UI sounds)
                commands.spawn(AudioBundle {
                    source: sound_handle,
                    settings: PlaybackSettings {
                        volume: audio_state.sfx_volume,
                        ..default()
                    },
                });
            }
        }
    }
}

fn handle_music_events(
    mut commands: Commands,
    mut music_events: EventReader<ChangeMusicEvent>,
    asset_server: Res<AssetServer>,
) {
    for event in music_events.read() {
        let music_path = match event.music {
            MusicType::MainMenu => "music/menu.ogg",
            MusicType::Gameplay => BACKGROUND_MUSIC,
            MusicType::Victory => "music/victory.ogg",
            MusicType::BossBattle => "music/boss.ogg",
        };

        commands.spawn((
            AudioBundle {
                source: asset_server.load(music_path),
                settings: PlaybackSettings::LOOP,
            },
            MusicController,
        ));
    }
}

fn handle_keyboard_input(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut sound_events: EventWriter<PlaySoundEvent>,
    mut music_events: EventWriter<ChangeMusicEvent>,
    mut audio_state: ResMut<AudioState>,
) {
    // Sound effects
    if keyboard_input.just_pressed(KeyCode::Space) {
        sound_events.send(PlaySoundEvent {
            sound: SoundType::Jump,
            position: None,
        });
    }

    if keyboard_input.just_pressed(KeyCode::KeyC) {
        sound_events.send(PlaySoundEvent {
            sound: SoundType::Coin,
            position: None,
        });
    }

    if keyboard_input.just_pressed(KeyCode::KeyE) {
        sound_events.send(PlaySoundEvent {
            sound: SoundType::Explosion,
            position: None,
        });
    }

    // Music control
    if keyboard_input.just_pressed(KeyCode::KeyM) {
        music_events.send(ChangeMusicEvent {
            music: MusicType::Gameplay,
            fade_duration: Some(2.0),
        });
    }

    // Volume control
    if keyboard_input.pressed(KeyCode::ArrowUp) {
        audio_state.music_volume = (audio_state.music_volume + 0.02).min(1.0);
    }
    if keyboard_input.pressed(KeyCode::ArrowDown) {
        audio_state.music_volume = (audio_state.music_volume - 0.02).max(0.0);
    }
    if keyboard_input.pressed(KeyCode::ArrowRight) {
        audio_state.sfx_volume = (audio_state.sfx_volume + 0.02).min(1.0);
    }
    if keyboard_input.pressed(KeyCode::ArrowLeft) {
        audio_state.sfx_volume = (audio_state.sfx_volume - 0.02).max(0.0);
    }

    // Mute toggle
    if keyboard_input.just_pressed(KeyCode::KeyP) {
        audio_state.is_muted = !audio_state.is_muted;
    }
}

fn update_audio_state(
    audio_state: Res<AudioState>,
    mut music_controllers: Query<&mut AudioSink, With<MusicController>>,
    audio_sinks: Res<Assets<AudioSink>>,
) {
    // Update music volume
    for mut sink in music_controllers.iter_mut() {
        if let Some(sink) = audio_sinks.get(&sink) {
            sink.set_volume(audio_state.music_volume);
        }
    }
}

fn spawn_sound_effects(
    mut commands: Commands,
    audio_sources: Query<(&Transform, &mut AudioSource, &SpatialSound)>,
    time: Res<Time>,
    mut sound_events: EventWriter<PlaySoundEvent>,
) {
    for (transform, mut audio_source, spatial) in audio_sources.iter_mut() {
        audio_source.cooldown.tick(time.delta());

        // Randomly emit sounds from objects
        if audio_source.cooldown.finished() {
            if fastrand::f32() < 0.01 { // 1% chance per frame
                sound_events.send(PlaySoundEvent {
                    sound: audio_source.sound_type,
                    position: Some(transform.translation),
                });
                audio_source.cooldown.reset();
            }
        }
    }
}