Description
What I did
I have a state that holds information about the general game state (being in main menu, splash screen, main game etc).
Some resources in my application are only relevant in that specific state, so i want to remove them when they are not needed
I thought the StateTransition
schedule is where the new state gets set, so i can put any system that removes / inserts the relevant resources there
What went wrong
Sometimes, the StateTransition schedule gets run 1 frame later than the global state is set (i think), so any systems relying on state specific resources will cause the game to crash
in this example, it will just print an error to the console
Workarounds
I could insert a custom resource that handles the game state and transitions, but what is the point of bevys own system feature then?
I dont understand what the point of a dedicated StateTransition
schedule is, if not for things like this
Code
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, sys_start_transition)
.add_systems(StateTransition, sys_do_transition)
.add_systems(FixedUpdate, sys_main_menu.run_if(in_main_menu))
.add_systems(FixedUpdate, sys_in_game.run_if(in_game))
.init_state::<GameState>()
.run();
}
#[derive(Clone, Copy, PartialEq, Eq, States, Debug, Hash, Default)]
enum GameState {
#[default]
Init,
MainMenu,
InGame,
}
/// key input to transition to a new state
fn sys_start_transition(
mut transition: ResMut<NextState<GameState>>,
mut commands: Commands,
current: Res<State<GameState>>,
keys: Res<ButtonInput<KeyCode>>,
) {
let current = **current;
let next = match current {
GameState::Init => GameState::MainMenu,
GameState::MainMenu => GameState::InGame,
GameState::InGame => GameState::MainMenu,
};
// the issue occures no matter if the transition is triggered with Commands or NextState
if keys.just_pressed(KeyCode::Space) {
commands.set_state(next);
} else if keys.just_pressed(KeyCode::Enter) {
transition.set(next);
} else {
return;
};
info!("transition from {current:?} to {next:?}");
}
/// remove and insert the state specific resources
fn sys_do_transition(
mut commands: Commands,
mut events: EventReader<StateTransitionEvent<GameState>>,
) {
for event in events.read() {
let old = event.exited;
let new = event.entered;
// remove old state
match old {
Some(GameState::Init) => (),
Some(GameState::MainMenu) => commands.remove_resource::<MainMenuData>(),
Some(GameState::InGame) => commands.remove_resource::<InGameData>(),
None => (),
}
// insert new state
match new {
Some(GameState::Init) => (),
Some(GameState::MainMenu) => commands.init_resource::<MainMenuData>(),
Some(GameState::InGame) => commands.init_resource::<InGameData>(),
None => (),
}
}
}
#[derive(Default, Resource)]
struct MainMenuData;
#[derive(Default, Resource)]
struct InGameData;
/// example system that gets run in the game
fn sys_in_game(data: Option<ResMut<InGameData>>) {
if data.is_none() {
error!("InGameData cannot be found!!")
}
}
/// example system that gets run in the main menu
fn sys_main_menu(data: Option<ResMut<MainMenuData>>) {
if data.is_none() {
error!("MainMenuData cannot be found!!")
}
}
fn in_main_menu(state: Res<State<GameState>>) -> bool {
matches!(**state, GameState::MainMenu)
}
fn in_game(state: Res<State<GameState>>) -> bool {
matches!(**state, GameState::InGame)
}
Bevy version
i tested this on 0.15.0, 0.15.3, 0.16.1, and the main branch