benimator icon indicating copy to clipboard operation
benimator copied to clipboard

[QOL] Specify an animation to run after a `Once` animation finishes

Open bmanturner opened this issue 1 year ago • 7 comments

Would be great to queue up a new animation, or specify a default animation to run once one finishes

bmanturner avatar Sep 01 '22 01:09 bmanturner

Yeah, definitely, that sounds like a good idea.

Maybe can you share more details about your current use-case? So that I can try thinking about an API.

jcornaz avatar Sep 01 '22 07:09 jcornaz

So imagine you're making a tile-based game. You're gonna be swapping the animations a lot.

  1. standing still, idle_down on repeat
  2. walking left, swap idle_down for walk_left on repeat
  3. reach destination tile, swap walk_left for idle_left on repeat, etc

In some cases, like an attack, you'll play the attack animation once and then return to the animation you were playing before you attacked.

Here's some musing:

An AnimationChain and AnimationChainBuilder

let animation_chain = AnimationChainBuilder::new()
                                        .begins_with(attack_animation)
                                        .then(stow_weapon_animation)
                                        .then(idle_animation);

And then that allows you to expand the AnimationChainBuilder to support options other than begins_with and then, and have maybe play_n_times(3, animation), then_in_reverse(animation_to_play_backwards) or more.

Or maybe something like iyes_loopless where you specify the animation to run given a particular animation state enum value.

let animation_states = AnimationStatesBuilder::new()
                                        .run_in_state(EntityAnimState::IdleDown, idle_down_animation)
                                        .run_in_state(EntityAnimState::WalkLeft, walk_left_animation)
                                        .run_in_state(EntityAnimState::AttackLeft, attack_left_animation);

let animation_state = State::new(EntityAnimState::IdleDown, animation_chain);

animation_state.now(EntityAnimState::WalkLeft); // change animation

animation_state.now(EntityAnimState::AttackLeft).next(EntityAnimState::IdleLeft);

I hope this helps! Let me know if you want to chat about it

bmanturner avatar Sep 01 '22 13:09 bmanturner

Yes, it helps, thanks. I will think more about it and try to come up with an initial design.

jcornaz avatar Sep 01 '22 17:09 jcornaz

I've chosen to use benimator (at the very least in it's current form) in a game I'm working on. I would be glad to give feedback on any design ideas you come up with regarding switching between animations. Thanks Jonathan

bmanturner avatar Sep 01 '22 20:09 bmanturner

Using bevy-simple-state-machine as a reference I was able to implement something that works for benimator over the weekend

Here's the crux of the code. Also, state_machine.animation_finished() checks benimator to see if a Once animation has finished

pub fn check_animation_transitions(
    mut state_machines_query: Query<(Entity, &mut AnimationStateMachine)>,
    mut event_writer: EventWriter<TransitionEndedEvent>,
    animations: Res<Assets<Animation>>,
) {
    for (entity, mut state_machine) in &mut state_machines_query {
        if let Some(current_state) = state_machine.current_state() {
            if current_state.interruptible || state_machine.animation_finished() {
                for transition in state_machine.transitions_from_current_state() {
                    if transition.trigger.evaluate(&state_machine.variables) {
                        if let Some(next_state) =
                            state_machine.get_state(transition.end_state.unwrap())
                        {
                            println!("triggering {}", transition);
                            state_machine.current_state = next_state.name;

                            // Get new animation and restart benimator with it
                            let animation = match animations.get(&next_state.base_animation) {
                                Some(anim) => anim,
                                None => continue,
                            };
                            state_machine.base_frame.reset();
                            state_machine
                                .base_frame
                                .update(animation, Duration::from_millis(0));

                            // emit event that the transition has completed
                            event_writer.send(TransitionEndedEvent {
                                entity,
                                origin: current_state.state_ref(),
                                end: transition.end_state,
                            });
                        }
                    }
                }
            }
        }
    }
}

pub fn update_animations(
    time: Res<Time>,
    mut state_machines_query: Query<(&mut AnimationStateMachine, &mut TextureAtlasSprite)>,
    animations: Res<Assets<Animation>>,
) {
    for (mut state_machine, mut sprite_texture) in &mut state_machines_query {
        if let Some(current_state) = state_machine.current_state() {
            // Get animation and update it
            let animation = match animations.get(&current_state.base_animation) {
                Some(anim) => anim,
                None => continue,
            };

            state_machine.base_frame.update(animation, time.delta());

            sprite_texture.index = state_machine.base_frame.frame_index();
        }
    }
}

bmanturner avatar Sep 14 '22 19:09 bmanturner

Thanks @bmanturner, I'll have to look into this.

Right of the bat, I would like to make sure you're aware that benimator is no longer a bevy plugin. The bevy systems you just shared are still interesting to look at, and it is indeed worthwhile to think about how usage from bevy would look like. I will definitely study what you shared. I just want to make sure you're aware I am looking for an API that is agnostic of bevy.

jcornaz avatar Sep 15 '22 08:09 jcornaz

Yeah I dig. Aside from those systems the rest of the code in the repo is agnostic. Worth a look!

I put my code here because I’ve seen this issue linked to in bevy discords more than once so I think it might help people that come here and are wondering about implementing that functionality.

bmanturner avatar Sep 15 '22 12:09 bmanturner