#[cfg(feature = "bevy_reflect")]1use bevy_ecs::reflect::ReflectComponent;2use bevy_ecs::{3component::Component,4entity::Entity,5entity_disabling::Disabled,6message::MessageReader,7query::Allow,8system::{Commands, Query},9};10#[cfg(feature = "bevy_reflect")]11use bevy_reflect::prelude::*;1213use crate::state::{StateTransitionEvent, States};1415/// Entities marked with this component will be removed16/// when the world's state of the matching type no longer matches the supplied value.17///18/// If you need to disable this behavior, add the attribute `#[states(scoped_entities = false)]` when deriving [`States`].19///20/// ```21/// use bevy_state::prelude::*;22/// use bevy_ecs::prelude::*;23/// use bevy_ecs::system::ScheduleSystem;24///25/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]26/// enum GameState {27/// #[default]28/// MainMenu,29/// SettingsMenu,30/// InGame,31/// }32///33/// # #[derive(Component)]34/// # struct Player;35///36/// fn spawn_player(mut commands: Commands) {37/// commands.spawn((38/// DespawnOnExit(GameState::InGame),39/// Player40/// ));41/// }42///43/// # struct AppMock;44/// # impl AppMock {45/// # fn init_state<S>(&mut self) {}46/// # fn add_systems<S, M>(&mut self, schedule: S, systems: impl IntoScheduleConfigs<ScheduleSystem, M>) {}47/// # }48/// # struct Update;49/// # let mut app = AppMock;50///51/// app.init_state::<GameState>();52/// app.add_systems(OnEnter(GameState::InGame), spawn_player);53/// ```54#[derive(Component, Clone)]55#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]56pub struct DespawnOnExit<S: States>(pub S);5758impl<S> Default for DespawnOnExit<S>59where60S: States + Default,61{62fn default() -> Self {63Self(S::default())64}65}6667/// Despawns entities marked with [`DespawnOnExit<S>`] when their state no68/// longer matches the world state.69///70/// If the entity has already been despawned no warning will be emitted.71pub fn despawn_entities_on_exit_state<S: States>(72mut commands: Commands,73mut transitions: MessageReader<StateTransitionEvent<S>>,74query: Query<(Entity, &DespawnOnExit<S>), Allow<Disabled>>,75) {76// We use the latest event, because state machine internals generate at most 177// transition event (per type) each frame. No event means no change happened78// and we skip iterating all entities.79let Some(transition) = transitions.read().last() else {80return;81};82if transition.entered == transition.exited {83return;84}85let Some(exited) = &transition.exited else {86return;87};88for (entity, binding) in &query {89if binding.0 == *exited {90commands.entity(entity).try_despawn();91}92}93}9495/// Entities marked with this component will be despawned96/// upon entering the given state.97///98/// ```99/// use bevy_state::prelude::*;100/// use bevy_ecs::{prelude::*, system::ScheduleSystem};101///102/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]103/// enum GameState {104/// #[default]105/// MainMenu,106/// SettingsMenu,107/// InGame,108/// }109///110/// # #[derive(Component)]111/// # struct Player;112///113/// fn spawn_player(mut commands: Commands) {114/// commands.spawn((115/// DespawnOnEnter(GameState::MainMenu),116/// Player117/// ));118/// }119///120/// # struct AppMock;121/// # impl AppMock {122/// # fn init_state<S>(&mut self) {}123/// # fn add_systems<S, M>(&mut self, schedule: S, systems: impl IntoScheduleConfigs<ScheduleSystem, M>) {}124/// # }125/// # struct Update;126/// # let mut app = AppMock;127///128/// app.init_state::<GameState>();129/// app.add_systems(OnEnter(GameState::InGame), spawn_player);130/// ```131#[derive(Component, Clone)]132#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]133pub struct DespawnOnEnter<S: States>(pub S);134135/// Despawns entities marked with [`DespawnOnEnter<S>`] when their state136/// matches the world state.137///138/// If the entity has already been despawned no warning will be emitted.139pub fn despawn_entities_on_enter_state<S: States>(140mut commands: Commands,141mut transitions: MessageReader<StateTransitionEvent<S>>,142query: Query<(Entity, &DespawnOnEnter<S>), Allow<Disabled>>,143) {144// We use the latest event, because state machine internals generate at most 1145// transition event (per type) each frame. No event means no change happened146// and we skip iterating all entities.147let Some(transition) = transitions.read().last() else {148return;149};150if transition.entered == transition.exited {151return;152}153let Some(entered) = &transition.entered else {154return;155};156for (entity, binding) in &query {157if binding.0 == *entered {158commands.entity(entity).try_despawn();159}160}161}162163#[cfg(test)]164mod tests {165use super::*;166167use bevy_app::App;168169use crate::{170app::{AppExtStates, StatesPlugin},171prelude::CommandsStatesExt,172};173174#[test]175fn despawn_on_exit_from_computed_state() {176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)]177enum State {178On,179Off,180}181182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]183struct ComputedState;184impl bevy_state::state::ComputedStates for ComputedState {185type SourceStates = State;186187fn compute(sources: Self::SourceStates) -> Option<Self> {188match sources {189State::On => Some(ComputedState),190State::Off => None,191}192}193}194195let mut app = App::new();196app.add_plugins(StatesPlugin);197198app.insert_state(State::On);199app.add_computed_state::<ComputedState>();200app.update();201202assert_eq!(203app.world()204.resource::<bevy_state::state::State<State>>()205.get(),206&State::On207);208assert_eq!(209app.world()210.resource::<bevy_state::state::State<ComputedState>>()211.get(),212&ComputedState213);214215let entity = app.world_mut().spawn(DespawnOnExit(ComputedState)).id();216assert!(app.world().get_entity(entity).is_ok());217218app.world_mut().commands().set_state(State::Off);219app.update();220221assert_eq!(222app.world()223.resource::<bevy_state::state::State<State>>()224.get(),225&State::Off226);227assert!(app228.world()229.get_resource::<bevy_state::state::State<ComputedState>>()230.is_none());231assert!(app.world().get_entity(entity).is_err());232}233}234235236