Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bmanturner opened this issue Sep 1, 2022 · 7 comments
Open
Labels
enhancement New feature or request up-for-grabs The maintener is unlikely to work on this anytime soon. If you want it, build it!

Comments

@bmanturner
Copy link

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

@jcornaz
Copy link
Owner

jcornaz commented Sep 1, 2022

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.

@bmanturner
Copy link
Author

bmanturner commented Sep 1, 2022

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

@jcornaz
Copy link
Owner

jcornaz commented Sep 1, 2022

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

@bmanturner
Copy link
Author

bmanturner commented Sep 1, 2022

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
Copy link
Author

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();
        }
    }
}

@jcornaz
Copy link
Owner

jcornaz commented Sep 15, 2022

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.

@bmanturner
Copy link
Author

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.

@jcornaz jcornaz added the enhancement New feature or request label Oct 24, 2022
@jcornaz jcornaz added the up-for-grabs The maintener is unlikely to work on this anytime soon. If you want it, build it! label Dec 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request up-for-grabs The maintener is unlikely to work on this anytime soon. If you want it, build it!
Projects
None yet
Development

No branches or pull requests

2 participants