States

Relevant official examples: state.


States allow you to structure the runtime "flow" of your app.

This is how you can implement things like:

  • A menu screen or a loading screen
  • Pausing / unpausing the game
  • Different game modes

In every state, you can have different systems running. You can also add setup and cleanup systems to run when entering or exiting a state.


To use states, first define an enum type. You need to derive States + an assortment of required standard Rust traits:

#[derive(States, Debug, Clone, PartialEq, Eq, Hash)]
enum MyAppState {
    LoadingScreen,
    MainMenu,
    InGame,
}

#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum MyGameModeState {
    #[default]
    NotInGame,
    Singleplayer,
    Multiplayer,
}

#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum MyPausedState {
    #[default]
    Paused,
    Running,
}

Note: you can have multiple orthogonal states! Create multiple types if you want to track multiple things independently!

You then need to register the state type(s) in the app builder:

// Specify the initial value:
app.insert_state(MyAppState::LoadingScreen);

// Or use the default (if the type impls Default):
app.init_state::<MyGameModeState>();
app.init_state::<MyPausedState>();

Running Different Systems for Different States

If you want some systems to only run in specific states, Bevy offers an in_state run condition. Add it to your systems. You probably want to create system sets to help you group many systems and control them at once.

// configure some system sets to help us manage our systems
// (note: it is per-schedule, so we also need it for FixedUpdate
// if we plan to use fixed timestep)
app.configure_sets(Update, (
    MyMainMenuSet
        .run_if(in_state(MyAppState::MainMenu)),
    MyGameplaySet
        // note: you can check for a combination of different states
        .run_if(in_state(MyAppState::InGame))
        .run_if(in_state(MyPausedState::Running)),
));
app.configure_sets(FixedUpdate, (
    // configure the same set here, so we can use it in both
    // FixedUpdate and Update
    MyGameplaySet
        .run_if(in_state(MyAppState::InGame))
        .run_if(in_state(MyPausedState::Running)),
    // configure a bunch of different sets only for FixedUpdate
    MySingleplayerSet
        // inherit configuration from MyGameplaySet and add extras
        .in_set(MyGameplaySet)
        .run_if(in_state(MyGameModeState::Singleplayer)),
    MyMultiplayerSet
        .in_set(MyGameplaySet)
        .run_if(in_state(MyGameModeState::Multiplayer)),
));

// now we can easily add our different systems
app.add_systems(Update, (
    update_loading_progress_bar
        .run_if(in_state(MyAppState::LoadingScreen)),
    (
        handle_main_menu_ui_input,
        play_main_menu_sounds,
    ).in_set(MyMainMenuSet),
    (
        camera_movement,
        play_game_music,
    ).in_set(MyGameplaySet),
));
app.add_systems(FixedUpdate, (
    (
        player_movement,
        enemy_ai,
    ).in_set(MySingleplayerSet),
    (
        player_net_sync,
        enemy_net_sync,
    ).in_set(MyMultiplayerSet),
));

// of course, if we need some global (state-independent)
// setup to run on app startup, we can still use Startup as usual
app.add_systems(Startup, (
    load_settings,
    setup_window_icon,
));

Bevy also creates special OnEnter, OnExit, and OnTransition schedules for each possible value of your state type. Use them to perform setup and cleanup for specific states. Any systems you add to them will run once every time the state is changed to/from the respective values.

// do the respective setup and cleanup on state transitions
app.add_systems(OnEnter(MyAppState::LoadingScreen), (
    start_load_assets,
    spawn_progress_bar,
));
app.add_systems(OnExit(MyAppState::LoadingScreen), (
    despawn_loading_screen,
));
app.add_systems(OnEnter(MyAppState::MainMenu), (
    setup_main_menu_ui,
    setup_main_menu_camera,
));
app.add_systems(OnExit(MyAppState::MainMenu), (
    despawn_main_menu,
));
app.add_systems(OnEnter(MyAppState::InGame), (
    spawn_game_map,
    setup_game_camera,
    spawn_enemies,
));
app.add_systems(OnEnter(MyGameModeState::Singleplayer), (
    setup_singleplayer,
));
app.add_systems(OnEnter(MyGameModeState::Multiplayer), (
    setup_multiplayer,
));
// ...

With Plugins

This can also be useful with Plugins. You can set up all the state types for your project in one place, and then your different plugins can just add their systems to the relevant states.

You can also make plugins that are configurable, so that it is possible to specify what state they should add their systems to:

pub struct MyPlugin<S: States> {
    pub state: S,
}

impl<S: States> Plugin for MyPlugin<S> {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, (
            my_plugin_system1,
            my_plugin_system2,
            // ...
        ).run_if(in_state(self.state.clone())));
    }
}

Now you can configure the plugin when adding it to the app:

app.add_plugins(MyPlugin {
    state: MyAppState::InGame,
});

When you are just using plugins to help with internal organization of your project, and you know what systems should go into each state, you probably don't need to bother with making the plugin configurable as shown above. Just hardcode the states / add things to the correct states directly.

Controlling States

Inside of systems, you can check the current state using the State<T> resource:

fn debug_current_gamemode_state(state: Res<State<MyGameModeState>>) {
    eprintln!("Current state: {:?}", state.get());
}

To change to another state, you can use the NextState<T>:

fn toggle_pause_game(
    state: Res<State<MyPausedState>>,
    mut next_state: ResMut<NextState<MyPausedState>>,
) {
    match state.get() {
        MyPausedState::Paused => next_state.set(MyPausedState::Running),
        MyPausedState::Running => next_state.set(MyPausedState::Paused),
    }
}

// if you have multiple states that must be set correctly,
// don't forget to manage them all
fn new_game_multiplayer(
    mut next_app: ResMut<NextState<MyAppState>>,
    mut next_mode: ResMut<NextState<MyGameModeState>>,
) {
    next_app.set(MyAppState::InGame);
    next_mode.set(MyGameModeState::Multiplayer);
}

This will queue up state transitions to be performed during the next frame update cycle.

State Transitions

Every frame update, a schedule called StateTransition runs. There, Bevy will check if any new state is queued up in NextState<T> and perform the transition for you.

The transition involves several steps:

StateTransitionEvent is useful in any systems that run regardless of state, but want to know if a transition has occurred. You can use it to detect state transitions.

The StateTransition schedule runs after PreUpdate (which contains Bevy engine internals), but before FixedMain (fixed timestep) and Update, where your game's systems usually live.

Therefore, state transitions happen before your game logic for the current frame.

If doing state transitions once per frame is not enough for you, you can add additional transition points, by adding Bevy's apply_state_transition system wherever you like.

// Example: also do state transitions for MyPausedState
// before MyGameplaySet on each fixed timestep run
app.add_systems(
    FixedUpdate,
    apply_state_transition::<MyPausedState>
        .before(MyGameplaySet)
);

Known Pitfalls

System set configuration is per-schedule!

This is the same general caveat that applies any time you configure system sets.

Note that app.configure_sets() is per-schedule! If you configure some sets in one schedule, that configuration does not carry over to other schedules.

Because states are so schedule-heavy, you have to be especially careful. Don't assume that just because you configured a set, you can use it anywhere.

For example, your sets from Update and FixedUpdate will not work in OnEnter/OnExit for your various state transitions.

Events

This is the same general caveat that applies to any systems with run conditions that want to receive events.

When receiving events in systems that don't run all the time, such as during a pause state, you will miss any events that are sent while when the receiving systems are not running!

To mitigate this, you could implement a custom cleanup strategy, to manually manage the lifetime of the relevant event types.