use bevy_app::{App, MainScheduleOrder, Plugin, PreStartup, PreUpdate, SubApp}; use bevy_ecs::{event::Events, schedule::IntoScheduleConfigs, world::FromWorld}; use bevy_utils::once; use log::warn; use crate::{ state::{ setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, State, StateTransition, StateTransitionEvent, StateTransitionSteps, States, SubStates, }, state_scoped::clear_state_scoped_entities, }; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{FromReflect, GetTypeRegistration, Typed}; /// State installation methods for [`App`] and [`SubApp`]. pub trait AppExtStates { /// Initializes a [`State`] with standard starting values. /// /// This method is idempotent: it has no effect when called again using the same generic type. /// /// Adds [`State`] and [`NextState`] resources, and enables use of the [`OnEnter`](crate::state::OnEnter), /// [`OnTransition`](crate::state::OnTransition) and [`OnExit`](crate::state::OnExit) schedules. /// These schedules are triggered before [`Update`](bevy_app::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`Condition`](bevy_ecs::prelude::Condition). /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. /// /// The use of any states requires the presence of [`StatesPlugin`] (which is included in `DefaultPlugins`). fn init_state(&mut self) -> &mut Self; /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// added of the same type. /// /// Adds [`State`] and [`NextState`] resources, and enables use of the [`OnEnter`](crate::state::OnEnter), /// [`OnTransition`](crate::state::OnTransition) and [`OnExit`](crate::state::OnExit) schedules. /// These schedules are triggered before [`Update`](bevy_app::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`Condition`](bevy_ecs::prelude::Condition). /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. fn insert_state(&mut self, state: S) -> &mut Self; /// Sets up a type implementing [`ComputedStates`]. /// /// This method is idempotent: it has no effect when called again using the same generic type. fn add_computed_state(&mut self) -> &mut Self; /// Sets up a type implementing [`SubStates`]. /// /// This method is idempotent: it has no effect when called again using the same generic type. fn add_sub_state(&mut self) -> &mut Self; /// Enable state-scoped entity clearing for state `S`. /// /// If the [`States`] trait was derived with the `#[states(scoped_entities)]` attribute, it /// will be called automatically. /// /// For more information refer to [`StateScoped`](crate::state_scoped::StateScoped). fn enable_state_scoped_entities(&mut self) -> &mut Self; #[cfg(feature = "bevy_reflect")] /// Registers the state type `T` using [`App::register_type`], /// and adds [`ReflectState`](crate::reflect::ReflectState) type data to `T` in the type registry. /// /// This enables reflection code to access the state. For detailed information, see the docs on [`crate::reflect::ReflectState`] . fn register_type_state(&mut self) -> &mut Self where S: States + FromReflect + GetTypeRegistration + Typed; #[cfg(feature = "bevy_reflect")] /// Registers the state type `T` using [`App::register_type`], /// and adds [`crate::reflect::ReflectState`] and [`crate::reflect::ReflectFreelyMutableState`] type data to `T` in the type registry. /// /// This enables reflection code to access and modify the state. /// For detailed information, see the docs on [`crate::reflect::ReflectState`] and [`crate::reflect::ReflectFreelyMutableState`]. fn register_type_mutable_state(&mut self) -> &mut Self where S: FreelyMutableState + FromReflect + GetTypeRegistration + Typed; } /// Separate function to only warn once for all state installation methods. fn warn_if_no_states_plugin_installed(app: &SubApp) { if !app.is_plugin_added::() { once!(warn!( "States were added to the app, but `StatesPlugin` is not installed." )); } } impl AppExtStates for SubApp { fn init_state(&mut self) -> &mut Self { warn_if_no_states_plugin_installed(self); if !self.world().contains_resource::>() { self.init_resource::>() .init_resource::>() .add_event::>(); let schedule = self.get_schedule_mut(StateTransition).expect( "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling init_state?" ); S::register_state(schedule); let state = self.world().resource::>().get().clone(); self.world_mut().send_event(StateTransitionEvent { exited: None, entered: Some(state), }); if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { let name = core::any::type_name::(); warn!("State {} is already initialized.", name); } self } fn insert_state(&mut self, state: S) -> &mut Self { warn_if_no_states_plugin_installed(self); if !self.world().contains_resource::>() { self.insert_resource::>(State::new(state.clone())) .init_resource::>() .add_event::>(); let schedule = self.get_schedule_mut(StateTransition).expect( "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling insert_state?" ); S::register_state(schedule); self.world_mut().send_event(StateTransitionEvent { exited: None, entered: Some(state), }); if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { // Overwrite previous state and initial event self.insert_resource::>(State::new(state.clone())); self.world_mut() .resource_mut::>>() .clear(); self.world_mut().send_event(StateTransitionEvent { exited: None, entered: Some(state), }); } self } fn add_computed_state(&mut self) -> &mut Self { warn_if_no_states_plugin_installed(self); if !self .world() .contains_resource::>>() { self.add_event::>(); let schedule = self.get_schedule_mut(StateTransition).expect( "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling add_computed_state?" ); S::register_computed_state_systems(schedule); let state = self .world() .get_resource::>() .map(|s| s.get().clone()); self.world_mut().send_event(StateTransitionEvent { exited: None, entered: state, }); if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { let name = core::any::type_name::(); warn!("Computed state {} is already initialized.", name); } self } fn add_sub_state(&mut self) -> &mut Self { warn_if_no_states_plugin_installed(self); if !self .world() .contains_resource::>>() { self.init_resource::>(); self.add_event::>(); let schedule = self.get_schedule_mut(StateTransition).expect( "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling add_sub_state?" ); S::register_sub_state_systems(schedule); let state = self .world() .get_resource::>() .map(|s| s.get().clone()); self.world_mut().send_event(StateTransitionEvent { exited: None, entered: state, }); if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { let name = core::any::type_name::(); warn!("Sub state {} is already initialized.", name); } self } fn enable_state_scoped_entities(&mut self) -> &mut Self { if !self .world() .contains_resource::>>() { let name = core::any::type_name::(); warn!("State scoped entities 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, clear_state_scoped_entities::.in_set(StateTransitionSteps::ExitSchedules), ) } #[cfg(feature = "bevy_reflect")] fn register_type_state(&mut self) -> &mut Self where S: States + FromReflect + GetTypeRegistration + Typed, { self.register_type::(); self.register_type::>(); self.register_type_data::(); self } #[cfg(feature = "bevy_reflect")] fn register_type_mutable_state(&mut self) -> &mut Self where S: FreelyMutableState + FromReflect + GetTypeRegistration + Typed, { self.register_type::(); self.register_type::>(); self.register_type::>(); self.register_type_data::(); self.register_type_data::(); self } } impl AppExtStates for App { fn init_state(&mut self) -> &mut Self { self.main_mut().init_state::(); self } fn insert_state(&mut self, state: S) -> &mut Self { self.main_mut().insert_state::(state); self } fn add_computed_state(&mut self) -> &mut Self { self.main_mut().add_computed_state::(); self } fn add_sub_state(&mut self) -> &mut Self { self.main_mut().add_sub_state::(); self } fn enable_state_scoped_entities(&mut self) -> &mut Self { self.main_mut().enable_state_scoped_entities::(); self } #[cfg(feature = "bevy_reflect")] fn register_type_state(&mut self) -> &mut Self where S: States + FromReflect + GetTypeRegistration + Typed, { self.main_mut().register_type_state::(); self } #[cfg(feature = "bevy_reflect")] fn register_type_mutable_state(&mut self) -> &mut Self where S: FreelyMutableState + FromReflect + GetTypeRegistration + Typed, { self.main_mut().register_type_mutable_state::(); self } } /// Registers the [`StateTransition`] schedule in the [`MainScheduleOrder`] to enable state processing. #[derive(Default)] pub struct StatesPlugin; impl Plugin for StatesPlugin { fn build(&self, app: &mut App) { let mut schedule = app.world_mut().resource_mut::(); schedule.insert_after(PreUpdate, StateTransition); schedule.insert_startup_before(PreStartup, StateTransition); setup_state_transitions_in_world(app.world_mut()); } } #[cfg(test)] mod tests { use crate::{ app::StatesPlugin, state::{State, StateTransition, StateTransitionEvent}, }; use bevy_app::App; use bevy_ecs::event::Events; use bevy_state_macros::States; use super::AppExtStates; #[derive(States, Default, PartialEq, Eq, Hash, Debug, Clone)] enum TestState { #[default] A, B, C, } #[test] fn insert_state_can_overwrite_init_state() { let mut app = App::new(); app.add_plugins(StatesPlugin); app.init_state::(); app.insert_state(TestState::B); let world = app.world_mut(); world.run_schedule(StateTransition); assert_eq!(world.resource::>().0, TestState::B); let events = world.resource::>>(); assert_eq!(events.len(), 1); let mut reader = events.get_cursor(); let last = reader.read(events).last().unwrap(); assert_eq!(last.exited, None); assert_eq!(last.entered, Some(TestState::B)); } #[test] fn insert_state_can_overwrite_insert_state() { let mut app = App::new(); app.add_plugins(StatesPlugin); app.insert_state(TestState::B); app.insert_state(TestState::C); let world = app.world_mut(); world.run_schedule(StateTransition); assert_eq!(world.resource::>().0, TestState::C); let events = world.resource::>>(); assert_eq!(events.len(), 1); let mut reader = events.get_cursor(); let last = reader.read(events).last().unwrap(); assert_eq!(last.exited, None); assert_eq!(last.entered, Some(TestState::C)); } }