Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/bevy_state/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -210,6 +211,7 @@ impl AppExtStates for SubApp {
{
self.register_type::<S>();
self.register_type::<State<S>>();
self.register_type::<PreviousState<S>>();
self.register_type_data::<S, crate::reflect::ReflectState>();
self
}
Expand All @@ -222,6 +224,7 @@ impl AppExtStates for SubApp {
self.register_type::<S>();
self.register_type::<State<S>>();
self.register_type::<NextState<S>>();
self.register_type::<PreviousState<S>>();
self.register_type_data::<S, crate::reflect::ReflectState>();
self.register_type_data::<S, crate::reflect::ReflectFreelyMutableState>();
self
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_state/src/state/freely_mutable_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>`](crate::state::NextState) resource.
///
Expand Down Expand Up @@ -50,6 +50,7 @@ fn apply_state_transition<S: FreelyMutableState>(
event: MessageWriter<StateTransitionEvent<S>>,
commands: Commands,
current_state: Option<ResMut<State<S>>>,
previous_state: Option<ResMut<PreviousState<S>>>,
next_state: Option<ResMut<NextState<S>>>,
) {
let Some((next_state, same_state_enforced)) = take_next_state(next_state) else {
Expand All @@ -62,6 +63,7 @@ fn apply_state_transition<S: FreelyMutableState>(
event,
commands,
Some(current_state),
previous_state,
Some(next_state),
same_state_enforced,
);
Expand Down
57 changes: 57 additions & 0 deletions crates/bevy_state/src/state/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,63 @@ impl<S: States> Deref for State<S> {
}
}

/// The previous state of [`State<S>`].
///
/// 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<Res<PreviousState<S>>>` 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<Res<PreviousState<GameState>>>) {
/// 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<S: States>(pub(crate) S);

impl<S: States> PreviousState<S> {
/// Get the previous state.
pub fn get(&self) -> &S {
&self.0
}
}

impl<S: States> Deref for PreviousState<S> {
type Target = S;

fn deref(&self) -> &Self::Target {
&self.0
}
}

/// The next state of [`State<S>`].
///
/// This can be fetched as a resource and used to queue state transitions.
Expand Down
22 changes: 17 additions & 5 deletions crates/bevy_state/src/state/state_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -99,6 +99,7 @@ impl<S: InnerStateSet> StateSet for S {
event: MessageWriter<StateTransitionEvent<T>>,
commands: Commands,
current_state: Option<ResMut<State<T>>>,
previous_state: Option<ResMut<PreviousState<T>>>,
state_set: Option<Res<State<S::RawState>>>| {
if parent_changed.is_empty() {
return;
Expand All @@ -112,7 +113,14 @@ impl<S: InnerStateSet> 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((
Expand Down Expand Up @@ -170,6 +178,7 @@ impl<S: InnerStateSet> StateSet for S {
event: MessageWriter<StateTransitionEvent<T>>,
commands: Commands,
current_state_res: Option<ResMut<State<T>>>,
previous_state: Option<ResMut<PreviousState<T>>>,
next_state_res: Option<ResMut<NextState<T>>>,
state_set: Option<Res<State<S::RawState>>>| {
let parent_changed = parent_changed.read().last().is_some();
Expand Down Expand Up @@ -207,6 +216,7 @@ impl<S: InnerStateSet> StateSet for S {
event,
commands,
current_state_res,
previous_state,
new_state,
same_state_enforced,
);
Expand Down Expand Up @@ -264,6 +274,7 @@ macro_rules! impl_state_set_sealed_tuples {
message: MessageWriter<StateTransitionEvent<T>>,
commands: Commands,
current_state: Option<ResMut<State<T>>>,
previous_state: Option<ResMut<PreviousState<T>>>,
($($val),*,): ($(Option<Res<State<$param::RawState>>>),*,)| {
if ($($evt.is_empty())&&*) {
return;
Expand All @@ -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((
Expand Down Expand Up @@ -308,6 +319,7 @@ macro_rules! impl_state_set_sealed_tuples {
message: MessageWriter<StateTransitionEvent<T>>,
commands: Commands,
current_state_res: Option<ResMut<State<T>>>,
previous_state: Option<ResMut<PreviousState<T>>>,
next_state_res: Option<ResMut<NextState<T>>>,
($($val),*,): ($(Option<Res<State<$param::RawState>>>),*,)| {
let parent_changed = ($($evt.read().last().is_some())||*);
Expand Down Expand Up @@ -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((
Expand Down
33 changes: 30 additions & 3 deletions crates/bevy_state/src/state/transitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>`] enters the provided state.
///
Expand Down Expand Up @@ -136,6 +139,7 @@ pub(crate) fn internal_apply_state_transition<S: States>(
mut event: MessageWriter<StateTransitionEvent<S>>,
mut commands: Commands,
current_state: Option<ResMut<State<S>>>,
mut previous_state: Option<ResMut<PreviousState<S>>>,
new_state: Option<S>,
same_state_enforced: bool,
) {
Expand All @@ -158,6 +162,12 @@ pub(crate) fn internal_apply_state_transition<S: States>(
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<S>`] resource does not exist, we create it, compute dependent states, send a transition event and register the `OnEnter` schedule.
Expand All @@ -168,19 +178,30 @@ pub(crate) fn internal_apply_state_transition<S: States>(
entered: Some(entered.clone()),
same_state_enforced,
});

if previous_state.is_some() {
commands.remove_resource::<PreviousState<S>>();
}
}
};
}
None => {
// We first remove the [`State<S>`] 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::<State<S>>();

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));
}
}
}
}
Expand All @@ -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);
}

Expand Down