diff --git a/src/entity/graph.rs b/src/entity/graph.rs index c83d98c..c5416cd 100644 --- a/src/entity/graph.rs +++ b/src/entity/graph.rs @@ -223,6 +223,19 @@ impl Graph { self.nodes.len() } + /// Returns an iterator over all nodes in the graph. + pub fn nodes(&self) -> impl Iterator { + self.nodes.iter() + } + + /// Returns an iterator over all edges in the graph. + pub fn edges(&self) -> impl Iterator + '_ { + self.adjacency_list + .iter() + .enumerate() + .flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id, edge))) + } + /// Finds a specific edge from a source node to a target node. pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option { self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to) diff --git a/src/game/mod.rs b/src/game/mod.rs index ac1d83d..53e1aff 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -17,6 +17,7 @@ use crate::systems::{ PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, Score, ScoreResource, }, control::player_system, + debug::{debug_render_system, DebugState, DebugTextureResource}, input::input_system, movement::movement_system, profiling::{profile, SystemTimings}, @@ -24,7 +25,14 @@ use crate::systems::{ }; use crate::texture::animated::AnimatedTexture; use bevy_ecs::schedule::IntoScheduleConfigs; -use bevy_ecs::{event::EventRegistry, observer::Trigger, schedule::Schedule, system::ResMut, world::World}; +use bevy_ecs::system::NonSendMut; +use bevy_ecs::{ + event::EventRegistry, + observer::Trigger, + schedule::Schedule, + system::{Res, ResMut}, + world::World, +}; use sdl2::image::LoadTexture; use sdl2::render::{Canvas, ScaleMode, TextureCreator}; use sdl2::video::{Window, WindowContext}; @@ -72,6 +80,13 @@ impl Game { .map_err(|e| GameError::Sdl(e.to_string()))?; map_texture.set_scale_mode(ScaleMode::Nearest); + // Create debug texture at output resolution for crisp debug rendering + let output_size = canvas.output_size().unwrap(); + let mut debug_texture = texture_creator + .create_texture_target(None, output_size.0, output_size.1) + .map_err(|e| GameError::Sdl(e.to_string()))?; + debug_texture.set_scale_mode(ScaleMode::Nearest); + // 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| { @@ -157,7 +172,7 @@ impl Game { }, entity_type: EntityType::Player, collider: Collider { - size: constants::CELL_SIZE as f32 * 1.25, + size: constants::CELL_SIZE as f32 * 1.1, layer: CollisionLayer::PACMAN, }, pacman_collider: PacmanCollider, @@ -168,6 +183,7 @@ impl Game { world.insert_non_send_resource(canvas); 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_resource(map); world.insert_resource(GlobalState { exit: false }); @@ -176,6 +192,7 @@ impl Game { world.insert_resource(Bindings::default()); world.insert_resource(DeltaTime(0f32)); world.insert_resource(RenderDirty::default()); + world.insert_resource(DebugState::default()); world.add_observer( |event: Trigger, mut state: ResMut, _score: ResMut| match *event { @@ -188,7 +205,6 @@ impl Game { GameEvent::Collision(_a, _b) => {} }, ); - schedule.add_systems( ( profile("input", input_system), @@ -197,11 +213,29 @@ impl Game { profile("collision", collision_system), profile("blinking", blinking_system), profile("directional_render", directional_render_system), + profile("dirty_render", dirty_render_system), + profile("render", render_system), + profile("debug_render", debug_render_system), + profile( + "present", + |mut canvas: NonSendMut<&mut Canvas>, + backbuffer: NonSendMut, + debug_state: Res, + mut dirty: ResMut| { + if dirty.0 { + // Only copy backbuffer to main canvas if debug rendering is off + // (debug rendering draws directly to main canvas) + if *debug_state == DebugState::Off { + canvas.copy(&backbuffer.0, None, None).unwrap(); + } + dirty.0 = false; + canvas.present(); + } + }, + ), ) .chain(), ); - schedule.add_systems((profile("dirty_render", dirty_render_system), profile("render", render_system)).chain()); - // Spawn player world.spawn(player); diff --git a/src/systems/control.rs b/src/systems/control.rs index 35bfdd2..bc4ef1f 100644 --- a/src/systems/control.rs +++ b/src/systems/control.rs @@ -9,6 +9,7 @@ use crate::{ error::GameError, events::{GameCommand, GameEvent}, systems::components::{GlobalState, PlayerControlled}, + systems::debug::DebugState, systems::movement::Movable, }; @@ -16,6 +17,7 @@ use crate::{ pub fn player_system( mut events: EventReader, mut state: ResMut, + mut debug_state: ResMut, mut players: Query<&mut Movable, With>, mut errors: EventWriter, ) { @@ -38,6 +40,9 @@ pub fn player_system( GameCommand::Exit => { state.exit = true; } + GameCommand::ToggleDebug => { + *debug_state = debug_state.next(); + } _ => {} } } diff --git a/src/systems/debug.rs b/src/systems/debug.rs new file mode 100644 index 0000000..d4d291b --- /dev/null +++ b/src/systems/debug.rs @@ -0,0 +1,143 @@ +//! Debug rendering system +use crate::constants::BOARD_PIXEL_OFFSET; +use crate::map::builder::Map; +use crate::systems::components::Collider; +use crate::systems::movement::Position; +use crate::systems::render::BackbufferResource; +use bevy_ecs::prelude::*; +use sdl2::pixels::Color; +use sdl2::rect::Rect; +use sdl2::render::{Canvas, Texture}; +use sdl2::video::Window; + +#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)] +pub enum DebugState { + #[default] + Off, + Graph, + Collision, +} + +impl DebugState { + pub fn next(&self) -> Self { + match self { + DebugState::Off => DebugState::Graph, + DebugState::Graph => DebugState::Collision, + DebugState::Collision => DebugState::Off, + } + } +} + +/// Resource to hold the debug texture for persistent rendering +pub struct DebugTextureResource(pub Texture<'static>); + +/// Transforms a position from logical canvas coordinates to output canvas coordinates +fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) { + let scale_x = output_size.0 as f32 / logical_size.0 as f32; + let scale_y = output_size.1 as f32 / logical_size.1 as f32; + let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio + + let x = (pos.0 * scale) as i32; + let y = (pos.1 * scale) as i32; + (x, y) +} + +/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset) +fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) { + let scale_x = output_size.0 as f32 / logical_size.0 as f32; + let scale_y = output_size.1 as f32 / logical_size.1 as f32; + let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio + + let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32; + let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32; + (x, y) +} + +/// Transforms a size from logical canvas coordinates to output canvas coordinates +fn transform_size(size: f32, output_size: (u32, u32), logical_size: (u32, u32)) -> u32 { + let scale_x = output_size.0 as f32 / logical_size.0 as f32; + let scale_y = output_size.1 as f32 / logical_size.1 as f32; + let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio + + (size * scale) as u32 +} + +pub fn debug_render_system( + mut canvas: NonSendMut<&mut Canvas>, + backbuffer: NonSendMut, + mut debug_texture: NonSendMut, + debug_state: Res, + map: Res, + colliders: Query<(&Collider, &Position)>, +) { + if *debug_state == DebugState::Off { + return; + } + + // Get canvas sizes for coordinate transformation + let output_size = canvas.output_size().unwrap(); + let logical_size = canvas.logical_size(); + + // Copy the current backbuffer to the debug texture + canvas + .with_texture_canvas(&mut debug_texture.0, |debug_canvas| { + // Clear the debug canvas + debug_canvas.set_draw_color(Color::BLACK); + debug_canvas.clear(); + + // Copy the backbuffer to the debug canvas + debug_canvas.copy(&backbuffer.0, None, None).unwrap(); + }) + .unwrap(); + + // Draw debug info on the high-resolution debug texture + canvas + .with_texture_canvas(&mut debug_texture.0, |debug_canvas| match *debug_state { + DebugState::Graph => { + debug_canvas.set_draw_color(Color::RED); + for (start_node, end_node) in map.graph.edges() { + let start_node = map.graph.get_node(start_node).unwrap().position; + let end_node = map.graph.get_node(end_node.target).unwrap().position; + + // Transform positions using common method + let (start_x, start_y) = + transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size); + let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size); + + debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap(); + } + + debug_canvas.set_draw_color(Color::BLUE); + for node in map.graph.nodes() { + let pos = node.position; + + // Transform position using common method + let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size); + let size = transform_size(4.0, output_size, logical_size); + + debug_canvas + .fill_rect(Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size)) + .unwrap(); + } + } + DebugState::Collision => { + debug_canvas.set_draw_color(Color::GREEN); + for (collider, position) in colliders.iter() { + let pos = position.get_pixel_pos(&map.graph).unwrap(); + + // Transform position and size using common methods + let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size); + let size = transform_size(collider.size, output_size, logical_size); + + // Center the collision box on the entity + let rect = Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size); + debug_canvas.draw_rect(rect).unwrap(); + } + } + _ => {} + }) + .unwrap(); + + // Draw the debug texture directly onto the main canvas at full resolution + canvas.copy(&debug_texture.0, None, None).unwrap(); +} diff --git a/src/systems/mod.rs b/src/systems/mod.rs index abdbe9a..d1f59db 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -7,6 +7,7 @@ pub mod blinking; pub mod collision; pub mod components; pub mod control; +pub mod debug; pub mod input; pub mod movement; pub mod profiling; diff --git a/src/systems/render.rs b/src/systems/render.rs index f533acc..ac2bd6b 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -65,17 +65,13 @@ pub fn render_system( mut backbuffer: NonSendMut, mut atlas: NonSendMut, map: Res, - mut dirty: ResMut, + dirty: Res, renderables: Query<(Entity, &Renderable, &Position)>, mut errors: EventWriter, ) { if !dirty.0 { return; } - // Clear the main canvas first - canvas.set_draw_color(sdl2::pixels::Color::BLACK); - canvas.clear(); - // Render to backbuffer canvas .with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| { @@ -116,14 +112,4 @@ pub fn render_system( }) .err() .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); - - // Copy backbuffer to main canvas and present - canvas - .copy(&backbuffer.0, None, None) - .err() - .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); - - canvas.present(); - - dirty.0 = false; }