mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 01:15:42 -06:00
feat: implement pause state management and single tick command
This commit is contained in:
3
Justfile
3
Justfile
@@ -44,4 +44,5 @@ fix:
|
||||
cargo fmt --all
|
||||
|
||||
push:
|
||||
git push origin --tags && git push
|
||||
git push origin --tags;
|
||||
git push
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{map::direction::Direction, systems::Ghost};
|
||||
///
|
||||
/// Commands are generated by the input system in response to keyboard events
|
||||
/// and processed by appropriate game systems to modify state or behavior.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GameCommand {
|
||||
/// Request immediate game shutdown
|
||||
Exit,
|
||||
@@ -24,6 +24,7 @@ pub enum GameCommand {
|
||||
/// Toggle fullscreen mode (desktop only)
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
ToggleFullscreen,
|
||||
SingleTick,
|
||||
}
|
||||
|
||||
/// Global events that flow through the ECS event system to coordinate game behavior.
|
||||
|
||||
16
src/game.rs
16
src/game.rs
@@ -12,7 +12,7 @@ use crate::events::{CollisionTrigger, GameEvent, StageTransition};
|
||||
use crate::map::builder::Map;
|
||||
use crate::map::direction::Direction;
|
||||
use crate::systems::item::PelletCount;
|
||||
use crate::systems::state::IntroPlayed;
|
||||
use crate::systems::state::{IntroPlayed, PauseState};
|
||||
use crate::systems::{
|
||||
self, audio_system, blinking_system, collision_system, combined_render_system, directional_render_system,
|
||||
dirty_render_system, eaten_ghost_system, fruit_sprite_system, ghost_collision_observer, ghost_movement_system,
|
||||
@@ -21,8 +21,8 @@ use crate::systems::{
|
||||
BackbufferResource, Blinking, BufferedDirection, Collider, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation,
|
||||
EntityType, Frozen, FruitSprites, GameStage, Ghost, GhostAnimation, GhostAnimations, GhostBundle, GhostCollider, GhostState,
|
||||
GlobalState, ItemBundle, ItemCollider, LastAnimationState, LinearAnimation, MapTextureResource, MovementModifiers, NodeId,
|
||||
PacmanCollider, Paused, PlayerAnimation, PlayerBundle, PlayerControlled, PlayerDeathAnimation, PlayerLives, Position,
|
||||
RenderDirty, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, Timing, TouchState, Velocity, Visibility,
|
||||
PacmanCollider, PlayerAnimation, PlayerBundle, PlayerControlled, PlayerDeathAnimation, PlayerLives, Position, RenderDirty,
|
||||
Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, Timing, TouchState, Velocity, Visibility,
|
||||
};
|
||||
|
||||
use crate::texture::animated::{DirectionalTiles, TileSequence};
|
||||
@@ -446,7 +446,7 @@ impl Game {
|
||||
world.insert_resource(GameStage::Starting(StartupSequence::TextOnly {
|
||||
remaining_ticks: constants::startup::STARTUP_FRAMES,
|
||||
}));
|
||||
world.insert_resource(Paused(false));
|
||||
world.insert_resource(PauseState::default());
|
||||
|
||||
world.insert_non_send_resource(event_pump);
|
||||
world.insert_non_send_resource::<&mut Canvas<Window>>(Box::leak(Box::new(canvas)));
|
||||
@@ -479,6 +479,7 @@ impl Game {
|
||||
let unified_ghost_state_system = profile(SystemId::GhostStateAnimation, ghost_state_system);
|
||||
let eaten_ghost_system = profile(SystemId::EatenGhost, eaten_ghost_system);
|
||||
let time_to_live_system = profile(SystemId::TimeToLive, time_to_live_system);
|
||||
let manage_pause_state_system = profile(SystemId::PauseManager, systems::state::manage_pause_state_system);
|
||||
|
||||
// Input system should always run to prevent SDL event pump from blocking
|
||||
let input_systems = (
|
||||
@@ -530,12 +531,13 @@ impl Game {
|
||||
.chain()
|
||||
.in_set(RenderSet::Draw),
|
||||
(present_system, audio_system).chain().in_set(RenderSet::Present),
|
||||
manage_pause_state_system.after(GameplaySet::Update),
|
||||
))
|
||||
.configure_sets((
|
||||
GameplaySet::Input,
|
||||
GameplaySet::Update.run_if(|paused: Res<Paused>| !paused.0),
|
||||
GameplaySet::Respond.run_if(|paused: Res<Paused>| !paused.0),
|
||||
RenderSet::Animation.run_if(|paused: Res<Paused>| !paused.0),
|
||||
GameplaySet::Update.run_if(|paused: Res<PauseState>| paused.active()),
|
||||
GameplaySet::Respond.run_if(|paused: Res<PauseState>| paused.active()),
|
||||
RenderSet::Animation.run_if(|paused: Res<PauseState>| paused.active()),
|
||||
RenderSet::Draw,
|
||||
RenderSet::Present,
|
||||
));
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
//! main-thread requirements while maintaining Bevy ECS compatibility.
|
||||
|
||||
use bevy_ecs::{
|
||||
event::{Event, EventReader, EventWriter},
|
||||
event::{Event, EventReader},
|
||||
resource::Resource,
|
||||
system::{NonSendMut, ResMut},
|
||||
};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::{audio::Audio, audio::Sound, error::GameError};
|
||||
use crate::{audio::Audio, audio::Sound};
|
||||
|
||||
/// Resource for tracking audio state
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
@@ -46,44 +46,39 @@ pub enum AudioEvent {
|
||||
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>,
|
||||
) {
|
||||
pub fn audio_system(mut audio: NonSendMut<AudioResource>, mut state: ResMut<AudioState>, mut events: EventReader<AudioEvent>) {
|
||||
// Set mute state if it has changed
|
||||
if audio.0.is_muted() != audio_state.muted {
|
||||
debug!(muted = audio_state.muted, "Audio mute state changed");
|
||||
audio.0.set_mute(audio_state.muted);
|
||||
if audio.0.is_muted() != state.muted {
|
||||
debug!(muted = state.muted, "Audio mute state changed");
|
||||
audio.0.set_mute(state.muted);
|
||||
}
|
||||
|
||||
// Process audio events
|
||||
for event in audio_events.read() {
|
||||
for event in events.read() {
|
||||
match event {
|
||||
AudioEvent::Waka => {
|
||||
if !audio.0.is_disabled() && !audio_state.muted {
|
||||
trace!(sound_index = audio_state.sound_index, "Playing eat sound");
|
||||
if !audio.0.is_disabled() && !state.muted {
|
||||
trace!(sound_index = state.sound_index, "Playing eat sound");
|
||||
audio.0.waka();
|
||||
// Update the sound index for cycling through sounds
|
||||
audio_state.sound_index = (audio_state.sound_index + 1) % 4;
|
||||
state.sound_index = (state.sound_index + 1) % 4;
|
||||
// 4 eat sounds available
|
||||
} else {
|
||||
debug!(
|
||||
disabled = audio.0.is_disabled(),
|
||||
muted = audio_state.muted,
|
||||
muted = state.muted,
|
||||
"Skipping eat sound due to audio state"
|
||||
);
|
||||
}
|
||||
}
|
||||
AudioEvent::PlaySound(sound) => {
|
||||
if !audio.0.is_disabled() && !audio_state.muted {
|
||||
if !audio.0.is_disabled() && !state.muted {
|
||||
trace!(?sound, "Playing sound");
|
||||
audio.0.play(*sound);
|
||||
} else {
|
||||
debug!(
|
||||
disabled = audio.0.is_disabled(),
|
||||
muted = audio_state.muted,
|
||||
muted = state.muted,
|
||||
"Skipping sound due to audio state"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ impl Default for Bindings {
|
||||
key_bindings.insert(Keycode::Space, GameCommand::ToggleDebug);
|
||||
key_bindings.insert(Keycode::M, GameCommand::MuteAudio);
|
||||
key_bindings.insert(Keycode::R, GameCommand::ResetLevel);
|
||||
key_bindings.insert(Keycode::T, GameCommand::SingleTick);
|
||||
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
{
|
||||
|
||||
@@ -163,6 +163,7 @@ pub enum SystemId {
|
||||
GhostStateAnimation,
|
||||
EatenGhost,
|
||||
TimeToLive,
|
||||
PauseManager,
|
||||
}
|
||||
|
||||
impl Display for SystemId {
|
||||
|
||||
@@ -57,28 +57,103 @@ pub enum GameStage {
|
||||
GameOver,
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct Paused(pub bool);
|
||||
#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum PauseState {
|
||||
Inactive,
|
||||
Active { remaining_ticks: Option<u32> },
|
||||
}
|
||||
|
||||
impl Default for PauseState {
|
||||
fn default() -> Self {
|
||||
Self::Active { remaining_ticks: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl PauseState {
|
||||
pub fn active(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
PauseState::Active { remaining_ticks: None }
|
||||
| PauseState::Active {
|
||||
remaining_ticks: Some(1..=u32::MAX)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Ticks the pause state
|
||||
/// # Returns
|
||||
/// `true` if the state changed significantly (e.g. from active to inactive or vice versa)
|
||||
pub fn tick(&mut self) -> bool {
|
||||
match self {
|
||||
// Permanent states
|
||||
PauseState::Active { remaining_ticks: None } | PauseState::Inactive => false,
|
||||
// Last tick of the active state
|
||||
PauseState::Active {
|
||||
remaining_ticks: Some(1),
|
||||
} => {
|
||||
*self = PauseState::Inactive;
|
||||
true
|
||||
}
|
||||
// Active state with remaining ticks
|
||||
PauseState::Active {
|
||||
remaining_ticks: Some(ticks),
|
||||
} => {
|
||||
*self = PauseState::Active {
|
||||
remaining_ticks: Some(*ticks - 1),
|
||||
};
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_pause_command(
|
||||
mut events: EventReader<GameEvent>,
|
||||
mut paused: ResMut<Paused>,
|
||||
mut pause_state: ResMut<PauseState>,
|
||||
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");
|
||||
match event {
|
||||
GameEvent::Command(GameCommand::TogglePause) => {
|
||||
*pause_state = match *pause_state {
|
||||
PauseState::Active { .. } => {
|
||||
info!("Game resumed");
|
||||
audio_events.write(AudioEvent::Resume);
|
||||
PauseState::Inactive
|
||||
}
|
||||
PauseState::Inactive => {
|
||||
info!("Game paused");
|
||||
audio_events.write(AudioEvent::Pause);
|
||||
PauseState::Active { remaining_ticks: None }
|
||||
}
|
||||
}
|
||||
}
|
||||
GameEvent::Command(GameCommand::SingleTick) => {
|
||||
// Single tick should not function while the game is playing
|
||||
if matches!(*pause_state, PauseState::Active { remaining_ticks: None }) {
|
||||
return;
|
||||
}
|
||||
|
||||
*pause_state = PauseState::Active {
|
||||
remaining_ticks: Some(1),
|
||||
};
|
||||
audio_events.write(AudioEvent::Resume);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn manage_pause_state_system(mut pause_state: ResMut<PauseState>, mut audio_events: EventWriter<AudioEvent>) {
|
||||
let changed = pause_state.tick();
|
||||
|
||||
// If the pause state changed, send the appropriate audio event
|
||||
if changed {
|
||||
// Since the pause state can never go from inactive to active, the only way to get here is if the game is now paused...
|
||||
audio_events.write(AudioEvent::Pause);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
pub fn handle_fullscreen_command(mut events: EventReader<GameEvent>, mut canvas: NonSendMut<&mut Canvas<Window>>) {
|
||||
for event in events.read() {
|
||||
|
||||
Reference in New Issue
Block a user