Skip to content

Commit f8a9f29

Browse files
authored
Add PreviousState Resource accessible in OnEnter (#21995)
# Objective - Adds a `PreviousState` resource to track the last active state, making it accessible during state transitions (e.g., in `OnEnter` schedules). - Fixes #21882. ## Solution - Added a new `PreviousState<S: States>` resource. - Updated `internal_apply_state_transition` to manage the lifecycle of `PreviousState`: - Standard Transition: Updates `PreviousState` to the exited state. - Creation: If a state is initialized (transition from None), any existing `PreviousState` is removed. - Removal: If a state is removed, `PreviousState` is updated to preserve the last known state. - Registered `PreviousState` in `AppExtStates` for reflection and general access. - Ensured `PreviousState` is available for `OnEnter` systems by using `ApplyDeferred` ordering in `setup_state_transitions_in_world`. ## Testing - Added a temporary local unit test previous_state_is_tracked_correctly which verifies that `PreviousState` is created, updated, and persisted correctly across multiple state transitions. - The implementation covered scenarios including standard transitions, state initialization (where no previous state exists), and state removal. - Don't think I handled all edge cases, would appreciate a second look. --- ## Showcase You can now access the state you just transitioned from directly in your systems! ``` fn handle_state_refresh<S: FreelyMutableState>( prev_state: Option<Res<PreviousState<S>>>, mut next_state: ResMut<NextState<S>> ) { if let Some(prev) = prev_state { println!("We just came from {:?}", prev.0); // Example: revert to previous state next_state.set(prev.0.clone()); } } app.add_systems(OnEnter(GameState::Refresh), handle_state_refresh::<GameState>); ``` <sub>*This is my first contribution, would highly appreciate some feedback!*</sub>
1 parent 196e84d commit f8a9f29

File tree

6 files changed

+103
-11
lines changed

6 files changed

+103
-11
lines changed

crates/bevy_state/src/app.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use log::warn;
55

66
use crate::{
77
state::{
8-
setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, State,
9-
StateTransition, StateTransitionEvent, StateTransitionSystems, States, SubStates,
8+
setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState,
9+
PreviousState, State, StateTransition, StateTransitionEvent, StateTransitionSystems,
10+
States, SubStates,
1011
},
1112
state_scoped::{despawn_entities_on_enter_state, despawn_entities_on_exit_state},
1213
};
@@ -215,6 +216,7 @@ impl AppExtStates for SubApp {
215216
{
216217
self.register_type::<S>();
217218
self.register_type::<State<S>>();
219+
self.register_type::<PreviousState<S>>();
218220
self.register_type_data::<S, crate::reflect::ReflectState>();
219221
self
220222
}
@@ -227,6 +229,7 @@ impl AppExtStates for SubApp {
227229
self.register_type::<S>();
228230
self.register_type::<State<S>>();
229231
self.register_type::<NextState<S>>();
232+
self.register_type::<PreviousState<S>>();
230233
self.register_type_data::<S, crate::reflect::ReflectState>();
231234
self.register_type_data::<S, crate::reflect::ReflectFreelyMutableState>();
232235
self

crates/bevy_state/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ pub mod prelude {
8989
condition::*,
9090
state::{
9191
last_transition, ComputedStates, EnterSchedules, ExitSchedules, NextState, OnEnter,
92-
OnExit, OnTransition, State, StateSet, StateTransition, StateTransitionEvent, States,
93-
SubStates, TransitionSchedules,
92+
OnExit, OnTransition, PreviousState, State, StateSet, StateTransition,
93+
StateTransitionEvent, States, SubStates, TransitionSchedules,
9494
},
9595
state_scoped::{DespawnOnEnter, DespawnOnExit},
9696
};

crates/bevy_state/src/state/freely_mutable_state.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bevy_ecs::{
55
system::{Commands, IntoSystem, ResMut},
66
};
77

8-
use super::{states::States, take_next_state, transitions::*, NextState, State};
8+
use super::{states::States, take_next_state, transitions::*, NextState, PreviousState, State};
99

1010
/// This trait allows a state to be mutated directly using the [`NextState<S>`](crate::state::NextState) resource.
1111
///
@@ -50,6 +50,7 @@ fn apply_state_transition<S: FreelyMutableState>(
5050
event: MessageWriter<StateTransitionEvent<S>>,
5151
commands: Commands,
5252
current_state: Option<ResMut<State<S>>>,
53+
previous_state: Option<ResMut<PreviousState<S>>>,
5354
next_state: Option<ResMut<NextState<S>>>,
5455
) {
5556
let Some((next_state, allow_same_state_transitions)) = take_next_state(next_state) else {
@@ -62,6 +63,7 @@ fn apply_state_transition<S: FreelyMutableState>(
6263
event,
6364
commands,
6465
Some(current_state),
66+
previous_state,
6567
Some(next_state),
6668
allow_same_state_transitions,
6769
);

crates/bevy_state/src/state/resources.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,63 @@ impl<S: States> Deref for State<S> {
9191
}
9292
}
9393

94+
/// The previous state of [`State<S>`].
95+
///
96+
/// This resource holds the state value that was active immediately **before** the
97+
/// most recent state transition. It is primarily useful for logic that runs
98+
/// during state exit or transition schedules ([`OnExit`](crate::state::OnExit), [`OnTransition`](crate::state::OnTransition)).
99+
///
100+
/// It is inserted into the world only after the first state transition occurs. It will
101+
/// remain present even if the primary state is removed (e.g., when a
102+
/// [`SubStates`](crate::state::SubStates) or [`ComputedStates`](crate::state::ComputedStates) instance ceases to exist).
103+
///
104+
/// Use `Option<Res<PreviousState<S>>>` to access it, as it will not exist
105+
/// before the first transition.
106+
///
107+
/// ```
108+
/// use bevy_state::prelude::*;
109+
/// use bevy_ecs::prelude::*;
110+
/// use bevy_state_macros::States;
111+
///
112+
/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
113+
/// enum GameState {
114+
/// #[default]
115+
/// MainMenu,
116+
/// InGame,
117+
/// }
118+
///
119+
/// // This system might run in an OnExit schedule
120+
/// fn log_previous_state(previous_state: Option<Res<PreviousState<GameState>>>) {
121+
/// if let Some(previous) = previous_state {
122+
/// // If this system is in OnExit(InGame), the previous state is what we
123+
/// // were in before InGame.
124+
/// println!("Transitioned from: {:?}", previous.get());
125+
/// }
126+
/// }
127+
/// ```
128+
#[derive(Resource, Debug, Clone, PartialEq, Eq)]
129+
#[cfg_attr(
130+
feature = "bevy_reflect",
131+
derive(bevy_reflect::Reflect),
132+
reflect(Resource, Debug, PartialEq)
133+
)]
134+
pub struct PreviousState<S: States>(pub(crate) S);
135+
136+
impl<S: States> PreviousState<S> {
137+
/// Get the previous state.
138+
pub fn get(&self) -> &S {
139+
&self.0
140+
}
141+
}
142+
143+
impl<S: States> Deref for PreviousState<S> {
144+
type Target = S;
145+
146+
fn deref(&self) -> &Self::Target {
147+
&self.0
148+
}
149+
}
150+
94151
/// The next state of [`State<S>`].
95152
///
96153
/// This can be fetched as a resource and used to queue state transitions.

crates/bevy_state/src/state/state_set.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use self::sealed::StateSetSealed;
1010
use super::{
1111
computed_states::ComputedStates, internal_apply_state_transition, last_transition, run_enter,
1212
run_exit, run_transition, sub_states::SubStates, take_next_state, ApplyStateTransition,
13-
EnterSchedules, ExitSchedules, NextState, State, StateTransitionEvent, StateTransitionSystems,
14-
States, TransitionSchedules,
13+
EnterSchedules, ExitSchedules, NextState, PreviousState, State, StateTransitionEvent,
14+
StateTransitionSystems, States, TransitionSchedules,
1515
};
1616

1717
mod sealed {
@@ -99,6 +99,7 @@ impl<S: InnerStateSet> StateSet for S {
9999
event: MessageWriter<StateTransitionEvent<T>>,
100100
commands: Commands,
101101
current_state: Option<ResMut<State<T>>>,
102+
previous_state: Option<ResMut<PreviousState<T>>>,
102103
state_set: Option<Res<State<S::RawState>>>| {
103104
if parent_changed.is_empty() {
104105
return;
@@ -116,6 +117,7 @@ impl<S: InnerStateSet> StateSet for S {
116117
event,
117118
commands,
118119
current_state,
120+
previous_state,
119121
new_state,
120122
T::ALLOW_SAME_STATE_TRANSITIONS,
121123
);
@@ -176,6 +178,7 @@ impl<S: InnerStateSet> StateSet for S {
176178
event: MessageWriter<StateTransitionEvent<T>>,
177179
commands: Commands,
178180
current_state_res: Option<ResMut<State<T>>>,
181+
previous_state: Option<ResMut<PreviousState<T>>>,
179182
next_state_res: Option<ResMut<NextState<T>>>,
180183
state_set: Option<Res<State<S::RawState>>>| {
181184
let parent_changed = parent_changed.read().last().is_some();
@@ -213,6 +216,7 @@ impl<S: InnerStateSet> StateSet for S {
213216
event,
214217
commands,
215218
current_state_res,
219+
previous_state,
216220
new_state,
217221
same_state_enforced,
218222
);
@@ -270,6 +274,7 @@ macro_rules! impl_state_set_sealed_tuples {
270274
message: MessageWriter<StateTransitionEvent<T>>,
271275
commands: Commands,
272276
current_state: Option<ResMut<State<T>>>,
277+
previous_state: Option<ResMut<PreviousState<T>>>,
273278
($($val),*,): ($(Option<Res<State<$param::RawState>>>),*,)| {
274279
if ($($evt.is_empty())&&*) {
275280
return;
@@ -282,7 +287,7 @@ macro_rules! impl_state_set_sealed_tuples {
282287
None
283288
};
284289

285-
internal_apply_state_transition(message, commands, current_state, new_state, false);
290+
internal_apply_state_transition(message, commands, current_state, previous_state, new_state, false);
286291
};
287292

288293
schedule.configure_sets((
@@ -314,6 +319,7 @@ macro_rules! impl_state_set_sealed_tuples {
314319
message: MessageWriter<StateTransitionEvent<T>>,
315320
commands: Commands,
316321
current_state_res: Option<ResMut<State<T>>>,
322+
previous_state: Option<ResMut<PreviousState<T>>>,
317323
next_state_res: Option<ResMut<NextState<T>>>,
318324
($($val),*,): ($(Option<Res<State<$param::RawState>>>),*,)| {
319325
let parent_changed = ($($evt.read().last().is_some())||*);
@@ -348,7 +354,7 @@ macro_rules! impl_state_set_sealed_tuples {
348354
.unwrap_or(x)
349355
});
350356

351-
internal_apply_state_transition(message, commands, current_state_res, new_state, same_state_enforced);
357+
internal_apply_state_transition(message, commands, current_state_res, previous_state, new_state, same_state_enforced);
352358
};
353359

354360
schedule.configure_sets((

crates/bevy_state/src/state/transitions.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use bevy_ecs::{
77
world::World,
88
};
99

10-
use super::{resources::State, states::States};
10+
use super::{
11+
resources::{PreviousState, State},
12+
states::States,
13+
};
1114

1215
/// The label of a [`Schedule`] that **only** runs whenever [`State<S>`] enters the provided state.
1316
///
@@ -136,6 +139,7 @@ pub(crate) fn internal_apply_state_transition<S: States>(
136139
mut event: MessageWriter<StateTransitionEvent<S>>,
137140
mut commands: Commands,
138141
current_state: Option<ResMut<State<S>>>,
142+
mut previous_state: Option<ResMut<PreviousState<S>>>,
139143
new_state: Option<S>,
140144
allow_same_state_transitions: bool,
141145
) {
@@ -158,6 +162,12 @@ pub(crate) fn internal_apply_state_transition<S: States>(
158162
entered: Some(entered.clone()),
159163
allow_same_state_transitions,
160164
});
165+
166+
if let Some(ref mut previous_state) = previous_state {
167+
previous_state.0 = exited;
168+
} else {
169+
commands.insert_resource(PreviousState(exited));
170+
}
161171
}
162172
None => {
163173
// If the [`State<S>`] resource does not exist, we create it, compute dependent states, send a transition event and register the `OnEnter` schedule.
@@ -168,19 +178,32 @@ pub(crate) fn internal_apply_state_transition<S: States>(
168178
entered: Some(entered.clone()),
169179
allow_same_state_transitions,
170180
});
181+
182+
// When [`State<S>`] is initialized, there can be stale data in
183+
// [`PreviousState<S>`] from a prior transition to `None`, so we remove it.
184+
if previous_state.is_some() {
185+
commands.remove_resource::<PreviousState<S>>();
186+
}
171187
}
172188
};
173189
}
174190
None => {
175191
// We first remove the [`State<S>`] resource, and if one existed we compute dependent states, send a transition event and run the `OnExit` schedule.
176192
if let Some(resource) = current_state {
193+
let exited = resource.get().clone();
177194
commands.remove_resource::<State<S>>();
178195

179196
event.write(StateTransitionEvent {
180-
exited: Some(resource.get().clone()),
197+
exited: Some(exited.clone()),
181198
entered: None,
182199
allow_same_state_transitions,
183200
});
201+
202+
if let Some(ref mut previous_state) = previous_state {
203+
previous_state.0 = exited;
204+
} else {
205+
commands.insert_resource(PreviousState(exited));
206+
}
184207
}
185208
}
186209
}
@@ -206,6 +229,7 @@ pub fn setup_state_transitions_in_world(world: &mut World) {
206229
)
207230
.chain(),
208231
);
232+
209233
schedules.insert(schedule);
210234
}
211235

0 commit comments

Comments
 (0)