Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_fx/
shake.rs

1//! Screen-shake counter.
2//!
3//! The cheapest possible "the world reacted" feedback. A single
4//! frame counter; `tick()` decrements it and returns a small
5//! `(dx, dy)` offset the caller adds to their vertex positions.
6//!
7//! Triangle-wave pattern -- sign flips each frame so it reads as a
8//! judder rather than a smooth drift. Magnitude decays linearly
9//! with the counter, so the last frames are subtle.
10//!
11//! # Example
12//!
13//! ```ignore
14//! let mut shake = ShakeState::new();
15//! // On impact:
16//! shake.trigger(6);
17//! // Every frame, folded into rendering:
18//! let (dx, dy) = shake.tick();
19//! draw_game_world_at_offset(dx, dy);
20//! ```
21
22/// Frame-countdown that emits a judder offset.
23#[derive(Copy, Clone, Debug, Default)]
24pub struct ShakeState {
25    /// Frames of shake remaining. `0` = idle.
26    pub frames: u8,
27}
28
29impl ShakeState {
30    /// Idle instance.
31    pub const fn new() -> Self {
32        Self { frames: 0 }
33    }
34
35    /// Start (or retrigger) a shake. If a bigger shake is already
36    /// active, it stays -- smaller triggers don't interrupt.
37    pub fn trigger(&mut self, frames: u8) {
38        self.frames = self.frames.max(frames);
39    }
40
41    /// Advance one frame. Returns `(dx, dy)` in pixels. Safe to
42    /// call every frame even when idle -- returns `(0, 0)` then.
43    pub fn tick(&mut self) -> (i16, i16) {
44        if self.frames == 0 {
45            return (0, 0);
46        }
47        let s = self.frames as i16;
48        // Sign flips with parity so the shake rattles instead of
49        // drifting. Magnitudes decrease linearly with `frames`.
50        let sign = if s & 1 == 0 { 1 } else { -1 };
51        let offset = ((s / 2) * sign, ((s + 1) / 2) * -sign);
52        self.frames -= 1;
53        offset
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn idle_is_zero() {
63        let mut s = ShakeState::new();
64        assert_eq!(s.tick(), (0, 0));
65        assert_eq!(s.tick(), (0, 0));
66    }
67
68    #[test]
69    fn trigger_then_decay_to_zero() {
70        let mut s = ShakeState::new();
71        s.trigger(4);
72        for _ in 0..4 {
73            let _ = s.tick();
74        }
75        assert_eq!(s.tick(), (0, 0));
76        assert_eq!(s.frames, 0);
77    }
78
79    #[test]
80    fn trigger_doesnt_shrink() {
81        let mut s = ShakeState::new();
82        s.trigger(10);
83        s.trigger(3); // smaller trigger shouldn't reset
84        assert_eq!(s.frames, 10);
85    }
86}