Compare commits

...

2 Commits

Author SHA1 Message Date
e0a15c1ca8 feat: implement audio muting functionality 2025-08-15 20:30:41 -05:00
fa12611c69 feat: ecs audio system 2025-08-15 20:28:47 -05:00
7 changed files with 87 additions and 4 deletions

View File

@@ -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::<GameError>(&mut world);
EventRegistry::register_event::<GameEvent>(&mut world);
EventRegistry::register_event::<AudioEvent>(&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<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
@@ -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),

54
src/systems/audio.rs Normal file
View File

@@ -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<AudioResource>,
mut audio_state: ResMut<AudioState>,
mut audio_events: EventReader<AudioEvent>,
_errors: EventWriter<GameError>,
) {
// 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
}
}
}
}
}

View File

@@ -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,
}

View File

@@ -8,7 +8,7 @@ use bevy_ecs::{
use crate::{
error::GameError,
events::{GameCommand, GameEvent},
systems::components::{GlobalState, PlayerControlled},
systems::components::{AudioState, GlobalState, PlayerControlled},
systems::debug::DebugState,
systems::movement::Movable,
};
@@ -18,6 +18,7 @@ pub fn player_system(
mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>,
mut debug_state: ResMut<DebugState>,
mut audio_state: ResMut<AudioState>,
mut players: Query<&mut Movable, With<PlayerControlled>>,
mut errors: EventWriter<GameError>,
) {
@@ -46,6 +47,10 @@ pub fn player_system(
GameCommand::ToggleDebug => {
*debug_state = debug_state.next();
}
GameCommand::MuteAudio => {
audio_state.muted = !audio_state.muted;
tracing::info!("Audio {}", if audio_state.muted { "muted" } else { "unmuted" });
}
_ => {}
}
}

View File

@@ -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<ScoreResource>,
pacman_query: Query<Entity, With<PacmanCollider>>,
item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
mut events: EventWriter<AudioEvent>,
) {
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);
}
}
}

View File

@@ -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;

View File

@@ -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;