make debounce functionality into a reusable ting

This commit is contained in:
2025-12-19 14:09:36 -05:00
parent 8132203653
commit c34fe0c90c
4 changed files with 170 additions and 21 deletions

View File

@@ -1,14 +1,10 @@
use bevy::prelude::*;
use bevy_pkv::prelude::*;
use crate::client::audio::SoundSettings;
#[derive(Resource)]
struct SaveTimer(Timer);
use crate::{client::audio::SoundSettings, utils::Debounce};
pub fn plugin(app: &mut App) {
app.insert_resource(PkvStore::new("Rustunit", "HEDZ"));
app.insert_resource(SaveTimer(Timer::from_seconds(1.0, TimerMode::Once)));
app.add_systems(Update, persist_settings);
app.add_systems(Startup, load_settings);
@@ -17,16 +13,13 @@ pub fn plugin(app: &mut App) {
fn persist_settings(
settings: Res<SoundSettings>,
mut pkv: ResMut<PkvStore>,
mut timer: ResMut<SaveTimer>,
time: Res<Time>,
mut debounce: Debounce<1000>,
) -> Result {
if settings.is_changed() {
timer.0.reset();
debounce.reset();
}
timer.0.tick(time.delta());
if timer.0.just_finished() {
if debounce.finished() {
pkv.set("audio", &*settings)?;
}

View File

@@ -0,0 +1,152 @@
use bevy::{ecs::system::SystemParam, prelude::*};
/// A debounce timer that delays an action until after a period of inactivity.
///
/// Useful for delaying expensive operations (like disk I/O) until user input has settled.
/// Automatically ticks with time, no manual `time` parameter needed.
///
/// # Type Parameters
/// * `MILLIS` - The debounce delay in milliseconds
///
/// # Example
/// ```ignore
/// fn save_settings(
/// settings: Res<GameSettings>,
/// mut debounce: Debounce<1000>, // 1 second debounce
/// mut pkv: ResMut<PkvStore>,
/// ) -> Result {
/// if settings.is_changed() {
/// debounce.reset();
/// }
///
/// if debounce.finished() {
/// pkv.set("settings", &*settings)?;
/// }
///
/// Ok(())
/// }
/// ```
#[derive(SystemParam)]
pub struct Debounce<'w, 's, const MILLIS: u64> {
timer: Local<'s, DebounceTimer<MILLIS>>,
time: Res<'w, Time>,
}
struct DebounceTimer<const MILLIS: u64> {
timer: Timer,
}
impl<const MILLIS: u64> Default for DebounceTimer<MILLIS> {
fn default() -> Self {
Self {
timer: Timer::from_seconds((MILLIS as f32) / 1000.0, TimerMode::Once),
}
}
}
impl<'w, 's, const MILLIS: u64> Debounce<'w, 's, MILLIS> {
/// Check if the debounce timer has finished.
///
/// Returns `true` if the timer just finished, meaning the debounce period has elapsed.
/// Automatically ticks the internal timer.
pub fn finished(&mut self) -> bool {
self.timer.timer.tick(self.time.delta()).just_finished()
}
/// Reset the debounce timer, restarting the countdown.
///
/// Call this when the event you're debouncing occurs.
pub fn reset(&mut self) {
self.timer.timer.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Resource, Default)]
struct Counter(u32);
fn increment_with_debounce(
mut counter: ResMut<Counter>,
mut debounce: Debounce<50>, // 50ms debounce
) {
if debounce.finished() {
counter.0 += 1;
}
}
#[test]
fn debounce_triggers_after_delay() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.init_resource::<Counter>();
app.add_systems(Update, increment_with_debounce);
// Counter should not increment immediately
app.update();
assert_eq!(app.world().resource::<Counter>().0, 0);
// Sleep for just under the debounce time
std::thread::sleep(std::time::Duration::from_millis(40));
app.update();
assert_eq!(app.world().resource::<Counter>().0, 0);
// Sleep past the debounce time
std::thread::sleep(std::time::Duration::from_millis(15));
app.update();
assert_eq!(app.world().resource::<Counter>().0, 1);
// Should not trigger again on subsequent updates
app.update();
assert_eq!(app.world().resource::<Counter>().0, 1);
}
#[test]
fn debounce_resets_on_reset() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.init_resource::<Counter>();
#[derive(Resource, Default)]
struct ShouldReset(bool);
fn reset_and_increment(
mut counter: ResMut<Counter>,
mut debounce: Debounce<50>,
mut should_reset: ResMut<ShouldReset>,
) {
if should_reset.0 {
debounce.reset();
should_reset.0 = false;
}
if debounce.finished() {
counter.0 += 1;
}
}
app.init_resource::<ShouldReset>();
app.add_systems(Update, reset_and_increment);
// Sleep for 40ms and update - should NOT trigger yet
std::thread::sleep(std::time::Duration::from_millis(40));
app.update();
assert_eq!(app.world().resource::<Counter>().0, 0);
// Reset the timer now
app.world_mut().resource_mut::<ShouldReset>().0 = true;
app.update();
assert_eq!(app.world().resource::<Counter>().0, 0);
// Sleep for another 40ms - still shouldn't trigger (timer was reset)
std::thread::sleep(std::time::Duration::from_millis(40));
app.update();
assert_eq!(app.world().resource::<Counter>().0, 0);
// Sleep past the 50ms from reset point
std::thread::sleep(std::time::Duration::from_millis(15));
app.update();
assert_eq!(app.world().resource::<Counter>().0, 1);
}
}

View File

@@ -1,5 +1,6 @@
pub mod auto_rotate;
pub mod billboards;
pub mod debounce;
pub mod explosions;
pub mod observers;
pub mod one_shot_force;
@@ -9,6 +10,7 @@ pub mod squish_animation;
pub mod trail;
use bevy::prelude::*;
pub use debounce::Debounce;
pub(crate) use observers::global_observer;
pub fn plugin(app: &mut App) {

View File

@@ -1,4 +1,5 @@
# map trenchbroom game folder to here:
# see https://trenchbroom.github.io/manual/latest/#game_configuration_files
tb_setup_mac:
mkdir -p "$HOME/Library/Application Support/TrenchBroom/games/hedz" | true
@@ -9,16 +10,16 @@ client_args := "--bin hedz_reloaded"
server_args := "--bin hedz_reloaded_server --no-default-features"
run *args:
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
RUST_BACKTRACE=1 cargo r {{ client_args }} -- {{ args }}
server:
RUST_BACKTRACE=1 cargo r {{server_args}}
RUST_BACKTRACE=1 cargo r {{ server_args }}
dbg *args:
RUST_BACKTRACE=1 cargo r {{client_args}} --features dbg -- {{args}}
RUST_BACKTRACE=1 cargo r {{ client_args }} --features dbg -- {{ args }}
dbg-server:
RUST_BACKTRACE=1 cargo r {{server_args}} --features dbg
RUST_BACKTRACE=1 cargo r {{ server_args }} --features dbg
sort:
cargo sort --check --workspace
@@ -26,9 +27,10 @@ sort:
check:
cargo sort --check --workspace
cargo fmt --check
cargo b {{client_args}}
cargo b {{server_args}}
cargo clippy {{client_args}}
cargo clippy {{server_args}}
cargo test {{client_args}}
cargo test {{server_args}}
cargo b {{ client_args }}
cargo b {{ server_args }}
cargo clippy {{ client_args }}
cargo clippy {{ server_args }}
cargo test --lib
cargo test {{ client_args }}
cargo test {{ server_args }}