make debounce functionality into a reusable ting
This commit is contained in:
@@ -1,14 +1,10 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_pkv::prelude::*;
|
use bevy_pkv::prelude::*;
|
||||||
|
|
||||||
use crate::client::audio::SoundSettings;
|
use crate::{client::audio::SoundSettings, utils::Debounce};
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
struct SaveTimer(Timer);
|
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.insert_resource(PkvStore::new("Rustunit", "HEDZ"));
|
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(Update, persist_settings);
|
||||||
app.add_systems(Startup, load_settings);
|
app.add_systems(Startup, load_settings);
|
||||||
@@ -17,16 +13,13 @@ pub fn plugin(app: &mut App) {
|
|||||||
fn persist_settings(
|
fn persist_settings(
|
||||||
settings: Res<SoundSettings>,
|
settings: Res<SoundSettings>,
|
||||||
mut pkv: ResMut<PkvStore>,
|
mut pkv: ResMut<PkvStore>,
|
||||||
mut timer: ResMut<SaveTimer>,
|
mut debounce: Debounce<1000>,
|
||||||
time: Res<Time>,
|
|
||||||
) -> Result {
|
) -> Result {
|
||||||
if settings.is_changed() {
|
if settings.is_changed() {
|
||||||
timer.0.reset();
|
debounce.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.0.tick(time.delta());
|
if debounce.finished() {
|
||||||
|
|
||||||
if timer.0.just_finished() {
|
|
||||||
pkv.set("audio", &*settings)?;
|
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 auto_rotate;
|
||||||
pub mod billboards;
|
pub mod billboards;
|
||||||
|
pub mod debounce;
|
||||||
pub mod explosions;
|
pub mod explosions;
|
||||||
pub mod observers;
|
pub mod observers;
|
||||||
pub mod one_shot_force;
|
pub mod one_shot_force;
|
||||||
@@ -9,6 +10,7 @@ pub mod squish_animation;
|
|||||||
pub mod trail;
|
pub mod trail;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
pub use debounce::Debounce;
|
||||||
pub(crate) use observers::global_observer;
|
pub(crate) use observers::global_observer;
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
|
|||||||
2
justfile
2
justfile
@@ -1,4 +1,5 @@
|
|||||||
# map trenchbroom game folder to here:
|
# map trenchbroom game folder to here:
|
||||||
|
|
||||||
# see https://trenchbroom.github.io/manual/latest/#game_configuration_files
|
# see https://trenchbroom.github.io/manual/latest/#game_configuration_files
|
||||||
tb_setup_mac:
|
tb_setup_mac:
|
||||||
mkdir -p "$HOME/Library/Application Support/TrenchBroom/games/hedz" | true
|
mkdir -p "$HOME/Library/Application Support/TrenchBroom/games/hedz" | true
|
||||||
@@ -30,5 +31,6 @@ check:
|
|||||||
cargo b {{ server_args }}
|
cargo b {{ server_args }}
|
||||||
cargo clippy {{ client_args }}
|
cargo clippy {{ client_args }}
|
||||||
cargo clippy {{ server_args }}
|
cargo clippy {{ server_args }}
|
||||||
|
cargo test --lib
|
||||||
cargo test {{ client_args }}
|
cargo test {{ client_args }}
|
||||||
cargo test {{ server_args }}
|
cargo test {{ server_args }}
|
||||||
|
|||||||
Reference in New Issue
Block a user