feat: re-implement pausing mechanism with tick-perfect audio & state pauses

This commit is contained in:
Ryan Walters
2025-09-11 00:03:14 -05:00
parent 8b2d18b3da
commit 08c964c32e
4 changed files with 102 additions and 38 deletions

View File

@@ -191,6 +191,20 @@ impl Audio {
} }
} }
/// Pauses all currently playing audio channels.
pub fn pause_all(&mut self) {
if !self.disabled {
mixer::Channel::all().pause();
}
}
/// Resumes all currently playing audio channels.
pub fn resume_all(&mut self) {
if !self.disabled {
mixer::Channel::all().resume();
}
}
/// Instantly mutes or unmutes all audio channels by adjusting their volume. /// Instantly mutes or unmutes all audio channels by adjusting their volume.
/// ///
/// Sets all 4 mixer channels to zero volume when muting, or restores them to /// Sets all 4 mixer channels to zero volume when muting, or restores them to

View File

@@ -19,8 +19,8 @@ use crate::systems::{
BackbufferResource, Blinking, BufferedDirection, Collider, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation, BackbufferResource, Blinking, BufferedDirection, Collider, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation,
EntityType, Frozen, FruitSprites, GameStage, Ghost, GhostAnimation, GhostAnimations, GhostBundle, GhostCollider, GhostState, EntityType, Frozen, FruitSprites, GameStage, Ghost, GhostAnimation, GhostAnimations, GhostBundle, GhostCollider, GhostState,
GlobalState, ItemBundle, ItemCollider, LastAnimationState, LinearAnimation, MapTextureResource, MovementModifiers, NodeId, GlobalState, ItemBundle, ItemCollider, LastAnimationState, LinearAnimation, MapTextureResource, MovementModifiers, NodeId,
PacmanCollider, PlayerAnimation, PlayerBundle, PlayerControlled, PlayerDeathAnimation, PlayerLives, Position, RenderDirty, PacmanCollider, Paused, PlayerAnimation, PlayerBundle, PlayerControlled, PlayerDeathAnimation, PlayerLives, Position,
Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, Timing, TouchState, Velocity, Visibility, RenderDirty, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, Timing, TouchState, Velocity, Visibility,
}; };
use crate::texture::animated::{DirectionalTiles, TileSequence}; use crate::texture::animated::{DirectionalTiles, TileSequence};
@@ -443,6 +443,7 @@ impl Game {
world.insert_resource(GameStage::Starting(StartupSequence::TextOnly { world.insert_resource(GameStage::Starting(StartupSequence::TextOnly {
remaining_ticks: constants::startup::STARTUP_FRAMES, remaining_ticks: constants::startup::STARTUP_FRAMES,
})); }));
world.insert_resource(Paused(false));
world.insert_non_send_resource(event_pump); world.insert_non_send_resource(event_pump);
world.insert_non_send_resource::<&mut Canvas<Window>>(Box::leak(Box::new(canvas))); world.insert_non_send_resource::<&mut Canvas<Window>>(Box::leak(Box::new(canvas)));
@@ -457,6 +458,7 @@ impl Game {
fn configure_schedule(schedule: &mut Schedule) { fn configure_schedule(schedule: &mut Schedule) {
let stage_system = profile(SystemId::Stage, systems::stage_system); let stage_system = profile(SystemId::Stage, systems::stage_system);
let input_system = profile(SystemId::Input, systems::input::input_system); let input_system = profile(SystemId::Input, systems::input::input_system);
let pause_system = profile(SystemId::Input, systems::handle_pause_command);
let player_control_system = profile(SystemId::PlayerControls, systems::player_control_system); let player_control_system = profile(SystemId::PlayerControls, systems::player_control_system);
let player_movement_system = profile(SystemId::PlayerMovement, systems::player_movement_system); let player_movement_system = profile(SystemId::PlayerMovement, systems::player_movement_system);
let player_tunnel_slowdown_system = profile(SystemId::PlayerMovement, systems::player::player_tunnel_slowdown_system); let player_tunnel_slowdown_system = profile(SystemId::PlayerMovement, systems::player::player_tunnel_slowdown_system);
@@ -483,6 +485,7 @@ impl Game {
*local % 2 == 0 *local % 2 == 0
}), }),
player_control_system, player_control_system,
pause_system,
) )
.chain(); .chain();
@@ -525,9 +528,9 @@ impl Game {
)) ))
.configure_sets(( .configure_sets((
GameplaySet::Input, GameplaySet::Input,
GameplaySet::Update, GameplaySet::Update.run_if(|paused: Res<Paused>| !paused.0),
GameplaySet::Respond, GameplaySet::Respond.run_if(|paused: Res<Paused>| !paused.0),
RenderSet::Animation, RenderSet::Animation.run_if(|paused: Res<Paused>| !paused.0),
RenderSet::Draw, RenderSet::Draw,
RenderSet::Present, RenderSet::Present,
)); ));

