Compare commits

...

2 Commits

Author SHA1 Message Date
Ryan Walters
1f5af2cd96 feat: touch movement controls 2025-09-04 11:02:51 -05:00
Ryan Walters
36a2f00d8c chore: set explicit ARGB8888 pixel format for transparency support, 'web' task with caddy fs 2025-09-04 00:13:48 -05:00
5 changed files with 180 additions and 8 deletions

View File

@@ -41,4 +41,4 @@ samply:
# Build the project for Emscripten
web:
bun run web.build.ts
bun run web.build.ts; caddy file-server --root dist

View File

@@ -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,
@@ -159,7 +159,7 @@ impl Game {
// 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)
.create_texture_target(Some(sdl2::pixels::PixelFormatEnum::ARGB8888), output_size.0, output_size.1)
.map_err(|e| GameError::Sdl(e.to_string()))?;
// Debug texture is copied over the backbuffer, it requires transparency abilities
@@ -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(),

View File

@@ -29,6 +29,7 @@ pub fn requires_console() -> bool {
false
}
#[allow(dead_code)]
pub fn get_canvas_size() -> Option<(u32, u32)> {
let mut width = 0.0;
let mut height = 0.0;

View File

@@ -25,6 +25,59 @@ pub enum CursorPosition {
},
}
#[derive(Resource, Default, Debug)]
pub struct TouchState {
pub active_touch: Option<TouchData>,
}
#[derive(Debug, Clone)]
pub struct TouchData {
pub finger_id: i64,
pub start_pos: Vec2,
pub current_pos: Vec2,
pub current_direction: Option<Direction>,
}
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<Direction> {
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<Keycode, GameCommand>,
@@ -131,6 +184,7 @@ pub fn input_system(
mut writer: EventWriter<GameEvent>,
mut pump: NonSendMut<EventPump>,
mut cursor: ResMut<CursorPosition>,
mut touch_state: ResMut<TouchState>,
) {
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 {

View File

@@ -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<BackbufferResource>,
mut canvas: NonSendMut<&mut Canvas<Window>>,
touch_state: Res<TouchState>,
mut errors: EventWriter<GameError>,
) {
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<BackbufferResource>,