From da47daf5ada1a8d871c4e0769f0debffa4fd07fe Mon Sep 17 00:00:00 2001 From: Br3nnabee Date: Mon, 1 Dec 2025 07:58:34 +0100 Subject: [PATCH 1/4] Add PreviousState Resource accessible in OnEnter --- crates/bevy_state/src/app.rs | 4 +- crates/bevy_state/src/lib.rs | 4 +- .../src/state/freely_mutable_state.rs | 4 +- crates/bevy_state/src/state/resources.rs | 50 +++++++++++++++++++ crates/bevy_state/src/state/state_set.rs | 14 ++++-- crates/bevy_state/src/state/transitions.rs | 30 +++++++++-- 6 files changed, 95 insertions(+), 11 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index fb0bb6104366d..16fd47d53ebcd 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -5,7 +5,7 @@ use log::warn; use crate::{ state::{ - setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, State, + 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}, @@ -215,6 +215,7 @@ impl AppExtStates for SubApp { { self.register_type::(); self.register_type::>(); + self.register_type::>(); self.register_type_data::(); self } @@ -227,6 +228,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 d16cf8ea42147..a7b22e1ef8a29 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, allow_same_state_transitions)) = take_next_state(next_state) else { @@ -62,6 +63,7 @@ fn apply_state_transition( event, commands, Some(current_state), + previous_state, Some(next_state), allow_same_state_transitions, ); diff --git a/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs index e4376499f94e7..f47f1c2c91486 100644 --- a/crates/bevy_state/src/state/resources.rs +++ b/crates/bevy_state/src/state/resources.rs @@ -91,6 +91,56 @@ 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 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 a9589f495b509..73e1cbf3c328e 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; @@ -116,6 +117,7 @@ impl StateSet for S { event, commands, current_state, + previous_state, new_state, T::ALLOW_SAME_STATE_TRANSITIONS, ); @@ -176,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(); @@ -213,6 +216,7 @@ impl StateSet for S { event, commands, current_state_res, + previous_state, new_state, same_state_enforced, ); @@ -270,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; @@ -282,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(( @@ -314,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())||*); @@ -348,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 deb6a63bab152..ded872014f59b 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -2,12 +2,12 @@ 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 +136,7 @@ pub(crate) fn internal_apply_state_transition( mut event: MessageWriter>, mut commands: Commands, current_state: Option>>, + mut previous_state: Option>>, new_state: Option, allow_same_state_transitions: bool, ) { @@ -158,6 +159,12 @@ pub(crate) fn internal_apply_state_transition( entered: Some(entered.clone()), allow_same_state_transitions, }); + + 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 +175,30 @@ pub(crate) fn internal_apply_state_transition( entered: Some(entered.clone()), allow_same_state_transitions, }); + + 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, allow_same_state_transitions, }); + + if let Some(ref mut previous_state) = previous_state { + previous_state.0 = exited; + } else { + commands.insert_resource(PreviousState(exited)); + } } } } @@ -206,6 +224,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); } From 9bae6f3b012705fa3a8672ca9b21d32eb31d6380 Mon Sep 17 00:00:00 2001 From: Br3nnabee Date: Mon, 1 Dec 2025 07:59:04 +0100 Subject: [PATCH 2/4] Added public & fixed formatting --- crates/bevy_state/src/app.rs | 5 +++-- crates/bevy_state/src/state/resources.rs | 7 +++++++ crates/bevy_state/src/state/transitions.rs | 5 ++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 16fd47d53ebcd..3d79fd7e8a56c 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, PreviousState, 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}, }; diff --git a/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs index f47f1c2c91486..f82effd304d5d 100644 --- a/crates/bevy_state/src/state/resources.rs +++ b/crates/bevy_state/src/state/resources.rs @@ -133,6 +133,13 @@ impl Deref for State { )] 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; diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index ded872014f59b..f50690d721066 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -7,7 +7,10 @@ use bevy_ecs::{ world::World, }; -use super::{resources::{PreviousState, State},states::States}; +use super::{ + resources::{PreviousState, State}, + states::States, +}; /// The label of a [`Schedule`] that **only** runs whenever [`State`] enters the provided state. /// From eecfc4cedb2ba5e3a3b25cdccf2e74429e884fa4 Mon Sep 17 00:00:00 2001 From: Br3nnabee Date: Mon, 1 Dec 2025 07:59:06 +0100 Subject: [PATCH 3/4] Remove redundant ApplyDeferred --- crates/bevy_state/src/state/transitions.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index f50690d721066..f3237c7ca5946 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -2,7 +2,7 @@ use core::{marker::PhantomData, mem}; use bevy_ecs::{ message::{Message, MessageReader, MessageWriter}, - schedule::{ApplyDeferred, IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, + schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, }; @@ -227,11 +227,6 @@ pub fn setup_state_transitions_in_world(world: &mut World) { ) .chain(), ); - schedule.add_systems( - ApplyDeferred - .after(StateTransitionSystems::DependentTransitions) - .before(StateTransitionSystems::ExitSchedules), - ); schedules.insert(schedule); } From 6bf1bd001253f4b59498a91a77523571617e5d89 Mon Sep 17 00:00:00 2001 From: Br3nnabee Date: Wed, 17 Dec 2025 21:35:27 +0100 Subject: [PATCH 4/4] Added clarification comment about stale data in PreviousState --- crates/bevy_state/src/state/transitions.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index f3237c7ca5946..a0d0fb7ab9212 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -179,6 +179,8 @@ pub(crate) fn internal_apply_state_transition( allow_same_state_transitions, }); + // When [`State`] is initialized, there can be stale data in + // [`PreviousState`] from a prior transition to `None`, so we remove it. if previous_state.is_some() { commands.remove_resource::>(); }