make debounce functionality into a reusable ting
This commit is contained in:
@@ -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)?;
|
||||
}
|
||||
|
||||
|
||||
152
crates/hedz_reloaded/src/utils/debounce.rs
Normal file
152
crates/hedz_reloaded/src/utils/debounce.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
22
justfile
22
justfile
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user