diff --git a/src/game/mod.rs b/src/game/mod.rs index dd5e99b..3876384 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -10,10 +10,11 @@ use crate::map::builder::Map; use crate::systems::blinking::Blinking; use crate::systems::movement::{Movable, MovementState, Position}; use crate::systems::{ + audio::{audio_system, AudioEvent, AudioResource}, blinking::blinking_system, collision::collision_system, components::{ - Collider, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, PacmanCollider, + AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource, }, control::player_system, @@ -70,6 +71,7 @@ impl Game { EventRegistry::register_event::(&mut world); EventRegistry::register_event::(&mut world); + EventRegistry::register_event::(&mut world); let mut backbuffer = texture_creator .create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y) @@ -88,6 +90,9 @@ impl Game { .map_err(|e| GameError::Sdl(e.to_string()))?; debug_texture.set_scale_mode(ScaleMode::Nearest); + // Initialize audio system + let audio = crate::audio::Audio::new(); + // Load atlas and create map texture let atlas_bytes = get_asset_bytes(Asset::Atlas)?; let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| { @@ -184,6 +189,7 @@ impl Game { world.insert_non_send_resource(BackbufferResource(backbuffer)); world.insert_non_send_resource(MapTextureResource(map_texture)); world.insert_non_send_resource(DebugTextureResource(debug_texture)); + world.insert_non_send_resource(AudioResource(audio)); world.insert_resource(map); world.insert_resource(GlobalState { exit: false }); @@ -193,6 +199,7 @@ impl Game { world.insert_resource(DeltaTime(0f32)); world.insert_resource(RenderDirty::default()); world.insert_resource(DebugState::default()); + world.insert_resource(AudioState::default()); world.add_observer( |event: Trigger, mut state: ResMut, _score: ResMut| { @@ -208,6 +215,7 @@ impl Game { profile("movement", movement_system), profile("collision", collision_system), profile("item", item_system), + profile("audio", audio_system), profile("blinking", blinking_system), profile("directional_render", directional_render_system), profile("dirty_render", dirty_render_system), diff --git a/src/systems/audio.rs b/src/systems/audio.rs new file mode 100644 index 0000000..915cc83 --- /dev/null +++ b/src/systems/audio.rs @@ -0,0 +1,54 @@ +//! Audio system for handling sound playback in the Pac-Man game. +//! +//! This module provides an ECS-based audio system that integrates with SDL2_mixer +//! for playing sound effects. The system uses NonSendMut resources to handle SDL2's +//! main-thread requirements while maintaining Bevy ECS compatibility. + +use bevy_ecs::{ + event::{Event, EventReader, EventWriter}, + system::{NonSendMut, ResMut}, +}; + +use crate::{audio::Audio, error::GameError, systems::components::AudioState}; + +/// Events for triggering audio playback +#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +pub enum AudioEvent { + /// Play the "eat" sound when Pac-Man consumes a pellet + PlayEat, +} + +/// Non-send resource wrapper for SDL2 audio system +/// +/// This wrapper is needed because SDL2 audio components are not Send, +/// but Bevy ECS requires Send for regular resources. Using NonSendMut +/// allows us to use SDL2 audio on the main thread while integrating +/// with the ECS system. +pub struct AudioResource(pub Audio); + +/// System that processes audio events and plays sounds +pub fn audio_system( + mut audio: NonSendMut, + mut audio_state: ResMut, + mut audio_events: EventReader, + _errors: EventWriter, +) { + // Set mute state if it has changed + if audio.0.is_muted() != audio_state.muted { + audio.0.set_mute(audio_state.muted); + } + + // Process audio events + for event in audio_events.read() { + match event { + AudioEvent::PlayEat => { + if !audio.0.is_disabled() && !audio_state.muted { + audio.0.eat(); + // Update the sound index for cycling through sounds + audio_state.sound_index = (audio_state.sound_index + 1) % 4; + // 4 eat sounds available + } + } + } + } +} diff --git a/src/systems/components.rs b/src/systems/components.rs index e6705fc..f80d22d 100644 --- a/src/systems/components.rs +++ b/src/systems/components.rs @@ -107,3 +107,12 @@ pub struct DeltaTime(pub f32); #[derive(Resource, Default)] pub struct RenderDirty(pub bool); + +/// Resource for tracking audio state +#[derive(Resource, Debug, Clone, Default)] +pub struct AudioState { + /// Whether audio is currently muted + pub muted: bool, + /// Current sound index for cycling through eat sounds + pub sound_index: usize, +} diff --git a/src/systems/item.rs b/src/systems/item.rs index d2cd91a..9aff5de 100644 --- a/src/systems/item.rs +++ b/src/systems/item.rs @@ -2,7 +2,10 @@ use bevy_ecs::{event::EventReader, prelude::*, query::With, system::Query}; use crate::{ events::GameEvent, - systems::components::{EntityType, ItemCollider, PacmanCollider, ScoreResource}, + systems::{ + audio::AudioEvent, + components::{EntityType, ItemCollider, PacmanCollider, ScoreResource}, + }, }; pub fn item_system( @@ -11,6 +14,7 @@ pub fn item_system( mut score: ResMut, pacman_query: Query>, item_query: Query<(Entity, &EntityType), With>, + mut events: EventWriter, ) { for event in collision_events.read() { if let GameEvent::Collision(entity1, entity2) = event { @@ -37,6 +41,8 @@ pub fn item_system( // Remove the collected item commands.entity(item_ent).despawn(); + + events.write(AudioEvent::PlayEat); } } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index 87530f1..b065230 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -3,6 +3,7 @@ //! This module contains all the ECS-related logic, including components, systems, //! and resources. +pub mod audio; pub mod blinking; pub mod collision; pub mod components; diff --git a/src/systems/profiling.rs b/src/systems/profiling.rs index 5ddac4d..51f7f11 100644 --- a/src/systems/profiling.rs +++ b/src/systems/profiling.rs @@ -7,7 +7,7 @@ use std::time::Duration; use thousands::Separable; /// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic. -const MAX_SYSTEMS: usize = 11; +const MAX_SYSTEMS: usize = 12; /// The number of durations to keep in the circular buffer. const TIMING_WINDOW_SIZE: usize = 30;