From 1f5af2cd96dcb59389a6139eecdbbf0054381b3d Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Thu, 4 Sep 2025 11:02:51 -0500 Subject: [PATCH] feat: touch movement controls --- src/game.rs | 14 ++++--- src/systems/input.rs | 95 +++++++++++++++++++++++++++++++++++++++++++ src/systems/render.rs | 74 +++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 6 deletions(-) diff --git a/src/game.rs b/src/game.rs index c0f1ed7..5a4d2f7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -13,6 +13,7 @@ use crate::systems::blinking::Blinking; use crate::systems::components::{GhostAnimation, GhostState, LastAnimationState}; use crate::systems::movement::{BufferedDirection, Position, Velocity}; use crate::systems::profiling::SystemId; +use crate::systems::render::touch_ui_render_system; use crate::systems::render::RenderDirty; use crate::systems::{ self, combined_render_system, ghost_collision_system, present_system, Hidden, LinearAnimation, MovementModifiers, NodeId, @@ -108,9 +109,9 @@ impl Game { EventType::ControllerTouchpadDown, EventType::ControllerTouchpadMotion, EventType::ControllerTouchpadUp, - EventType::FingerDown, - EventType::FingerUp, - EventType::FingerMotion, + // EventType::FingerDown, // Enable for touch controls + // EventType::FingerUp, // Enable for touch controls + // EventType::FingerMotion, // Enable for touch controls EventType::DollarGesture, EventType::DollarRecord, EventType::MultiGesture, @@ -130,9 +131,8 @@ impl Game { EventType::Window, EventType::MouseWheel, // EventType::MouseMotion, - EventType::MouseButtonDown, - EventType::MouseButtonUp, - EventType::MouseButtonDown, + // EventType::MouseButtonDown, // Enable for desktop touch testing + // EventType::MouseButtonUp, // Enable for desktop touch testing EventType::AppDidEnterBackground, EventType::AppWillEnterForeground, EventType::AppWillEnterBackground, @@ -317,6 +317,7 @@ impl Game { world.insert_resource(DebugState::default()); world.insert_resource(AudioState::default()); world.insert_resource(CursorPosition::default()); + world.insert_resource(systems::input::TouchState::default()); world.insert_resource(StartupSequence::new( constants::startup::STARTUP_FRAMES, constants::startup::STARTUP_TICKS_PER_FRAME, @@ -388,6 +389,7 @@ impl Game { dirty_render_system, combined_render_system, hud_render_system, + touch_ui_render_system, present_system, ) .chain(), diff --git a/src/systems/input.rs b/src/systems/input.rs index 8ee5010..bb89792 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -25,6 +25,59 @@ pub enum CursorPosition { }, } +#[derive(Resource, Default, Debug)] +pub struct TouchState { + pub active_touch: Option, +} + +#[derive(Debug, Clone)] +pub struct TouchData { + pub finger_id: i64, + pub start_pos: Vec2, + pub current_pos: Vec2, + pub current_direction: Option, +} + +impl TouchData { + pub fn new(finger_id: i64, start_pos: Vec2) -> Self { + Self { + finger_id, + start_pos, + current_pos: start_pos, + current_direction: None, + } + } + + pub fn update_position(&mut self, new_pos: Vec2) -> Option { + self.current_pos = new_pos; + let delta = new_pos - self.start_pos; + + // Minimum threshold for direction detection (in pixels) + const THRESHOLD: f32 = 20.0; + + if delta.length() < THRESHOLD { + self.current_direction = None; + return None; + } + + // Determine primary direction based on larger component + let direction = if delta.x.abs() > delta.y.abs() { + if delta.x > 0.0 { + Direction::Right + } else { + Direction::Left + } + } else if delta.y > 0.0 { + Direction::Down + } else { + Direction::Up + }; + + self.current_direction = Some(direction); + Some(direction) + } +} + #[derive(Resource, Debug, Clone)] pub struct Bindings { key_bindings: HashMap, @@ -131,6 +184,7 @@ pub fn input_system( mut writer: EventWriter, mut pump: NonSendMut, mut cursor: ResMut, + mut touch_state: ResMut, ) { let mut cursor_seen = false; // Collect all events for this frame. @@ -159,6 +213,47 @@ pub fn input_system( remaining_time: 0.20, }; cursor_seen = true; + + // Handle mouse motion as touch motion for desktop testing + if let Some(ref mut touch_data) = touch_state.active_touch { + if let Some(direction) = touch_data.update_position(Vec2::new(x as f32, y as f32)) { + writer.write(GameEvent::Command(GameCommand::MovePlayer(direction))); + } + } + } + // Handle mouse events as touch for desktop testing + Event::MouseButtonDown { x, y, .. } => { + let pos = Vec2::new(x as f32, y as f32); + touch_state.active_touch = Some(TouchData::new(0, pos)); // Use ID 0 for mouse + } + Event::MouseButtonUp { .. } => { + touch_state.active_touch = None; + } + // Handle actual touch events for mobile + Event::FingerDown { finger_id, x, y, .. } => { + // Convert normalized coordinates (0.0-1.0) to screen coordinates + let screen_x = x * crate::constants::CANVAS_SIZE.x as f32; + let screen_y = y * crate::constants::CANVAS_SIZE.y as f32; + let pos = Vec2::new(screen_x, screen_y); + touch_state.active_touch = Some(TouchData::new(finger_id, pos)); + } + Event::FingerMotion { finger_id, x, y, .. } => { + if let Some(ref mut touch_data) = touch_state.active_touch { + if touch_data.finger_id == finger_id { + let screen_x = x * crate::constants::CANVAS_SIZE.x as f32; + let screen_y = y * crate::constants::CANVAS_SIZE.y as f32; + if let Some(direction) = touch_data.update_position(Vec2::new(screen_x, screen_y)) { + writer.write(GameEvent::Command(GameCommand::MovePlayer(direction))); + } + } + } + } + Event::FingerUp { finger_id, .. } => { + if let Some(ref touch_data) = touch_state.active_touch { + if touch_data.finger_id == finger_id { + touch_state.active_touch = None; + } + } } Event::KeyDown { keycode, repeat, .. } => { if let Some(key) = keycode { diff --git a/src/systems/render.rs b/src/systems/render.rs index d3fa5ed..ac9d325 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -1,6 +1,7 @@ use crate::constants::CANVAS_SIZE; use crate::error::{GameError, TextureError}; use crate::map::builder::Map; +use crate::systems::input::TouchState; use crate::systems::{ debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation, LinearAnimation, Position, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, @@ -114,6 +115,79 @@ pub struct MapTextureResource(pub Texture); /// A non-send resource for the backbuffer texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource. pub struct BackbufferResource(pub Texture); +/// Renders touch UI overlay for mobile/testing. +pub fn touch_ui_render_system( + mut backbuffer: NonSendMut, + mut canvas: NonSendMut<&mut Canvas>, + touch_state: Res, + mut errors: EventWriter, +) { + if let Some(ref touch_data) = touch_state.active_touch { + let _ = canvas.with_texture_canvas(&mut backbuffer.0, |canvas| { + // Set blend mode for transparency + canvas.set_blend_mode(BlendMode::Blend); + + // Draw semi-transparent circle at touch start position + canvas.set_draw_color(Color::RGBA(255, 255, 255, 100)); + let center = Point::new(touch_data.start_pos.x as i32, touch_data.start_pos.y as i32); + + // Draw a simple circle by drawing filled rectangles (basic approach) + let radius = 30; + for dy in -radius..=radius { + for dx in -radius..=radius { + if dx * dx + dy * dy <= radius * radius { + let point = Point::new(center.x + dx, center.y + dy); + if let Err(e) = canvas.draw_point(point) { + errors.write(TextureError::RenderFailed(format!("Touch UI render error: {}", e)).into()); + return; + } + } + } + } + + // Draw direction indicator if we have a direction + if let Some(direction) = touch_data.current_direction { + canvas.set_draw_color(Color::RGBA(0, 255, 0, 150)); + + // Draw arrow indicating direction + let arrow_length = 40; + let (dx, dy) = match direction { + crate::map::direction::Direction::Up => (0, -arrow_length), + crate::map::direction::Direction::Down => (0, arrow_length), + crate::map::direction::Direction::Left => (-arrow_length, 0), + crate::map::direction::Direction::Right => (arrow_length, 0), + }; + + let end_point = Point::new(center.x + dx, center.y + dy); + if let Err(e) = canvas.draw_line(center, end_point) { + errors.write(TextureError::RenderFailed(format!("Touch arrow render error: {}", e)).into()); + } + + // Draw arrowhead (simple approach) + let arrow_size = 8; + match direction { + crate::map::direction::Direction::Up => { + let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y + arrow_size)); + let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y + arrow_size)); + } + crate::map::direction::Direction::Down => { + let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y - arrow_size)); + let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y - arrow_size)); + } + crate::map::direction::Direction::Left => { + let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y - arrow_size)); + let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y + arrow_size)); + } + crate::map::direction::Direction::Right => { + let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y - arrow_size)); + let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y + arrow_size)); + } + } + } + }); + } +} + /// Renders the HUD (score, lives, etc.) on top of the game. pub fn hud_render_system( mut backbuffer: NonSendMut,