View File

@@ -31,6 +31,10 @@ pub enum AudioEvent {
PlayDeath, PlayDeath,
/// Stop all currently playing sounds /// Stop all currently playing sounds
StopAll, StopAll,
/// Pause all sounds
Pause,
/// Resume all sounds
Resume,
} }
/// Non-send resource wrapper for SDL2 audio system /// Non-send resource wrapper for SDL2 audio system
@@ -92,6 +96,22 @@ pub fn audio_system(
debug!("Audio disabled, ignoring stop all request"); debug!("Audio disabled, ignoring stop all request");
} }
} }
AudioEvent::Pause => {
if !audio.0.is_disabled() {
debug!("Pausing all audio");
audio.0.pause_all();
} else {
debug!("Audio disabled, ignoring pause all request");
}
}
AudioEvent::Resume => {
if !audio.0.is_disabled() {
debug!("Resuming all audio");
audio.0.resume_all();
} else {
debug!("Audio disabled, ignoring resume all request");
}
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
use std::mem::discriminant; use std::mem::discriminant;
use tracing::{debug, info, warn}; use tracing::{debug, info};
use crate::constants; use crate::constants;
use crate::events::StageTransition; use crate::events::StageTransition;
@@ -20,6 +20,8 @@ use bevy_ecs::{
system::{Commands, Query, Res, ResMut, Single}, system::{Commands, Query, Res, ResMut, Single},
}; };
use crate::events::{GameCommand, GameEvent};
#[derive(Resource, Clone)] #[derive(Resource, Clone)]
pub struct PlayerAnimation(pub DirectionalAnimation); pub struct PlayerAnimation(pub DirectionalAnimation);
@@ -45,6 +47,28 @@ pub enum GameStage {
GameOver, GameOver,
} }
#[derive(Resource, Debug, Default)]
pub struct Paused(pub bool);
pub fn handle_pause_command(
mut events: EventReader<GameEvent>,
mut paused: ResMut<Paused>,
mut audio_events: EventWriter<AudioEvent>,
) {
for event in events.read() {
if let GameEvent::Command(GameCommand::TogglePause) = event {
paused.0 = !paused.0;
if paused.0 {
info!("Game paused");
audio_events.write(AudioEvent::Pause);
} else {
info!("Game resumed");
audio_events.write(AudioEvent::Resume);
}
}
}
}
pub trait TooSimilar { pub trait TooSimilar {
fn too_similar(&self, other: &Self) -> bool; fn too_similar(&self, other: &Self) -> bool;
} }
@@ -161,7 +185,7 @@ pub fn stage_system(
mut ghost_query: Query<(Entity, &Ghost, &mut Position, &mut GhostState), (With<GhostCollider>, Without<PlayerControlled>)>, mut ghost_query: Query<(Entity, &Ghost, &mut Position, &mut GhostState), (With<GhostCollider>, Without<PlayerControlled>)>,
) { ) {
let old_state = *game_state; let old_state = *game_state;
let mut new_state: Option<GameStage> = None; let mut new_state_opt: Option<GameStage> = None;
// Handle stage transition requests before normal ticking // Handle stage transition requests before normal ticking
for event in stage_event_reader.read() { for event in stage_event_reader.read() {
@@ -172,7 +196,7 @@ pub fn stage_system(
let pac_node = player.1.current_node(); let pac_node = player.1.current_node();
debug!(ghost = ?ghost_type, node = pac_node, "Ghost eaten, entering pause state"); debug!(ghost = ?ghost_type, node = pac_node, "Ghost eaten, entering pause state");
new_state = Some(GameStage::GhostEatenPause { new_state_opt = Some(GameStage::GhostEatenPause {
remaining_ticks: 30, remaining_ticks: 30,
ghost_entity, ghost_entity,
ghost_type, ghost_type,
@@ -180,29 +204,11 @@ pub fn stage_system(
}); });
} }
let new_state: GameStage = match new_state.unwrap_or(*game_state) { let new_state: GameStage = new_state_opt.unwrap_or_else(|| match *game_state {
GameStage::Starting(startup) => match startup { GameStage::Playing => {
StartupSequence::TextOnly { remaining_ticks } => { // This is the default state, do nothing
if remaining_ticks > 0 { *game_state
GameStage::Starting(StartupSequence::TextOnly { }
remaining_ticks: remaining_ticks - 1,
})
} else {
GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 })
}
}
StartupSequence::CharactersVisible { remaining_ticks } => {
if remaining_ticks > 0 {
GameStage::Starting(StartupSequence::CharactersVisible {
remaining_ticks: remaining_ticks - 1,
})
} else {
info!("Startup sequence completed, beginning gameplay");
GameStage::Playing
}
}
},
GameStage::Playing => GameStage::Playing,
GameStage::GhostEatenPause { GameStage::GhostEatenPause {
remaining_ticks, remaining_ticks,
ghost_entity, ghost_entity,
@@ -221,11 +227,32 @@ pub fn stage_system(
GameStage::Playing GameStage::Playing
} }
} }
GameStage::PlayerDying(dying) => match dying { GameStage::Starting(sequence) => match sequence {
StartupSequence::TextOnly { remaining_ticks } => {
if remaining_ticks > 0 {
GameStage::Starting(StartupSequence::TextOnly {
remaining_ticks: remaining_ticks.saturating_sub(1),
})
} else {
GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 })
}
}
StartupSequence::CharactersVisible { remaining_ticks } => {
if remaining_ticks > 0 {
GameStage::Starting(StartupSequence::CharactersVisible {
remaining_ticks: remaining_ticks.saturating_sub(1),
})
} else {
info!("Startup sequence completed, beginning gameplay");
GameStage::Playing
}
}
},
GameStage::PlayerDying(sequence) => match sequence {
DyingSequence::Frozen { remaining_ticks } => { DyingSequence::Frozen { remaining_ticks } => {
if remaining_ticks > 0 { if remaining_ticks > 0 {
GameStage::PlayerDying(DyingSequence::Frozen { GameStage::PlayerDying(DyingSequence::Frozen {
remaining_ticks: remaining_ticks - 1, remaining_ticks: remaining_ticks.saturating_sub(1),
}) })
} else { } else {
let death_animation = &player_death_animation.0; let death_animation = &player_death_animation.0;
@@ -237,7 +264,7 @@ pub fn stage_system(
DyingSequence::Animating { remaining_ticks } => { DyingSequence::Animating { remaining_ticks } => {
if remaining_ticks > 0 { if remaining_ticks > 0 {
GameStage::PlayerDying(DyingSequence::Animating { GameStage::PlayerDying(DyingSequence::Animating {
remaining_ticks: remaining_ticks - 1, remaining_ticks: remaining_ticks.saturating_sub(1),
}) })
} else { } else {
GameStage::PlayerDying(DyingSequence::Hidden { remaining_ticks: 60 }) GameStage::PlayerDying(DyingSequence::Hidden { remaining_ticks: 60 })
@@ -246,7 +273,7 @@ pub fn stage_system(
DyingSequence::Hidden { remaining_ticks } => { DyingSequence::Hidden { remaining_ticks } => {
if remaining_ticks > 0 { if remaining_ticks > 0 {
GameStage::PlayerDying(DyingSequence::Hidden { GameStage::PlayerDying(DyingSequence::Hidden {
remaining_ticks: remaining_ticks - 1, remaining_ticks: remaining_ticks.saturating_sub(1),
}) })
} else { } else {
player_lives.0 = player_lives.0.saturating_sub(1); player_lives.0 = player_lives.0.saturating_sub(1);
@@ -255,14 +282,14 @@ pub fn stage_system(
info!(remaining_lives = player_lives.0, "Player died, returning to startup sequence"); info!(remaining_lives = player_lives.0, "Player died, returning to startup sequence");
GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 }) GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 })
} else { } else {
warn!("All lives lost, game over"); info!("All lives lost, game over");
GameStage::GameOver GameStage::GameOver
} }
} }
} }
}, },
GameStage::GameOver => GameStage::GameOver, GameStage::GameOver => *game_state,
}; });
if old_state == new_state { if old_state == new_state {
return; return;