
# Objective - Alternative to and builds on top of #16284. - Fixes #15849. ## Solution - Rename component `StateScoped` to `DespawnOnExitState`. - Rename system `clear_state_scoped_entities` to `despawn_entities_on_exit_state`. - Add `DespawnOnEnterState` and `despawn_entities_on_enter_state` which is the `OnEnter` equivalent. > [!NOTE] > Compared to #16284, the main change is that I did the rename in such a way as to keep the terms `OnExit` and `OnEnter` together. In my own game, I was adding `VisibleOnEnterState` and `HiddenOnExitState` and when naming those, I kept the `OnExit` and `OnEnter` together. When I checked #16284 it stood out to me that the naming was a bit awkward. Putting the `State` in the middle and breaking up `OnEnter` and `OnExit` also breaks searching for those terms. ## Open questions 1. Should we split `enable_state_scoped_entities` into two functions, one for the `OnEnter` and one for the `OnExit`? I personally have zero need thus far for the `OnEnter` version, so I'd be interested in not having this enabled unless I ask for it. 2. If yes to 1., should we follow my lead in my `Visibility` state components (see below) and name these `app.enable_despawn_entities_on_enter_state()` and `app.enable_despawn_entities_on_exit_state()`, which IMO says what it does on the tin? ## Testing Ran all changed examples. ## Side note: `VisibleOnEnterState` and `HiddenOnExitState` For reference to anyone else and to help with the open questions, I'm including the code I wrote for controlling entity visibility when a state is entered/exited. <details> <summary>visibility.rs</summary> ```rust use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_state::{prelude::*, state::StateTransitionSteps}; use tracing::*; pub trait AppExtStates { fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self; fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self; } impl AppExtStates for App { fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self { self.main_mut() .enable_visible_entities_on_enter_state::<S>(); self } fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self { self.main_mut().enable_hidden_entities_on_exit_state::<S>(); self } } impl AppExtStates for SubApp { fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self { if !self .world() .contains_resource::<Events<StateTransitionEvent<S>>>() { let name = core::any::type_name::<S>(); warn!("Visible entities on enter state are enabled for state `{}`, but the state isn't installed in the app!", name); } // We work with [`StateTransition`] in set // [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`], // because [`OnExit`] only runs for one specific variant of the state. self.add_systems( StateTransition, update_to_visible_on_enter_state::<S>.in_set(StateTransitionSteps::ExitSchedules), ) } fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self { if !self .world() .contains_resource::<Events<StateTransitionEvent<S>>>() { let name = core::any::type_name::<S>(); warn!("Hidden entities on exit state are enabled for state `{}`, but the state isn't installed in the app!", name); } // We work with [`StateTransition`] in set // [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`], // because [`OnExit`] only runs for one specific variant of the state. self.add_systems( StateTransition, update_to_hidden_on_exit_state::<S>.in_set(StateTransitionSteps::ExitSchedules), ) } } #[derive(Clone, Component, Debug, Reflect)] #[reflect(Component, Debug)] pub struct VisibleOnEnterState<S: States>(pub S); #[derive(Clone, Component, Debug, Reflect)] #[reflect(Component, Debug)] pub struct HiddenOnExitState<S: States>(pub S); /// Makes entities marked with [`VisibleOnEnterState<S>`] visible when the state /// `S` is entered. pub fn update_to_visible_on_enter_state<S: States>( mut transitions: EventReader<StateTransitionEvent<S>>, mut query: Query<(&VisibleOnEnterState<S>, &mut Visibility)>, ) { // We use the latest event, because state machine internals generate at most // 1 transition event (per type) each frame. No event means no change // happened and we skip iterating all entities. let Some(transition) = transitions.read().last() else { return; }; if transition.entered == transition.exited { return; } let Some(entered) = &transition.entered else { return; }; for (binding, mut visibility) in query.iter_mut() { if binding.0 == *entered { visibility.set_if_neq(Visibility::Visible); } } } /// Makes entities marked with [`HiddenOnExitState<S>`] invisible when the state /// `S` is exited. pub fn update_to_hidden_on_exit_state<S: States>( mut transitions: EventReader<StateTransitionEvent<S>>, mut query: Query<(&HiddenOnExitState<S>, &mut Visibility)>, ) { // We use the latest event, because state machine internals generate at most // 1 transition event (per type) each frame. No event means no change // happened and we skip iterating all entities. let Some(transition) = transitions.read().last() else { return; }; if transition.entered == transition.exited { return; } let Some(exited) = &transition.exited else { return; }; for (binding, mut visibility) in query.iter_mut() { if binding.0 == *exited { visibility.set_if_neq(Visibility::Hidden); } } } ``` </details> --------- Co-authored-by: Benjamin Brienen <Benjamin.Brienen@outlook.com> Co-authored-by: Ben Frankel <ben.frankel7@gmail.com>
128 lines
3.3 KiB
Rust
128 lines
3.3 KiB
Rust
//! Shows how to spawn entities that are automatically despawned either when
|
|
//! entering or exiting specific game states.
|
|
//!
|
|
//! This pattern is useful for managing menus, levels, or other state-specific
|
|
//! content that should only exist during certain states.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.init_state::<GameState>()
|
|
.enable_state_scoped_entities::<GameState>()
|
|
.add_systems(Startup, setup_camera)
|
|
.add_systems(OnEnter(GameState::A), on_a_enter)
|
|
.add_systems(OnEnter(GameState::B), on_b_enter)
|
|
.add_systems(OnExit(GameState::A), on_a_exit)
|
|
.add_systems(OnExit(GameState::B), on_b_exit)
|
|
.add_systems(Update, toggle)
|
|
.insert_resource(TickTock(Timer::from_seconds(1.0, TimerMode::Repeating)))
|
|
.run();
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
|
|
enum GameState {
|
|
#[default]
|
|
A,
|
|
B,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct TickTock(Timer);
|
|
|
|
fn on_a_enter(mut commands: Commands) {
|
|
info!("on_a_enter");
|
|
commands.spawn((
|
|
DespawnOnExitState(GameState::A),
|
|
Text::new("Game is in state 'A'"),
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.5, 0.5, 1.0)),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(0.0),
|
|
left: Val::Px(0.0),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn on_a_exit(mut commands: Commands) {
|
|
info!("on_a_exit");
|
|
commands.spawn((
|
|
DespawnOnEnterState(GameState::A),
|
|
Text::new("Game state 'A' will be back in 1 second"),
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.5, 0.5, 1.0)),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(0.0),
|
|
left: Val::Px(500.0),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn on_b_enter(mut commands: Commands) {
|
|
info!("on_b_enter");
|
|
commands.spawn((
|
|
DespawnOnExitState(GameState::B),
|
|
Text::new("Game is in state 'B'"),
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.5, 0.5, 1.0)),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(50.0),
|
|
left: Val::Px(0.0),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn on_b_exit(mut commands: Commands) {
|
|
info!("on_b_exit");
|
|
commands.spawn((
|
|
DespawnOnEnterState(GameState::B),
|
|
Text::new("Game state 'B' will be back in 1 second"),
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.5, 0.5, 1.0)),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(50.0),
|
|
left: Val::Px(500.0),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn setup_camera(mut commands: Commands) {
|
|
commands.spawn(Camera3d::default());
|
|
}
|
|
|
|
fn toggle(
|
|
time: Res<Time>,
|
|
mut timer: ResMut<TickTock>,
|
|
state: Res<State<GameState>>,
|
|
mut next_state: ResMut<NextState<GameState>>,
|
|
) {
|
|
if !timer.0.tick(time.delta()).finished() {
|
|
return;
|
|
}
|
|
*next_state = match state.get() {
|
|
GameState::A => NextState::Pending(GameState::B),
|
|
GameState::B => NextState::Pending(GameState::A),
|
|
}
|
|
}
|