Skip to main content

game_toolkit_platformer/anim/
animator.rs

1//! [`Animator`]: runs a [`Graph`]: plays the current state's clip and takes transitions.
2
3use std::collections::VecDeque;
4use std::sync::Arc;
5
6use super::{Graph, Params, Rate, StateId};
7
8/// How many past states the animator remembers, for debugging.
9const HISTORY: usize = 8;
10
11/// Something the animator did in an update, for sounds and effects.
12#[derive(Clone, Debug, PartialEq)]
13pub enum AnimEvent {
14    /// Entered a state.
15    Entered(StateId),
16    /// A frame with this marker started.
17    Marker(Arc<str>),
18    /// A once-clip reached its end.
19    Finished(StateId),
20}
21
22/// One running instance of a graph.
23#[derive(Clone, Debug)]
24pub struct Animator {
25    /// The graph, shared between animators.
26    graph: Arc<Graph>,
27    /// This animator's parameter values. Drivers write here.
28    pub params: Params,
29    /// The current state.
30    state: StateId,
31    /// Seconds in the current state.
32    time: f32,
33    /// Clip time (seconds scaled by the rate).
34    clip_t: f32,
35    /// The frame shown.
36    frame: usize,
37    /// The once-clip has finished.
38    finished: bool,
39    /// What happened since the last drain.
40    events: Vec<AnimEvent>,
41    /// Recently left states, newest last, with seconds spent in each.
42    history: VecDeque<(StateId, f32)>,
43}
44
45impl Animator {
46    /// An animator in the graph's entry state.
47    pub fn new(graph: Arc<Graph>) -> Self {
48        let mut a = Self {
49            params: graph.params.clone(),
50            state: graph.entry,
51            graph,
52            time: 0.0,
53            clip_t: 0.0,
54            frame: 0,
55            finished: false,
56            events: Vec::new(),
57            history: VecDeque::new(),
58        };
59        a.enter(a.state);
60        a
61    }
62
63    /// Start over: the graph's entry state, its parameters at their defaults (and any a
64    /// driver declared on its own at 0), no pending triggers, no history. For a character
65    /// that respawns.
66    pub fn reset(&mut self) {
67        self.params.reset_to(&self.graph.params);
68        self.history.clear();
69        // Entering the current state with no time in it leaves no history.
70        self.time = 0.0;
71        self.state = self.graph.entry;
72        self.enter(self.state);
73    }
74
75    /// Advance `dt` seconds: play the clip, then take the first transition (by priority)
76    /// whose condition holds, consuming the triggers it tested; failing that, follow the
77    /// state's `next` if its clip finished. Pending triggers then age.
78    pub fn update(&mut self, dt: f32) {
79        self.advance(dt);
80        if let Some(i) = self.pick() {
81            let mut used = Vec::new();
82            let tr = &self.graph.transitions[i];
83            tr.when.used_triggers(&self.params, &mut used);
84            let to = tr.to;
85            used.into_iter().for_each(|id| self.params.consume(id));
86            self.enter(to);
87        } else if self.finished
88            && let Some(next) = self.graph.state(self.state).next
89        {
90            self.enter(next);
91        }
92        self.params.decay(dt);
93    }
94
95    /// Play the clip on by `dt` at the state's rate; report new frames' markers and the
96    /// end of once-clips.
97    fn advance(&mut self, dt: f32) {
98        let st = self.graph.state(self.state);
99        let rate = match st.rate {
100            Rate::Fixed(r) => r,
101            Rate::Param {
102                param,
103                per,
104                min,
105                max,
106            } => (self.params.get_float(param).abs() / per).clamp(min, max),
107        };
108        self.time += dt;
109        self.clip_t += dt * rate;
110        let (frame, finished) = st.clip.frame_at(self.clip_t);
111        if frame != self.frame {
112            self.frame = frame;
113            self.markers();
114        }
115        if finished && !self.finished {
116            self.finished = true;
117            self.events.push(AnimEvent::Finished(self.state));
118        }
119    }
120
121    /// The index of the transition to take now, if any.
122    fn pick(&self) -> Option<usize> {
123        let locked = self.locked();
124        self.graph.transitions.iter().position(|t| {
125            t.from.applies(self.state)
126                && (!locked || t.breaks_lock)
127                && (t.to != self.state || t.restart)
128                && t.when.eval(&self.params, self.finished, self.time)
129        })
130    }
131
132    /// Switch to `to`, from the start of its clip.
133    fn enter(&mut self, to: StateId) {
134        if self.time > 0.0 || to != self.state {
135            self.history.push_back((self.state, self.time));
136            if self.history.len() > HISTORY {
137                self.history.pop_front();
138            }
139        }
140        self.state = to;
141        self.time = 0.0;
142        self.clip_t = 0.0;
143        self.frame = 0;
144        self.finished = false;
145        self.events.push(AnimEvent::Entered(to));
146        self.markers();
147    }
148
149    /// Report the markers on the current frame.
150    fn markers(&mut self) {
151        let clip = &self.graph.state(self.state).clip;
152        for (f, name) in &clip.markers {
153            if *f == self.frame {
154                self.events.push(AnimEvent::Marker(name.clone()));
155            }
156        }
157    }
158
159    /// The graph.
160    pub fn graph(&self) -> &Graph {
161        &self.graph
162    }
163
164    /// The current state.
165    pub fn state(&self) -> StateId {
166        self.state
167    }
168
169    /// The current state's name.
170    pub fn state_name(&self) -> &str {
171        self.graph.state(self.state).name.as_ref()
172    }
173
174    /// The sprite to draw now.
175    pub fn sprite(&self) -> u16 {
176        self.graph.state(self.state).clip.sprite(self.frame)
177    }
178
179    /// The frame index within the clip.
180    pub fn frame(&self) -> usize {
181        self.frame
182    }
183
184    /// Seconds in the current state.
185    pub fn time_in_state(&self) -> f32 {
186        self.time
187    }
188
189    /// Whether only lock-breaking transitions can leave the state right now.
190    pub fn locked(&self) -> bool {
191        self.time < self.graph.state(self.state).lock
192    }
193
194    /// Recently left states, oldest first, with seconds spent in each.
195    pub fn history(&self) -> impl Iterator<Item = (StateId, f32)> + '_ {
196        self.history.iter().copied()
197    }
198
199    /// Take what happened since the last call.
200    pub fn drain_events(&mut self) -> std::vec::Drain<'_, AnimEvent> {
201        self.events.drain(..)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::anim::{Clip, Cond, GraphSpec, Playback, StateSpec, TransitionSpec};
209
210    /// idle <-> run by speed; any -> jump on a trigger; jump is a locked once-clip that
211    /// returns to idle.
212    fn graph() -> Arc<Graph> {
213        let c = |s: &[u16]| Clip::uniform(Playback::Loop, s, 0.1);
214        Arc::new(
215            GraphSpec::new()
216                .float("speed")
217                .trigger("jump", 0.2)
218                .trigger("hit", 0.0)
219                .state(StateSpec::new("idle", c(&[0])))
220                .state(StateSpec::new("run", c(&[1, 2]).marker(1, "step")))
221                .state(
222                    StateSpec::new("jump", Clip::uniform(Playback::Once, &[3, 4], 0.1))
223                        .lock(0.15)
224                        .then("idle"),
225                )
226                .state(StateSpec::new("hurt", c(&[5])))
227                .transition(TransitionSpec::new(
228                    ["idle"],
229                    "run",
230                    Cond::Above("speed".into(), 1.0),
231                ))
232                .transition(TransitionSpec::new(
233                    ["run"],
234                    "idle",
235                    Cond::Below("speed".into(), 1.0),
236                ))
237                .transition(TransitionSpec::any("jump", Cond::Trigger("jump".into())).priority(10))
238                .transition(
239                    TransitionSpec::any("hurt", Cond::Trigger("hit".into()))
240                        .priority(20)
241                        .breaks_lock(),
242                )
243                .compile()
244                .unwrap(),
245        )
246    }
247
248    /// The animator's state name after an update with `speed`.
249    fn step(a: &mut Animator, speed: f32) -> String {
250        let id = a.params.id("speed").unwrap();
251        a.params.set_float(id, speed);
252        a.update(0.01);
253        a.state_name().to_string()
254    }
255
256    #[test]
257    fn follows_parameters_and_reports_markers() {
258        let mut a = Animator::new(graph());
259        assert_eq!(step(&mut a, 0.0), "idle");
260        assert_eq!(step(&mut a, 5.0), "run");
261        a.drain_events();
262        for _ in 0..12 {
263            step(&mut a, 5.0);
264        }
265        assert!(
266            a.drain_events()
267                .any(|e| e == AnimEvent::Marker("step".into()))
268        );
269        assert_eq!(step(&mut a, 0.0), "idle");
270    }
271
272    #[test]
273    fn a_trigger_fired_during_a_lock_waits_for_it() {
274        let mut a = Animator::new(graph());
275        let jump = a.params.id("jump").unwrap();
276        a.params.fire(jump);
277        assert_eq!(step(&mut a, 0.0), "jump");
278        assert!(!a.params.pending(jump), "the transition consumed it");
279        // Fired again while locked: no restart (and no restart flag), but it doesn't block
280        // the clip's own way out either.
281        a.params.fire(jump);
282        assert!(a.locked());
283        for _ in 0..25 {
284            step(&mut a, 0.0);
285        }
286        assert_eq!(a.state_name(), "idle");
287    }
288
289    #[test]
290    fn lock_breakers_cut_in_and_zero_ttl_triggers_expire() {
291        let mut a = Animator::new(graph());
292        a.params.fire(a.params.id("jump").unwrap());
293        step(&mut a, 0.0);
294        a.params.fire(a.params.id("hit").unwrap());
295        assert_eq!(step(&mut a, 0.0), "hurt");
296
297        let mut b = Animator::new(graph());
298        b.params.fire(b.params.id("hit").unwrap());
299        b.update(0.0);
300        assert_eq!(b.state_name(), "hurt");
301        let hit = b.params.id("hit").unwrap();
302        b.params.fire(hit);
303        b.update(0.01);
304        assert!(
305            !b.params.pending(hit),
306            "unconsumed zero-ttl trigger expired"
307        );
308    }
309
310    #[test]
311    fn once_clips_finish_and_follow_next() {
312        let mut a = Animator::new(graph());
313        a.params.fire(a.params.id("jump").unwrap());
314        step(&mut a, 0.0);
315        assert_eq!(a.sprite(), 3);
316        for _ in 0..11 {
317            step(&mut a, 0.0);
318        }
319        assert_eq!(a.sprite(), 4);
320        for _ in 0..10 {
321            step(&mut a, 0.0);
322        }
323        assert_eq!(a.state_name(), "idle");
324    }
325
326    #[test]
327    fn reset_starts_over_from_the_entry_state() {
328        let mut a = Animator::new(graph());
329        let jump = a.params.id("jump").unwrap();
330        a.params.fire(jump);
331        step(&mut a, 5.0);
332        assert!(a.locked());
333        a.params.fire(jump);
334        a.reset();
335        assert_eq!(a.state_name(), "idle");
336        assert!(!a.locked());
337        assert!(!a.params.pending(jump), "pending triggers are gone");
338        assert_eq!(a.history().count(), 0);
339        let entered = AnimEvent::Entered(a.state());
340        assert!(a.drain_events().any(|e| e == entered));
341        // The speed went back to its default, so it stays idle.
342        a.update(0.01);
343        assert_eq!(a.state_name(), "idle");
344    }
345}