mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-07 09:15:49 -06:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0a15c1ca8 | |||
| fa12611c69 |
@@ -10,10 +10,11 @@ use crate::map::builder::Map;
|
|||||||
use crate::systems::blinking::Blinking;
|
use crate::systems::blinking::Blinking;
|
||||||
use crate::systems::movement::{Movable, MovementState, Position};
|
use crate::systems::movement::{Movable, MovementState, Position};
|
||||||
use crate::systems::{
|
use crate::systems::{
|
||||||
|
audio::{audio_system, AudioEvent, AudioResource},
|
||||||
blinking::blinking_system,
|
blinking::blinking_system,
|
||||||
collision::collision_system,
|
collision::collision_system,
|
||||||
components::{
|
components::{
|
||||||
Collider, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, PacmanCollider,
|
AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, PacmanCollider,
|
||||||
PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource,
|
PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource,
|
||||||
},
|
},
|
||||||
control::player_system,
|
control::player_system,
|
||||||
@@ -70,6 +71,7 @@ impl Game {
|
|||||||
|
|
||||||
EventRegistry::register_event::<GameError>(&mut world);
|
EventRegistry::register_event::<GameError>(&mut world);
|
||||||
EventRegistry::register_event::<GameEvent>(&mut world);
|
EventRegistry::register_event::<GameEvent>(&mut world);
|
||||||
|
EventRegistry::register_event::<AudioEvent>(&mut world);
|
||||||
|
|
||||||
let mut backbuffer = texture_creator
|
let mut backbuffer = texture_creator
|
||||||
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
|
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
|
||||||
@@ -88,6 +90,9 @@ impl Game {
|
|||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
debug_texture.set_scale_mode(ScaleMode::Nearest);
|
debug_texture.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
|
// Initialize audio system
|
||||||
|
let audio = crate::audio::Audio::new();
|
||||||
|
|
||||||
// Load atlas and create map texture
|
// Load atlas and create map texture
|
||||||
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
||||||
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
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(BackbufferResource(backbuffer));
|
||||||
world.insert_non_send_resource(MapTextureResource(map_texture));
|
world.insert_non_send_resource(MapTextureResource(map_texture));
|
||||||
world.insert_non_send_resource(DebugTextureResource(debug_texture));
|
world.insert_non_send_resource(DebugTextureResource(debug_texture));
|
||||||
|
world.insert_non_send_resource(AudioResource(audio));
|
||||||
|
|
||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
@@ -193,6 +199,7 @@ impl Game {
|
|||||||
world.insert_resource(DeltaTime(0f32));
|
world.insert_resource(DeltaTime(0f32));
|
||||||
world.insert_resource(RenderDirty::default());
|
world.insert_resource(RenderDirty::default());
|
||||||
world.insert_resource(DebugState::default());
|
world.insert_resource(DebugState::default());
|
||||||
|
world.insert_resource(AudioState::default());
|
||||||
|
|
||||||
world.add_observer(
|
world.add_observer(
|
||||||
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
|
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
|
||||||
@@ -208,6 +215,7 @@ impl Game {
|
|||||||
profile("movement", movement_system),
|
profile("movement", movement_system),
|
||||||
profile("collision", collision_system),
|
profile("collision", collision_system),
|
||||||
profile("item", item_system),
|
profile("item", item_system),
|
||||||
|
profile("audio", audio_system),
|
||||||
profile("blinking", blinking_system),
|
profile("blinking", blinking_system),
|
||||||
profile("directional_render", directional_render_system),
|
profile("directional_render", directional_render_system),
|
||||||
profile("dirty_render", dirty_render_system),
|
profile("dirty_render", dirty_render_system),
|
||||||
|
|||||||
54
src/systems/audio.rs
Normal file
54
src/systems/audio.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,3 +107,12 @@ pub struct DeltaTime(pub f32);
|
|||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct RenderDirty(pub bool);
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use bevy_ecs::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
events::{GameCommand, GameEvent},
|
events::{GameCommand, GameEvent},
|
||||||
systems::components::{GlobalState, PlayerControlled},
|
systems::components::{AudioState, GlobalState, PlayerControlled},
|
||||||
systems::debug::DebugState,
|
systems::debug::DebugState,
|
||||||
systems::movement::Movable,
|
systems::movement::Movable,
|
||||||
};
|
};
|
||||||
@@ -18,6 +18,7 @@ pub fn player_system(
|
|||||||
mut events: EventReader<GameEvent>,
|
mut events: EventReader<GameEvent>,
|
||||||
mut state: ResMut<GlobalState>,
|
mut state: ResMut<GlobalState>,
|
||||||
mut debug_state: ResMut<DebugState>,
|
mut debug_state: ResMut<DebugState>,
|
||||||
|
mut audio_state: ResMut<AudioState>,
|
||||||
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
@@ -46,6 +47,10 @@ pub fn player_system(
|
|||||||
GameCommand::ToggleDebug => {
|
GameCommand::ToggleDebug => {
|
||||||
*debug_state = debug_state.next();
|
*debug_state = debug_state.next();
|
||||||
}
|
}
|
||||||
|
GameCommand::MuteAudio => {
|
||||||
|
audio_state.muted = !audio_state.muted;
|
||||||
|
tracing::info!("Audio {}", if audio_state.muted { "muted" } else { "unmuted" });
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use bevy_ecs::{event::EventReader, prelude::*, query::With, system::Query};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::GameEvent,
|
events::GameEvent,
|
||||||
systems::components::{EntityType, ItemCollider, PacmanCollider, ScoreResource},
|
systems::{
|
||||||
|
audio::AudioEvent,
|
||||||
|
components::{EntityType, ItemCollider, PacmanCollider, ScoreResource},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn item_system(
|
pub fn item_system(
|
||||||
@@ -11,6 +14,7 @@ pub fn item_system(
|
|||||||
mut score: ResMut<ScoreResource>,
|
mut score: ResMut<ScoreResource>,
|
||||||
pacman_query: Query<Entity, With<PacmanCollider>>,
|
pacman_query: Query<Entity, With<PacmanCollider>>,
|
||||||
item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
|
item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
|
||||||
|
mut events: EventWriter<AudioEvent>,
|
||||||
) {
|
) {
|
||||||
for event in collision_events.read() {
|
for event in collision_events.read() {
|
||||||
if let GameEvent::Collision(entity1, entity2) = event {
|
if let GameEvent::Collision(entity1, entity2) = event {
|
||||||
@@ -37,6 +41,8 @@ pub fn item_system(
|
|||||||
|
|
||||||
// Remove the collected item
|
// Remove the collected item
|
||||||
commands.entity(item_ent).despawn();
|
commands.entity(item_ent).despawn();
|
||||||
|
|
||||||
|
events.write(AudioEvent::PlayEat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//! This module contains all the ECS-related logic, including components, systems,
|
//! This module contains all the ECS-related logic, including components, systems,
|
||||||
//! and resources.
|
//! and resources.
|
||||||
|
|
||||||
|
pub mod audio;
|
||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
|||||||
use thousands::Separable;
|
use thousands::Separable;
|
||||||
|
|
||||||
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
|
/// 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.
|
/// The number of durations to keep in the circular buffer.
|
||||||
const TIMING_WINDOW_SIZE: usize = 30;
|
const TIMING_WINDOW_SIZE: usize = 30;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user