diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index a22ce701c842c..c304408d7b15d 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -5,8 +5,9 @@ use log::warn; use crate::{ state::{ - setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, State, - StateTransition, StateTransitionEvent, StateTransitionSystems, States, SubStates, + setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, + PreviousState, State, StateTransition, StateTransitionEvent, StateTransitionSystems, + States, SubStates, }, state_scoped::{despawn_entities_on_enter_state, despawn_entities_on_exit_state}, }; @@ -210,6 +211,7 @@ impl AppExtStates for SubApp { { self.register_type::(); self.register_type::>(); + self.register_type::>(); self.register_type_data::(); self } @@ -222,6 +224,7 @@ impl AppExtStates for SubApp { self.register_type::(); self.register_type::>(); self.register_type::>(); + self.register_type::>(); self.register_type_data::(); self.register_type_data::(); self diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 31e8b5bea8aac..ddbdbf2581bfc 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -89,8 +89,8 @@ pub mod prelude { condition::*, state::{ last_transition, ComputedStates, EnterSchedules, ExitSchedules, NextState, OnEnter, - OnExit, OnTransition, State, StateSet, StateTransition, StateTransitionEvent, States, - SubStates, TransitionSchedules, + OnExit, OnTransition, PreviousState, State, StateSet, StateTransition, + StateTransitionEvent, States, SubStates, TransitionSchedules, }, state_scoped::{DespawnOnEnter, DespawnOnExit}, }; diff --git a/crates/bevy_state/src/state/freely_mutable_state.rs b/crates/bevy_state/src/state/freely_mutable_state.rs index b4fd26481e62c..054396d2e50c6 100644 --- a/crates/bevy_state/src/state/freely_mutable_state.rs +++ b/crates/bevy_state/src/state/freely_mutable_state.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ system::{Commands, IntoSystem, ResMut}, }; -use super::{states::States, take_next_state, transitions::*, NextState, State}; +use super::{states::States, take_next_state, transitions::*, NextState, PreviousState, State}; /// This trait allows a state to be mutated directly using the [`NextState`](crate::state::NextState) resource. /// @@ -50,6 +50,7 @@ fn apply_state_transition( event: MessageWriter>, commands: Commands, current_state: Option>>, + previous_state: Option>>, next_state: Option>>, ) { let Some((next_state, same_state_enforced)) = take_next_state(next_state) else { @@ -62,6 +63,7 @@ fn apply_state_transition( event, commands, Some(current_state), + previous_state, Some(next_state), same_state_enforced, ); diff --git a/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs index 0f5f0246289c7..e6c4d9ae8f11c 100644 --- a/crates/bevy_state/src/state/resources.rs +++ b/crates/bevy_state/src/state/resources.rs @@ -91,6 +91,63 @@ impl Deref for State { } } +/// The previous state of [`State`]. +/// +/// This resource holds the state value that was active immediately **before** the +/// most recent state transition. It is primarily useful for logic that runs +/// during state exit or transition schedules ([`OnExit`](crate::state::OnExit), [`OnTransition`](crate::state::OnTransition)). +/// +/// It is inserted into the world only after the first state transition occurs. It will +/// remain present even if the primary state is removed (e.g., when a +/// [`SubStates`](crate::state::SubStates) or [`ComputedStates`](crate::state::ComputedStates) instance ceases to exist). +/// +/// Use `Option>>` to access it, as it will not exist +/// before the first transition. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// use bevy_state_macros::States; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// } +/// +/// // This system might run in an OnExit schedule +/// fn log_previous_state(previous_state: Option>>) { +/// if let Some(previous) = previous_state { +/// // If this system is in OnExit(InGame), the previous state is what we +/// // were in before InGame. +/// println!("Transitioned from: {:?}", previous.get()); +/// } +/// } +/// ``` +#[derive(Resource, Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource, Debug, PartialEq) +)] +pub struct PreviousState(pub(crate) S); + +impl PreviousState { + /// Get the previous state. + pub fn get(&self) -> &S { + &self.0 + } +} + +impl Deref for PreviousState { + type Target = S; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// The next state of [`State`]. /// /// This can be fetched as a resource and used to queue state transitions. diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index b850af83ddd89..753f05967c3dd 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -10,8 +10,8 @@ use self::sealed::StateSetSealed; use super::{ computed_states::ComputedStates, internal_apply_state_transition, last_transition, run_enter, run_exit, run_transition, sub_states::SubStates, take_next_state, ApplyStateTransition, - EnterSchedules, ExitSchedules, NextState, State, StateTransitionEvent, StateTransitionSystems, - States, TransitionSchedules, + EnterSchedules, ExitSchedules, NextState, PreviousState, State, StateTransitionEvent, + StateTransitionSystems, States, TransitionSchedules, }; mod sealed { @@ -99,6 +99,7 @@ impl StateSet for S { event: MessageWriter>, commands: Commands, current_state: Option>>, + previous_state: Option>>, state_set: Option>>| { if parent_changed.is_empty() { return; @@ -112,7 +113,14 @@ impl StateSet for S { None }; - internal_apply_state_transition(event, commands, current_state, new_state, false); + internal_apply_state_transition( + event, + commands, + current_state, + previous_state, + new_state, + false, + ); }; schedule.configure_sets(( @@ -170,6 +178,7 @@ impl StateSet for S { event: MessageWriter>, commands: Commands, current_state_res: Option>>, + previous_state: Option>>, next_state_res: Option>>, state_set: Option>>| { let parent_changed = parent_changed.read().last().is_some(); @@ -207,6 +216,7 @@ impl StateSet for S { event, commands, current_state_res, + previous_state, new_state, same_state_enforced, ); @@ -264,6 +274,7 @@ macro_rules! impl_state_set_sealed_tuples { message: MessageWriter>, commands: Commands, current_state: Option>>, + previous_state: Option>>, ($($val),*,): ($(Option>>),*,)| { if ($($evt.is_empty())&&*) { return; @@ -276,7 +287,7 @@ macro_rules! impl_state_set_sealed_tuples { None }; - internal_apply_state_transition(message, commands, current_state, new_state, false); + internal_apply_state_transition(message, commands, current_state, previous_state, new_state, false); }; schedule.configure_sets(( @@ -308,6 +319,7 @@ macro_rules! impl_state_set_sealed_tuples { message: MessageWriter>, commands: Commands, current_state_res: Option>>, + previous_state: Option>>, next_state_res: Option>>, ($($val),*,): ($(Option>>),*,)| { let parent_changed = ($($evt.read().last().is_some())||*); @@ -342,7 +354,7 @@ macro_rules! impl_state_set_sealed_tuples { .unwrap_or(x) }); - internal_apply_state_transition(message, commands, current_state_res, new_state, same_state_enforced); + internal_apply_state_transition(message, commands, current_state_res, previous_state, new_state, same_state_enforced); }; schedule.configure_sets(( diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index 35d421bf0f711..ef24229f68366 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -2,12 +2,15 @@ use core::{marker::PhantomData, mem}; use bevy_ecs::{ message::{Message, MessageReader, MessageWriter}, - schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, + schedule::{ApplyDeferred, IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, }; -use super::{resources::State, states::States}; +use super::{ + resources::{PreviousState, State}, + states::States, +}; /// The label of a [`Schedule`] that **only** runs whenever [`State`] enters the provided state. /// @@ -136,6 +139,7 @@ pub(crate) fn internal_apply_state_transition( mut event: MessageWriter>, mut commands: Commands, current_state: Option>>, + mut previous_state: Option>>, new_state: Option, same_state_enforced: bool, ) { @@ -158,6 +162,12 @@ pub(crate) fn internal_apply_state_transition( entered: Some(entered.clone()), same_state_enforced, }); + + if let Some(ref mut previous_state) = previous_state { + previous_state.0 = exited; + } else { + commands.insert_resource(PreviousState(exited)); + } } None => { // If the [`State`] resource does not exist, we create it, compute dependent states, send a transition event and register the `OnEnter` schedule. @@ -168,19 +178,30 @@ pub(crate) fn internal_apply_state_transition( entered: Some(entered.clone()), same_state_enforced, }); + + if previous_state.is_some() { + commands.remove_resource::>(); + } } }; } None => { // We first remove the [`State`] resource, and if one existed we compute dependent states, send a transition event and run the `OnExit` schedule. if let Some(resource) = current_state { + let exited = resource.get().clone(); commands.remove_resource::>(); event.write(StateTransitionEvent { - exited: Some(resource.get().clone()), + exited: Some(exited.clone()), entered: None, same_state_enforced, }); + + if let Some(ref mut previous_state) = previous_state { + previous_state.0 = exited; + } else { + commands.insert_resource(PreviousState(exited)); + } } } } @@ -206,6 +227,12 @@ pub fn setup_state_transitions_in_world(world: &mut World) { ) .chain(), ); + schedule.add_systems( + ApplyDeferred + .after(StateTransitionSystems::DependentTransitions) + .before(StateTransitionSystems::ExitSchedules), + ); + schedules.insert(schedule); }