Fix: DespawnOnEnter/OnExit/When can trigger for same state transitions (#23390)

# Objective

- Fixes #23071
- Also allow state scoped messages to clear on same state transitions if
desired.

## Solution

- Add a `&& !transition.allow_same_state_transitions` check whenever the
`entered` state and `exited` state are compared. The
`allow_same_state_transitions` property is available from
`StateTransitionEvent`. This extra comparison is already being used for
`OnEnter` and `OnExit`.

## Testing

- Added a unit test to see that DespawnOnExit runs during same state
transitions if desired, since the logic is basically the same for all
three Despawn* Components.
- Ran the `state_scoped` example and everything looks ok
This commit is contained in:
Kevin Chen
2026-03-18 11:52:18 -04:00
committed by GitHub
parent 71cfd3ec01
commit 5ad5c9c028
3 changed files with 70 additions and 5 deletions
+58 -3
View File
@@ -93,7 +93,7 @@ pub fn despawn_entities_when_state<S: States>(
let Some(transition) = transitions.read().last() else {
return;
};
if transition.entered == transition.exited {
if transition.entered == transition.exited && !transition.allow_same_state_transitions {
return;
}
for (entity, when) in &query {
@@ -171,7 +171,7 @@ pub fn despawn_entities_on_exit_state<S: States>(
let Some(transition) = transitions.read().last() else {
return;
};
if transition.entered == transition.exited {
if transition.entered == transition.exited && !transition.allow_same_state_transitions {
return;
}
let Some(exited) = &transition.exited else {
@@ -249,7 +249,7 @@ pub fn despawn_entities_on_enter_state<S: States>(
let Some(transition) = transitions.read().last() else {
return;
};
if transition.entered == transition.exited {
if transition.entered == transition.exited && !transition.allow_same_state_transitions {
return;
}
let Some(entered) = &transition.entered else {
@@ -332,4 +332,59 @@ mod tests {
.is_none());
assert!(app.world().get_entity(entity).is_err());
}
#[test]
fn despawn_on_exit_same_state_transition() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)]
enum State {
On,
}
let mut app = App::new();
app.add_plugins(StatesPlugin);
app.insert_state(State::On);
app.update();
assert_eq!(
app.world()
.resource::<bevy_state::state::State<State>>()
.get(),
&State::On
);
let entity = app.world_mut().spawn(DespawnOnExit(State::On)).id();
assert!(app.world().get_entity(entity).is_ok());
app.world_mut().commands().set_state(State::On);
app.update();
assert_eq!(
app.world()
.resource::<bevy_state::state::State<State>>()
.get(),
&State::On
);
// entity was despawned on exit, despite setting the state to the same state.
// this is because "set_state" runs state transitions even if
// the next state and the previous are equal.
assert!(app.world().get_entity(entity).is_err());
let entity = app.world_mut().spawn(DespawnOnExit(State::On)).id();
assert!(app.world().get_entity(entity).is_ok());
app.world_mut().commands().set_state_if_neq(State::On);
app.update();
assert_eq!(
app.world()
.resource::<bevy_state::state::State<State>>()
.get(),
&State::On
);
// entity was not despawned on exit
// this is because "set_state_if_neq" skips state transitions since
// the app's next state is the same as its previous.
assert!(app.world().get_entity(entity).is_ok());
}
}
+2 -2
View File
@@ -71,7 +71,7 @@ fn clear_messages_on_exit<S: States>(
let Some(transition) = transitions.read().last() else {
return;
};
if transition.entered == transition.exited {
if transition.entered == transition.exited && !transition.allow_same_state_transitions {
return;
}
let Some(exited) = transition.exited.clone() else {
@@ -92,7 +92,7 @@ fn clear_messages_on_enter<S: States>(
let Some(transition) = transitions.read().last() else {
return;
};
if transition.entered == transition.exited {
if transition.entered == transition.exited && !transition.allow_same_state_transitions {
return;
}
let Some(entered) = transition.entered.clone() else {
@@ -0,0 +1,10 @@
---
title: "`DespawnOnEnter` / `DespawnOnExit` can now trigger during same state transitions"
pull_requests: [23390]
---
`DespawnOnEnter` and `DespawnOnExit` can now trigger on entities with those components during same state transitions.
If your application transitions between states using `NextState::set()`, your application will trigger `DespawnOnEnter` and `DespawnOnExit` during same state transitions.
If this is undesired, use `NextState::set_if_neq()` instead to transition between states. `set_if_neq()` does not run any state transition schedules if the target state is the same as the current one.