Bevy Version: | 0.13 | (outdated!) |
---|
As this page is outdated, please refer to Bevy's official migration guides while reading, to cover the differences: 0.13 to 0.14.
I apologize for the inconvenience. I will update the page as soon as I find the time.
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:
- A
StateTransitionEvent
event is sent. - The
OnExit(old_state)
schedule is run. - The
OnTransition { from: old_state, to: new_state }
schedule is run. - The
OnEnter(new_state)
schedule is run.
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.