Compare commits

...

2 Commits

8 changed files with 49 additions and 78 deletions

View File

@@ -6,7 +6,7 @@ use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::ttf::Sdl2TtfContext; use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::{Window, WindowContext}; use sdl2::video::{Window, WindowContext};
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem}; use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{debug, error, info, warn}; use tracing::{error, info, warn};
use crate::error::{GameError, GameResult}; use crate::error::{GameError, GameResult};
@@ -20,11 +20,11 @@ pub struct App {
game: Game, game: Game,
input_system: InputSystem, input_system: InputSystem,
canvas: Canvas<Window>, canvas: Canvas<Window>,
event_pump: &'static mut EventPump,
backbuffer: Texture<'static>, backbuffer: Texture<'static>,
paused: bool, event_pump: &'static mut EventPump,
focused: bool,
last_tick: Instant, last_tick: Instant,
focused: bool,
cursor_pos: Vec2, cursor_pos: Vec2,
} }
@@ -87,7 +87,6 @@ impl App {
canvas, canvas,
event_pump, event_pump,
backbuffer, backbuffer,
paused: false,
focused: true, focused: true,
last_tick: Instant::now(), last_tick: Instant::now(),
cursor_pos: Vec2::ZERO, cursor_pos: Vec2::ZERO,
@@ -105,17 +104,10 @@ impl App {
self.focused = true; self.focused = true;
} }
WindowEvent::FocusLost => { WindowEvent::FocusLost => {
debug!("Window focus lost");
self.focused = false; self.focused = false;
} }
_ => {} _ => {}
}, },
// It doesn't really make sense to have this available in the browser
#[cfg(not(target_os = "emscripten"))]
Event::Quit { .. } => {
info!("Exit requested. Exiting...");
return false;
}
Event::MouseMotion { x, y, .. } => { Event::MouseMotion { x, y, .. } => {
// Convert window coordinates to logical coordinates // Convert window coordinates to logical coordinates
self.cursor_pos = Vec2::new(x as f32, y as f32); self.cursor_pos = Vec2::new(x as f32, y as f32);
@@ -123,17 +115,12 @@ impl App {
_ => {} _ => {}
} }
let commands = self.input_system.handle_event(&event); if let Some(command) = self.input_system.handle_event(&event) {
for command in commands {
match command { match command {
GameCommand::Exit => { GameCommand::Exit => {
info!("Exit requested. Exiting..."); info!("Exit requested. Exiting...");
return false; return false;
} }
GameCommand::TogglePause => {
self.paused = !self.paused;
info!("{}", if self.paused { "Paused" } else { "Unpaused" });
}
_ => self.game.post_event(command.into()), _ => self.game.post_event(command.into()),
} }
} }
@@ -142,17 +129,20 @@ impl App {
let dt = self.last_tick.elapsed().as_secs_f32(); let dt = self.last_tick.elapsed().as_secs_f32();
self.last_tick = Instant::now(); self.last_tick = Instant::now();
if !self.paused { let exit = self.game.tick(dt);
self.game.tick(dt);
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) { if exit {
error!("Failed to draw game: {}", e); return false;
} }
if let Err(e) = self
.game if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
.present_backbuffer(&mut self.canvas, &self.backbuffer, self.cursor_pos) error!("Failed to draw game: {}", e);
{ }
error!("Failed to present backbuffer: {}", e); if let Err(e) = self
} .game
.present_backbuffer(&mut self.canvas, &self.backbuffer, self.cursor_pos)
{
error!("Failed to present backbuffer: {}", e);
} }
if start.elapsed() < LOOP_TIME { if start.elapsed() < LOOP_TIME {

View File

@@ -2,11 +2,11 @@ use crate::input::commands::GameCommand;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum GameEvent { pub enum GameEvent {
InputCommand(GameCommand), Command(GameCommand),
} }
impl From<GameCommand> for GameEvent { impl From<GameCommand> for GameEvent {
fn from(command: GameCommand) -> Self { fn from(command: GameCommand) -> Self {
GameEvent::InputCommand(command) GameEvent::Command(command)
} }
} }

View File

@@ -60,16 +60,17 @@ impl Game {
tracing::error!("Failed to reset game state: {}", e); tracing::error!("Failed to reset game state: {}", e);
} }
} }
GameCommand::Exit | GameCommand::TogglePause => { GameCommand::TogglePause => {
// These are handled in app.rs self.state.paused = !self.state.paused;
} }
GameCommand::Exit => {}
} }
} }
fn process_events(&mut self) { fn process_events(&mut self) {
while let Some(event) = self.state.event_queue.pop_front() { while let Some(event) = self.state.event_queue.pop_front() {
match event { match event {
GameEvent::InputCommand(command) => self.handle_command(command), GameEvent::Command(command) => self.handle_command(command),
} }
} }
} }
@@ -114,8 +115,18 @@ impl Game {
Ok(()) Ok(())
} }
pub fn tick(&mut self, dt: f32) { /// Ticks the game state.
///
/// Returns true if the game should exit.
pub fn tick(&mut self, dt: f32) -> bool {
// Process any events that have been posted (such as unpausing)
self.process_events(); self.process_events();
// If the game is paused, we don't need to do anything beyond returning
if self.state.paused {
return false;
}
self.state.pacman.tick(dt, &self.state.map.graph); self.state.pacman.tick(dt, &self.state.map.graph);
// Update all ghosts // Update all ghosts
@@ -128,6 +139,8 @@ impl Game {
// Check for collisions // Check for collisions
self.check_collisions(); self.check_collisions();
false
} }
/// Toggles the debug mode on and off. /// Toggles the debug mode on and off.

View File

@@ -35,6 +35,8 @@ include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
/// we can cleanly separate it from the game's logic, making it easier to manage /// we can cleanly separate it from the game's logic, making it easier to manage
/// and reason about. /// and reason about.
pub struct GameState { pub struct GameState {
pub paused: bool,
pub score: u32, pub score: u32,
pub map: Map, pub map: Map,
pub pacman: Pacman, pub pacman: Pacman,
@@ -128,6 +130,7 @@ impl GameState {
.collect(); .collect();
Ok(Self { Ok(Self {
paused: false,
map, map,
atlas, atlas,
pacman, pacman,

View File

@@ -36,13 +36,12 @@ impl InputSystem {
Self { key_bindings } Self { key_bindings }
} }
pub fn handle_event(&self, event: &Event) -> Vec<GameCommand> { /// Handles an event and returns a command if one is bound to the event.
let mut commands = Vec::new(); pub fn handle_event(&self, event: &Event) -> Option<GameCommand> {
if let Event::KeyDown { keycode: Some(key), .. } = event { match event {
if let Some(command) = self.key_bindings.get(key) { Event::Quit { .. } => Some(GameCommand::Exit),
commands.push(*command); Event::KeyDown { keycode: Some(key), .. } => self.key_bindings.get(key).copied(),
} _ => None,
} }
commands
} }
} }

View File

@@ -1,5 +1,5 @@
use pacman::constants::RAW_BOARD; use pacman::constants::RAW_BOARD;
use pacman::map::Map; use pacman::map::builder::Map;
mod collision; mod collision;
mod item; mod item;

View File

@@ -1,6 +1,6 @@
use glam::Vec2; use glam::Vec2;
use pacman::constants::{CELL_SIZE, RAW_BOARD}; use pacman::constants::{CELL_SIZE, RAW_BOARD};
use pacman::map::Map; use pacman::map::builder::Map;
use sdl2::render::Texture; use sdl2::render::Texture;
#[test] #[test]

View File

@@ -2,7 +2,6 @@ use pacman::entity::direction::Direction;
use pacman::entity::graph::{Graph, Node}; use pacman::entity::graph::{Graph, Node};
use pacman::entity::pacman::Pacman; use pacman::entity::pacman::Pacman;
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas}; use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
use sdl2::keyboard::Keycode;
use std::collections::HashMap; use std::collections::HashMap;
fn create_test_graph() -> Graph { fn create_test_graph() -> Graph {
@@ -72,36 +71,3 @@ fn test_pacman_creation() {
assert!(pacman.traverser.position.is_at_node()); assert!(pacman.traverser.position.is_at_node());
assert_eq!(pacman.traverser.direction, Direction::Left); assert_eq!(pacman.traverser.direction, Direction::Left);
} }
#[test]
fn test_pacman_key_handling() {
let graph = create_test_graph();
let atlas = create_test_atlas();
let mut pacman = Pacman::new(&graph, 0, &atlas).unwrap();
let test_cases = [
(Keycode::Up, Direction::Up),
(Keycode::Down, Direction::Down),
(Keycode::Left, Direction::Left),
(Keycode::Right, Direction::Right),
];
for (key, expected_direction) in test_cases {
pacman.handle_key(key);
assert!(pacman.traverser.next_direction.is_some() || pacman.traverser.direction == expected_direction);
}
}
#[test]
fn test_pacman_invalid_key() {
let graph = create_test_graph();
let atlas = create_test_atlas();
let mut pacman = Pacman::new(&graph, 0, &atlas).unwrap();
let original_direction = pacman.traverser.direction;
let original_next_direction = pacman.traverser.next_direction;
pacman.handle_key(Keycode::Space);
assert_eq!(pacman.traverser.direction, original_direction);
assert_eq!(pacman.traverser.next_direction, original_next_direction);
}