game_toolkit_platformer/anim/
animator.rs1use std::collections::VecDeque;
4use std::sync::Arc;
5
6use super::{Graph, Params, Rate, StateId};
7
8const HISTORY: usize = 8;
10
11#[derive(Clone, Debug, PartialEq)]
13pub enum AnimEvent {
14 Entered(StateId),
16 Marker(Arc<str>),
18 Finished(StateId),
20}
21
22#[derive(Clone, Debug)]
24pub struct Animator {
25 graph: Arc<Graph>,
27 pub params: Params,
29 state: StateId,
31 time: f32,
33 clip_t: f32,
35 frame: usize,
37 finished: bool,
39 events: Vec<AnimEvent>,
41 history: VecDeque<(StateId, f32)>,
43}
44
45impl Animator {
46 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 pub fn reset(&mut self) {
67 self.params.reset_to(&self.graph.params);
68 self.history.clear();
69 self.time = 0.0;
71 self.state = self.graph.entry;
72 self.enter(self.state);
73 }
74
75 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 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 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 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 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 pub fn graph(&self) -> &Graph {
161 &self.graph
162 }
163
164 pub fn state(&self) -> StateId {
166 self.state
167 }
168
169 pub fn state_name(&self) -> &str {
171 self.graph.state(self.state).name.as_ref()
172 }
173
174 pub fn sprite(&self) -> u16 {
176 self.graph.state(self.state).clip.sprite(self.frame)
177 }
178
179 pub fn frame(&self) -> usize {
181 self.frame
182 }
183
184 pub fn time_in_state(&self) -> f32 {
186 self.time
187 }
188
189 pub fn locked(&self) -> bool {
191 self.time < self.graph.state(self.state).lock
192 }
193
194 pub fn history(&self) -> impl Iterator<Item = (StateId, f32)> + '_ {
196 self.history.iter().copied()
197 }
198
199 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 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 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 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 a.update(0.01);
343 assert_eq!(a.state_name(), "idle");
344 }
345}