fix: background input thread for responsive non-blocking input
Problem: event::poll(0ms) once per frame missed events between frames, causing laggy/missed input. Terminal key repeat timing was unpredictable. Fix: dedicated input thread that blocks on event::read() and pushes all events to an mpsc channel. Game loop drains channel with try_iter() each frame — gets every event without blocking, zero missed inputs. - InputHandler::start() spawns background thread - InputHandler::stop() cleans up on game exit - try_iter() collects all accumulated events per frame - Removed 16ms sleep (no longer needed, input doesn't block) - Hold timeout reduced to 60ms (events arrive more reliably now) - 109 tests, 0 warnings, 0 failures
This commit is contained in:
+3
-2
@@ -170,6 +170,7 @@ impl Game {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.init_world();
|
self.init_world();
|
||||||
|
self.input.start();
|
||||||
|
|
||||||
self.last_time = Instant::now();
|
self.last_time = Instant::now();
|
||||||
|
|
||||||
@@ -196,10 +197,10 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.handle_input(vw, vh);
|
self.handle_input(vw, vh);
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(16));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.input.stop();
|
||||||
|
|
||||||
if let Err(e) = renderer.shutdown() {
|
if let Err(e) = renderer.shutdown() {
|
||||||
eprintln!("Renderer shutdown failed: {}", e);
|
eprintln!("Renderer shutdown failed: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-28
@@ -1,4 +1,6 @@
|
|||||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
@@ -42,15 +44,16 @@ pub enum HeldKey {
|
|||||||
|
|
||||||
struct HeldState {
|
struct HeldState {
|
||||||
last_seen: Instant,
|
last_seen: Instant,
|
||||||
just_pressed: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOLD_TIMEOUT: Duration = Duration::from_millis(80);
|
const HOLD_TIMEOUT: Duration = Duration::from_millis(60);
|
||||||
|
|
||||||
pub struct InputHandler {
|
pub struct InputHandler {
|
||||||
pub paint_brush: MaterialBrush,
|
pub paint_brush: MaterialBrush,
|
||||||
held: std::collections::HashMap<HeldKey, HeldState>,
|
held: std::collections::HashMap<HeldKey, HeldState>,
|
||||||
jump_pressed: bool,
|
jump_pressed: bool,
|
||||||
|
receiver: Option<mpsc::Receiver<Event>>,
|
||||||
|
input_thread: Option<thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputHandler {
|
impl InputHandler {
|
||||||
@@ -59,9 +62,34 @@ impl InputHandler {
|
|||||||
paint_brush: MaterialBrush::Sand,
|
paint_brush: MaterialBrush::Sand,
|
||||||
held: std::collections::HashMap::new(),
|
held: std::collections::HashMap::new(),
|
||||||
jump_pressed: false,
|
jump_pressed: false,
|
||||||
|
receiver: None,
|
||||||
|
input_thread: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
let (tx, rx) = mpsc::channel::<Event>();
|
||||||
|
self.receiver = Some(rx);
|
||||||
|
self.input_thread = Some(thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
match event::read() {
|
||||||
|
Ok(ev) => {
|
||||||
|
if tx.send(ev).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.receiver = None;
|
||||||
|
self.input_thread = None;
|
||||||
|
self.held.clear();
|
||||||
|
}
|
||||||
|
|
||||||
fn key_to_held(code: KeyCode) -> Option<HeldKey> {
|
fn key_to_held(code: KeyCode) -> Option<HeldKey> {
|
||||||
match code {
|
match code {
|
||||||
KeyCode::Left | KeyCode::Char('a') => Some(HeldKey::Left),
|
KeyCode::Left | KeyCode::Char('a') => Some(HeldKey::Left),
|
||||||
@@ -75,12 +103,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mark_held(&mut self, key: HeldKey) {
|
fn mark_held(&mut self, key: HeldKey) {
|
||||||
let state = self.held.entry(key).or_insert(HeldState { last_seen: Instant::now(), just_pressed: false });
|
self.held.insert(key, HeldState { last_seen: Instant::now() });
|
||||||
state.last_seen = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_pressed(&mut self, key: HeldKey) {
|
|
||||||
self.held.insert(key, HeldState { last_seen: Instant::now(), just_pressed: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn release(&mut self, key: HeldKey) {
|
fn release(&mut self, key: HeldKey) {
|
||||||
@@ -91,22 +114,19 @@ impl InputHandler {
|
|||||||
self.held.contains_key(&key)
|
self.held.contains_key(&key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain all pending input events, update held key state.
|
|
||||||
/// Returns one-shot actions (quit, paint, jump-on-press).
|
|
||||||
pub fn update(&mut self) -> Vec<Action> {
|
pub fn update(&mut self) -> Vec<Action> {
|
||||||
let mut one_shots = Vec::new();
|
let mut one_shots = Vec::new();
|
||||||
self.jump_pressed = false;
|
self.jump_pressed = false;
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
// Expire stale held keys (no Repeat received within timeout)
|
|
||||||
self.held.retain(|_, state| now.duration_since(state.last_seen) < HOLD_TIMEOUT);
|
self.held.retain(|_, state| now.duration_since(state.last_seen) < HOLD_TIMEOUT);
|
||||||
|
|
||||||
while event::poll(Duration::from_millis(0)).unwrap_or(false) {
|
let events: Vec<Event> = match &self.receiver {
|
||||||
let ev = match event::read() {
|
Some(rx) => rx.try_iter().collect(),
|
||||||
Ok(ev) => ev,
|
None => return one_shots,
|
||||||
Err(_) => continue,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
|
for ev in events {
|
||||||
if let Event::Key(KeyEvent { code, modifiers, kind, .. }) = ev {
|
if let Event::Key(KeyEvent { code, modifiers, kind, .. }) = ev {
|
||||||
let is_press = kind == KeyEventKind::Press;
|
let is_press = kind == KeyEventKind::Press;
|
||||||
let is_repeat = kind == KeyEventKind::Repeat;
|
let is_repeat = kind == KeyEventKind::Repeat;
|
||||||
@@ -117,7 +137,6 @@ impl InputHandler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jump: only on initial press, not repeat
|
|
||||||
if is_press {
|
if is_press {
|
||||||
match code {
|
match code {
|
||||||
KeyCode::Up | KeyCode::Char('w') | KeyCode::Char(' ') => {
|
KeyCode::Up | KeyCode::Char('w') | KeyCode::Char(' ') => {
|
||||||
@@ -143,11 +162,8 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement keys: track held state
|
|
||||||
if let Some(held_key) = Self::key_to_held(code) {
|
if let Some(held_key) = Self::key_to_held(code) {
|
||||||
if is_press {
|
if is_press || is_repeat {
|
||||||
self.mark_pressed(held_key);
|
|
||||||
} else if is_repeat {
|
|
||||||
self.mark_held(held_key);
|
self.mark_held(held_key);
|
||||||
} else if is_release {
|
} else if is_release {
|
||||||
self.release(held_key);
|
self.release(held_key);
|
||||||
@@ -156,18 +172,12 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear just_pressed flags
|
|
||||||
for state in self.held.values_mut() {
|
|
||||||
state.just_pressed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
one_shots
|
one_shots
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn held_actions(&self) -> Vec<Action> {
|
pub fn held_actions(&self) -> Vec<Action> {
|
||||||
let mut actions = Vec::new();
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
// Left/Right: last pressed wins if both held
|
|
||||||
let left = self.is_held(HeldKey::Left);
|
let left = self.is_held(HeldKey::Left);
|
||||||
let right = self.is_held(HeldKey::Right);
|
let right = self.is_held(HeldKey::Right);
|
||||||
|
|
||||||
@@ -176,7 +186,6 @@ impl InputHandler {
|
|||||||
} else if right && !left {
|
} else if right && !left {
|
||||||
actions.push(Action::MoveRight);
|
actions.push(Action::MoveRight);
|
||||||
} else if left && right {
|
} else if left && right {
|
||||||
// Both held — check which was pressed more recently
|
|
||||||
let left_time = self.held.get(&HeldKey::Left).map(|s| s.last_seen);
|
let left_time = self.held.get(&HeldKey::Left).map(|s| s.last_seen);
|
||||||
let right_time = self.held.get(&HeldKey::Right).map(|s| s.last_seen);
|
let right_time = self.held.get(&HeldKey::Right).map(|s| s.last_seen);
|
||||||
match (left_time, right_time) {
|
match (left_time, right_time) {
|
||||||
|
|||||||
Reference in New Issue
Block a user