From b993202d798c0a11e1557b7e2bedf9d764bedd2d Mon Sep 17 00:00:00 2001 From: AlephCubed <76791009+AlephCubed@users.noreply.github.com> Date: Sat, 31 May 2025 13:14:14 -0700 Subject: [PATCH] Refactor state scoped events to match entities. (#19435) This adds support for clearing events when **entering** a state (instead of just when exiting) and updates the names to match `DespawnOnExitState`. Before: ```rust app.add_state_scoped_event::(GameState::Play); ``` After: ```rust app .add_event::() .clear_events_on_exit_state::(GameState::Play); ``` --- crates/bevy_state/src/state_scoped_events.rs | 204 +++++++++++++++--- .../migration-guides/rename_StateScoped.md | 10 - .../migration-guides/rename_state_scoped.md | 20 ++ 3 files changed, 198 insertions(+), 36 deletions(-) delete mode 100644 release-content/migration-guides/rename_StateScoped.md create mode 100644 release-content/migration-guides/rename_state_scoped.md diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index 5dc242493e..b11d8e79df 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::HashMap; -use crate::state::{OnExit, StateTransitionEvent, States}; +use crate::state::{OnEnter, OnExit, StateTransitionEvent, States}; fn clear_event_queue(w: &mut World) { if let Some(mut queue) = w.get_resource_mut::>() { @@ -18,21 +18,35 @@ fn clear_event_queue(w: &mut World) { } } +#[derive(Copy, Clone)] +enum TransitionType { + OnExit, + OnEnter, +} + #[derive(Resource)] struct StateScopedEvents { - cleanup_fns: HashMap>, + /// Keeps track of which events need to be reset when the state is exited. + on_exit: HashMap>, + /// Keeps track of which events need to be reset when the state is entered. + on_enter: HashMap>, } impl StateScopedEvents { - fn add_event(&mut self, state: S) { - self.cleanup_fns - .entry(state) - .or_default() - .push(clear_event_queue::); + fn add_event(&mut self, state: S, transition_type: TransitionType) { + let map = match transition_type { + TransitionType::OnExit => &mut self.on_exit, + TransitionType::OnEnter => &mut self.on_enter, + }; + map.entry(state).or_default().push(clear_event_queue::); } - fn cleanup(&self, w: &mut World, state: S) { - let Some(fns) = self.cleanup_fns.get(&state) else { + fn cleanup(&self, w: &mut World, state: S, transition_type: TransitionType) { + let map = match transition_type { + TransitionType::OnExit => &self.on_exit, + TransitionType::OnEnter => &self.on_enter, + }; + let Some(fns) = map.get(&state) else { return; }; for callback in fns { @@ -44,12 +58,13 @@ impl StateScopedEvents { impl Default for StateScopedEvents { fn default() -> Self { Self { - cleanup_fns: HashMap::default(), + on_exit: HashMap::default(), + on_enter: HashMap::default(), } } } -fn cleanup_state_scoped_event( +fn clear_events_on_exit_state( mut c: Commands, mut transitions: EventReader>, ) { @@ -65,48 +80,185 @@ fn cleanup_state_scoped_event( c.queue(move |w: &mut World| { w.resource_scope::, ()>(|w, events| { - events.cleanup(w, exited); + events.cleanup(w, exited, TransitionType::OnExit); }); }); } -fn add_state_scoped_event_impl( +fn clear_events_on_enter_state( + mut c: Commands, + mut transitions: EventReader>, +) { + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited { + return; + } + let Some(entered) = transition.entered.clone() else { + return; + }; + + c.queue(move |w: &mut World| { + w.resource_scope::, ()>(|w, events| { + events.cleanup(w, entered, TransitionType::OnEnter); + }); + }); +} + +fn clear_events_on_state_transition( app: &mut SubApp, _p: PhantomData, state: S, + transition_type: TransitionType, ) { if !app.world().contains_resource::>() { app.init_resource::>(); } - app.add_event::(); app.world_mut() .resource_mut::>() - .add_event::(state.clone()); - app.add_systems(OnExit(state), cleanup_state_scoped_event::); + .add_event::(state.clone(), transition_type); + match transition_type { + TransitionType::OnExit => app.add_systems(OnExit(state), clear_events_on_exit_state::), + TransitionType::OnEnter => { + app.add_systems(OnEnter(state), clear_events_on_enter_state::) + } + }; } /// Extension trait for [`App`] adding methods for registering state scoped events. pub trait StateScopedEventsAppExt { - /// Adds an [`Event`] that is automatically cleaned up when leaving the specified `state`. + /// Clears an [`Event`] when exiting the specified `state`. /// - /// Note that event cleanup is ordered ambiguously relative to [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) - /// and [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity - /// cleanup and the [`OnExit`] schedule for the target state. All of these (state scoped - /// entities and events cleanup, and `OnExit`) occur within schedule [`StateTransition`](crate::prelude::StateTransition) + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, + /// and the [`OnExit`] schedule for the target state. + /// All of these (state scoped entities and events cleanup, and `OnExit`) + /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn add_state_scoped_event(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + + /// Clears an [`Event`] when entering the specified `state`. + /// + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, + /// and the [`OnEnter`] schedule for the target state. + /// All of these (state scoped entities and events cleanup, and `OnEnter`) + /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) + /// and system set `StateTransitionSystems::EnterSchedules`. + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; } impl StateScopedEventsAppExt for App { - fn add_state_scoped_event(&mut self, state: impl States) -> &mut Self { - add_state_scoped_event_impl(self.main_mut(), PhantomData::, state); + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition( + self.main_mut(), + PhantomData::, + state, + TransitionType::OnExit, + ); + self + } + + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition( + self.main_mut(), + PhantomData::, + state, + TransitionType::OnEnter, + ); self } } impl StateScopedEventsAppExt for SubApp { - fn add_state_scoped_event(&mut self, state: impl States) -> &mut Self { - add_state_scoped_event_impl(self, PhantomData::, state); + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnExit); + self + } + + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::StatesPlugin; + use bevy_state::prelude::*; + + #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)] + enum TestState { + #[default] + A, + B, + } + + #[derive(Event, Debug)] + struct StandardEvent; + + #[derive(Event, Debug)] + struct StateScopedEvent; + + #[test] + fn clear_event_on_exit_state() { + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.init_state::(); + + app.add_event::(); + app.add_event::() + .clear_events_on_exit_state::(TestState::A); + + app.world_mut().send_event(StandardEvent).unwrap(); + app.world_mut().send_event(StateScopedEvent).unwrap(); + assert!(!app.world().resource::>().is_empty()); + assert!(!app + .world() + .resource::>() + .is_empty()); + + app.world_mut() + .resource_mut::>() + .set(TestState::B); + app.update(); + + assert!(!app.world().resource::>().is_empty()); + assert!(app + .world() + .resource::>() + .is_empty()); + } + + #[test] + fn clear_event_on_enter_state() { + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.init_state::(); + + app.add_event::(); + app.add_event::() + .clear_events_on_enter_state::(TestState::B); + + app.world_mut().send_event(StandardEvent).unwrap(); + app.world_mut().send_event(StateScopedEvent).unwrap(); + assert!(!app.world().resource::>().is_empty()); + assert!(!app + .world() + .resource::>() + .is_empty()); + + app.world_mut() + .resource_mut::>() + .set(TestState::B); + app.update(); + + assert!(!app.world().resource::>().is_empty()); + assert!(app + .world() + .resource::>() + .is_empty()); + } +} diff --git a/release-content/migration-guides/rename_StateScoped.md b/release-content/migration-guides/rename_StateScoped.md deleted file mode 100644 index e5aea31f4d..0000000000 --- a/release-content/migration-guides/rename_StateScoped.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: `StateScoped` renamed to `DespawnOnExitState` -pull_requests: [18818] ---- - -Previously, Bevy provided the `StateScoped` component as a way to despawn an entity when **exiting** a state. - -However, it can also be useful to have the opposite behavior, where an entity is despawned when **entering** a state. This is now possible with the new `DespawnOnEnterState` component. - -To support despawning entities when entering a state, in Bevy 0.17 the `StateScoped` component was renamed to `DespawnOnExitState` and `clear_state_scoped_entities` was renamed to `despawn_entities_on_exit_state`. Replace all references and imports. diff --git a/release-content/migration-guides/rename_state_scoped.md b/release-content/migration-guides/rename_state_scoped.md new file mode 100644 index 0000000000..bf8dd42454 --- /dev/null +++ b/release-content/migration-guides/rename_state_scoped.md @@ -0,0 +1,20 @@ +--- +title: Renamed state scoped entities and events +pull_requests: [18818, 19435] +--- + +Previously, Bevy provided the `StateScoped` component and `add_state_scoped_event` method +as a way to remove entities/events when **exiting** a state. + +However, it can also be useful to have the opposite behavior, +where entities/events are removed when **entering** a state. +This is now possible with the new `DespawnOnEnterState` component and `clear_events_on_enter_state` method. + +To support this addition, the previous method and component have been renamed. +Also, `clear_event_on_exit_state` no longer adds the event automatically, so you must call `App::add_event` manually. + +| Before | After | +|-------------------------------|--------------------------------------------| +| `StateScoped` | `DespawnOnExitState` | +| `clear_state_scoped_entities` | `despawn_entities_on_exit_state` | +| `add_state_scoped_event` | `add_event` + `clear_events_on_exit_state` |