bevy: triggering state transition from player input locks the game

Bevy version

c78b76bba8def0d72b579b4a06673843f32e8532

What you did

In my game, I have several screens which I can go through by clicking the mouse (with just_pressed on Input<MouseButton>). Each screen has its own state.

Very dumb example that reproduce my issue:

use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_state(AppState::State1)
        .add_system_set(SystemSet::on_enter(AppState::State1).with_system(enter_state.system()))
        .add_system_set(SystemSet::on_update(AppState::State1).with_system(next_state.system()))
        .add_system_set(SystemSet::on_enter(AppState::State2).with_system(enter_state.system()))
        .add_system_set(SystemSet::on_update(AppState::State2).with_system(next_state.system()))
        .run();
}

#[derive(Clone, Eq, PartialEq, Debug)]
enum AppState {
    State1,
    State2,
}

fn enter_state(state: Res<State<AppState>>) {
    eprintln!("entering {:?}", state.current());
}

fn next_state(mut state: ResMut<State<AppState>>, mouse_button_input: Res<Input<MouseButton>>) {
    if mouse_button_input.just_pressed(MouseButton::Left) {
        match state.current() {
            AppState::State1 => state.set_next(AppState::State2),
            AppState::State2 => state.set_next(AppState::State1),
        }
        .unwrap();
    }
}

What you expected to happen

One click to go from State1 to State2, then one click to go from State2 to State1.

What actually happened

On first click, it changes state in a loop and lock the game.

Additional information

Now that system set can rerun in same frame, input isn’t reset between two passes so just_pressed is always true

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 18 (16 by maintainers)

Commits related to this issue

Most upvoted comments

I just ran into this when porting to 0.5 and I have to say this is surprising behavior. My mental intuition was that states own the entire frame. So if a state transition happens, it doesn’t apply until the next frame.

There is no loop, but the gist of the problem is the same: state changes are processed faster than input, so if you’re consecutively switching several states based on an input edge you’re gonna experience this issue. I still don’t think this is something that needs fixing on Bevy side.

My solution would be a layer of indirection that converts inputs into app-specific events (the same layer would also handle keybindings). So, when a key or a button is down (or just pressed), instead of changing state an event is emitted, and the state change system consumes that event.

a workaround is to add a on_enter system for your states that just does:

fn reset_input(mut keyboard_input: ResMut<Input<KeyCode>>,) {
      keyboard_input.update();
}

I hope we can find a better fix 😄

The Bevy Cheatbook lists the following workaround.

“If you want to use Input<T> to trigger state transitions using a button/key press, you need to clear the input manually by calling .reset:”

fn esc_to_menu(
    mut keys: ResMut<Input<KeyCode>>,
    mut app_state: ResMut<State<AppState>>,
) {
    if keys.just_pressed(KeyCode::Escape) {
        app_state.set(AppState::MainMenu).unwrap();
        keys.reset(KeyCode::Escape);
    }
}