Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Animation Playback

Live Demo: Dance

AnimationPlayer is the component that tracks playback state for one animated entity. It holds the loaded clips, the index of the current clip, the playback time within that clip, the speed multiplier, and the flags that control looping and pausing. The animation system on the frame schedule advances the time each frame and writes the sampled pose into the joint entities.

AnimationPlayer Component

#![allow(unused)]
fn main() {
pub struct AnimationPlayer {
    pub clips: Vec<AnimationClip>,
    pub current_clip: Option<usize>,
    pub time: f32,
    pub speed: f32,
    pub looping: bool,
    pub playing: bool,
    pub blend_from_clip: Option<usize>,
    pub blend_factor: f32,
}
}

blend_from_clip and blend_factor are the crossfade state used by blend_to. When blend_from_clip is Some, the system samples both clips and lerps the result by blend_factor. See the blending chapter for details.

Basic Playback

#![allow(unused)]
fn main() {
fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.play(clip_index);
        player.looping = true;
    }
}
}

play sets current_clip, resets time to zero, and sets playing to true. Setting looping separately controls whether the clip wraps or stops at the end.

Controlling Speed

#![allow(unused)]
fn main() {
fn set_animation_speed(world: &mut World, entity: Entity, speed: f32) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.speed = speed;
    }
}
}

Speed is a multiplier on the time advance. 1.0 is normal, 0.5 is half, 2.0 is double. Negative values play in reverse.

Pausing and Resuming

#![allow(unused)]
fn main() {
fn pause_animation(world: &mut World, entity: Entity) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.pause();
    }
}

fn resume_animation(world: &mut World, entity: Entity) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.resume();
    }
}
}

Pause stops the time from advancing but leaves the current pose frozen. Resume picks up where it left off.

Looping

#![allow(unused)]
fn main() {
fn set_looping(world: &mut World, entity: Entity, looping: bool) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.looping = looping;
    }
}
}

A non-looping clip stops at its duration and stays on the final pose. A looping clip wraps the time modulo duration.

Checking Animation State

For one-shot animations the caller often needs to know when the clip has finished playing.

#![allow(unused)]
fn main() {
fn is_animation_finished(world: &World, entity: Entity) -> bool {
    if let Some(player) = world.core.get_animation_player(entity) {
        if !player.looping {
            if let Some(index) = player.current_clip {
                let clip = &player.clips[index];
                return player.time >= clip.duration;
            }
        }
    }
    false
}

fn get_animation_progress(world: &World, entity: Entity) -> f32 {
    if let Some(player) = world.core.get_animation_player(entity) {
        if let Some(index) = player.current_clip {
            let clip = &player.clips[index];
            return player.time / clip.duration;
        }
    }
    0.0
}
}

get_animation_progress returns a normalized [0, 1] value useful for driving UI like an attack windup bar.

Animation by Name

#![allow(unused)]
fn main() {
fn play_animation_by_name(world: &mut World, entity: Entity, name: &str) -> bool {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        for (index, clip) in player.clips.iter().enumerate() {
            if clip.name.to_lowercase().contains(&name.to_lowercase()) {
                player.play(index);
                return true;
            }
        }
    }
    false
}
}

Name lookup is O(N) over the clips array. For tight loops, resolve indices once at load and store them on a per-character struct.

State-Based Animation

A character typically has an animation state machine driven by gameplay state. The cleanest pattern is to map each state to a clip index and call blend_to whenever the state changes.

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq)]
enum MovementState {
    Idle,
    Walking,
    Running,
    Jumping,
}

fn update_character_animation(
    world: &mut World,
    entity: Entity,
    state: MovementState,
    indices: &AnimationIndices,
    current: &mut Option<usize>,
) {
    let target = match state {
        MovementState::Idle => indices.idle,
        MovementState::Walking => indices.walk,
        MovementState::Running => indices.run,
        MovementState::Jumping => indices.jump,
    };

    if target != *current {
        if let Some(index) = target {
            if let Some(player) = world.core.get_animation_player_mut(entity) {
                player.blend_to(index, 0.2);
                *current = Some(index);
            }
        }
    }
}
}

The if target != *current guard is important. Calling blend_to every frame restarts the crossfade and the result is a stuck pose.

Speed Based on Movement

Walk and run clips are authored at a fixed speed. Scaling the playback rate to match the character's actual velocity prevents foot sliding.

#![allow(unused)]
fn main() {
fn sync_animation_to_movement(world: &mut World, entity: Entity, velocity: f32) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        let base_speed = 3.0;
        player.speed = (velocity / base_speed).clamp(0.5, 2.0);
    }
}
}

The clamp keeps the animation from playing absurdly fast or slow. A character creeping at 0.1 m/s should not freeze, and a character sprinting at 30 m/s should not blur.

One-Shot Animations

#![allow(unused)]
fn main() {
fn play_once(world: &mut World, entity: Entity, clip_index: usize) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.play(clip_index);
        player.looping = false;
    }
}
}

Animation Events

Animation events are gameplay triggers fired at specific times within a clip. Footsteps, weapon swing sounds, hit detection windows. The technique is to compare the current time against an event time and the previous frame's time, and fire when the threshold is crossed.

#![allow(unused)]
fn main() {
fn check_animation_events(world: &World, entity: Entity, event_time: f32) -> bool {
    if let Some(player) = world.core.get_animation_player(entity) {
        let prev_time = player.time - world.resources.window.timing.delta_time * player.speed;
        prev_time < event_time && player.time >= event_time
    } else {
        false
    }
}

fn footstep_system(world: &mut World, character: Entity, footstep_source: Entity) {
    if check_animation_events(world, character, 0.3) || check_animation_events(world, character, 0.8) {
        if let Some(audio) = world.core.get_audio_source_mut(footstep_source) {
            audio.playing = true;
        }
    }
}
}

The cross-the-threshold check fires exactly once per pass. A naive time >= 0.3 check would fire every frame after the threshold and play the footstep on every subsequent frame.