use std::collections::VecDeque;
use std::sync::Arc;
use super::{Graph, Params, Rate, StateId};
const HISTORY: usize = 8;
#[derive(Clone, Debug, PartialEq)]
pub enum AnimEvent {
Entered(StateId),
Marker(Arc<str>),
Finished(StateId),
}
#[derive(Clone, Debug)]
pub struct Animator {
graph: Arc<Graph>,
pub params: Params,
state: StateId,
time: f32,
clip_t: f32,
frame: usize,
finished: bool,
events: Vec<AnimEvent>,
history: VecDeque<(StateId, f32)>,
}
impl Animator {
pub fn new(graph: Arc<Graph>) -> Self {
let mut a = Self {
params: graph.params.clone(),
state: graph.entry,
graph,
time: 0.0,
clip_t: 0.0,
frame: 0,
finished: false,
events: Vec::new(),
history: VecDeque::new(),
};
a.enter(a.state);
a
}
pub fn reset(&mut self) {
self.params.reset_to(&self.graph.params);
self.history.clear();
self.time = 0.0;
self.state = self.graph.entry;
self.enter(self.state);
}
pub fn update(&mut self, dt: f32) {
self.advance(dt);
if let Some(i) = self.pick() {
let mut used = Vec::new();
let tr = &self.graph.transitions[i];
tr.when.used_triggers(&self.params, &mut used);
let to = tr.to;
used.into_iter().for_each(|id| self.params.consume(id));
self.enter(to);
} else if self.finished
&& let Some(next) = self.graph.state(self.state).next
{
self.enter(next);
}
self.params.decay(dt);
}
fn advance(&mut self, dt: f32) {
let st = self.graph.state(self.state);
let rate = match st.rate {
Rate::Fixed(r) => r,
Rate::Param {
param,
per,
min,
max,
} => (self.params.get_float(param).abs() / per).clamp(min, max),
};
self.time += dt;
self.clip_t += dt * rate;
let (frame, finished) = st.clip.frame_at(self.clip_t);
if frame != self.frame {
self.frame = frame;
self.markers();
}
if finished && !self.finished {
self.finished = true;
self.events.push(AnimEvent::Finished(self.state));
}
}
fn pick(&self) -> Option<usize> {
let locked = self.locked();
self.graph.transitions.iter().position(|t| {
t.from.applies(self.state)
&& (!locked || t.breaks_lock)
&& (t.to != self.state || t.restart)
&& t.when.eval(&self.params, self.finished, self.time)
})
}
fn enter(&mut self, to: StateId) {
if self.time > 0.0 || to != self.state {
self.history.push_back((self.state, self.time));
if self.history.len() > HISTORY {
self.history.pop_front();
}
}
self.state = to;
self.time = 0.0;
self.clip_t = 0.0;
self.frame = 0;
self.finished = false;
self.events.push(AnimEvent::Entered(to));
self.markers();
}
fn markers(&mut self) {
let clip = &self.graph.state(self.state).clip;
for (f, name) in &clip.markers {
if *f == self.frame {
self.events.push(AnimEvent::Marker(name.clone()));
}
}
}
pub fn graph(&self) -> &Graph {
&self.graph
}
pub fn state(&self) -> StateId {
self.state
}
pub fn state_name(&self) -> &str {
self.graph.state(self.state).name.as_ref()
}
pub fn sprite(&self) -> u16 {
self.graph.state(self.state).clip.sprite(self.frame)
}
pub fn frame(&self) -> usize {
self.frame
}
pub fn time_in_state(&self) -> f32 {
self.time
}
pub fn locked(&self) -> bool {
self.time < self.graph.state(self.state).lock
}
pub fn history(&self) -> impl Iterator<Item = (StateId, f32)> + '_ {
self.history.iter().copied()
}
pub fn drain_events(&mut self) -> std::vec::Drain<'_, AnimEvent> {
self.events.drain(..)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::anim::{Clip, Cond, GraphSpec, Playback, StateSpec, TransitionSpec};
fn graph() -> Arc<Graph> {
let c = |s: &[u16]| Clip::uniform(Playback::Loop, s, 0.1);
Arc::new(
GraphSpec::new()
.float("speed")
.trigger("jump", 0.2)
.trigger("hit", 0.0)
.state(StateSpec::new("idle", c(&[0])))
.state(StateSpec::new("run", c(&[1, 2]).marker(1, "step")))
.state(
StateSpec::new("jump", Clip::uniform(Playback::Once, &[3, 4], 0.1))
.lock(0.15)
.then("idle"),
)
.state(StateSpec::new("hurt", c(&[5])))
.transition(TransitionSpec::new(
["idle"],
"run",
Cond::Above("speed".into(), 1.0),
))
.transition(TransitionSpec::new(
["run"],
"idle",
Cond::Below("speed".into(), 1.0),
))
.transition(TransitionSpec::any("jump", Cond::Trigger("jump".into())).priority(10))
.transition(
TransitionSpec::any("hurt", Cond::Trigger("hit".into()))
.priority(20)
.breaks_lock(),
)
.compile()
.unwrap(),
)
}
fn step(a: &mut Animator, speed: f32) -> String {
let id = a.params.id("speed").unwrap();
a.params.set_float(id, speed);
a.update(0.01);
a.state_name().to_string()
}
#[test]
fn follows_parameters_and_reports_markers() {
let mut a = Animator::new(graph());
assert_eq!(step(&mut a, 0.0), "idle");
assert_eq!(step(&mut a, 5.0), "run");
a.drain_events();
for _ in 0..12 {
step(&mut a, 5.0);
}
assert!(
a.drain_events()
.any(|e| e == AnimEvent::Marker("step".into()))
);
assert_eq!(step(&mut a, 0.0), "idle");
}
#[test]
fn a_trigger_fired_during_a_lock_waits_for_it() {
let mut a = Animator::new(graph());
let jump = a.params.id("jump").unwrap();
a.params.fire(jump);
assert_eq!(step(&mut a, 0.0), "jump");
assert!(!a.params.pending(jump), "the transition consumed it");
a.params.fire(jump);
assert!(a.locked());
for _ in 0..25 {
step(&mut a, 0.0);
}
assert_eq!(a.state_name(), "idle");
}
#[test]
fn lock_breakers_cut_in_and_zero_ttl_triggers_expire() {
let mut a = Animator::new(graph());
a.params.fire(a.params.id("jump").unwrap());
step(&mut a, 0.0);
a.params.fire(a.params.id("hit").unwrap());
assert_eq!(step(&mut a, 0.0), "hurt");
let mut b = Animator::new(graph());
b.params.fire(b.params.id("hit").unwrap());
b.update(0.0);
assert_eq!(b.state_name(), "hurt");
let hit = b.params.id("hit").unwrap();
b.params.fire(hit);
b.update(0.01);
assert!(
!b.params.pending(hit),
"unconsumed zero-ttl trigger expired"
);
}
#[test]
fn once_clips_finish_and_follow_next() {
let mut a = Animator::new(graph());
a.params.fire(a.params.id("jump").unwrap());
step(&mut a, 0.0);
assert_eq!(a.sprite(), 3);
for _ in 0..11 {
step(&mut a, 0.0);
}
assert_eq!(a.sprite(), 4);
for _ in 0..10 {
step(&mut a, 0.0);
}
assert_eq!(a.state_name(), "idle");
}
#[test]
fn reset_starts_over_from_the_entry_state() {
let mut a = Animator::new(graph());
let jump = a.params.id("jump").unwrap();
a.params.fire(jump);
step(&mut a, 5.0);
assert!(a.locked());
a.params.fire(jump);
a.reset();
assert_eq!(a.state_name(), "idle");
assert!(!a.locked());
assert!(!a.params.pending(jump), "pending triggers are gone");
assert_eq!(a.history().count(), 0);
let entered = AnimEvent::Entered(a.state());
assert!(a.drain_events().any(|e| e == entered));
a.update(0.01);
assert_eq!(a.state_name(), "idle");
}
}