Skip to content

Commit 0b802bd

Browse files
authored
enable same state transitions (bevyengine#19363)
# Objective - Same state transitions have their uses but are not currently possible ## Solution - Add a `set_forced` method on `NextState` that will trigger `OnEnter` and `OnExit` - Rerun state transitions when `set_forced` has been used - Rerun them is `set` is called *after* `set_forced` with the same state
1 parent 24255a9 commit 0b802bd

File tree

7 files changed

+105
-16
lines changed

7 files changed

+105
-16
lines changed

crates/bevy_dev_tools/src/states.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ pub fn log_transitions<S: States>(mut transitions: MessageReader<StateTransition
1313
return;
1414
};
1515
let name = core::any::type_name::<S>();
16-
let StateTransitionEvent { exited, entered } = transition;
17-
info!("{} transition: {:?} => {:?}", name, exited, entered);
16+
let StateTransitionEvent {
17+
exited,
18+
entered,
19+
same_state_enforced,
20+
} = transition;
21+
info!(
22+
"{} transition: {:?} => {:?} | same state enforced: {:?}",
23+
name, exited, entered, same_state_enforced
24+
);
1825
}

crates/bevy_state/src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ impl AppExtStates for SubApp {
101101
self.world_mut().write_message(StateTransitionEvent {
102102
exited: None,
103103
entered: Some(state),
104+
same_state_enforced: false,
104105
});
105106
enable_state_scoped_entities::<S>(self);
106107
} else {
@@ -124,6 +125,7 @@ impl AppExtStates for SubApp {
124125
self.world_mut().write_message(StateTransitionEvent {
125126
exited: None,
126127
entered: Some(state),
128+
same_state_enforced: false,
127129
});
128130
enable_state_scoped_entities::<S>(self);
129131
} else {
@@ -135,6 +137,7 @@ impl AppExtStates for SubApp {
135137
self.world_mut().write_message(StateTransitionEvent {
136138
exited: None,
137139
entered: Some(state),
140+
same_state_enforced: false,
138141
});
139142
}
140143

@@ -159,6 +162,7 @@ impl AppExtStates for SubApp {
159162
self.world_mut().write_message(StateTransitionEvent {
160163
exited: None,
161164
entered: state,
165+
same_state_enforced: false,
162166
});
163167
enable_state_scoped_entities::<S>(self);
164168
} else {
@@ -188,6 +192,7 @@ impl AppExtStates for SubApp {
188192
self.world_mut().write_message(StateTransitionEvent {
189193
exited: None,
190194
entered: state,
195+
same_state_enforced: false,
191196
});
192197
enable_state_scoped_entities::<S>(self);
193198
} else {

crates/bevy_state/src/state/freely_mutable_state.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,17 @@ fn apply_state_transition<S: FreelyMutableState>(
5252
current_state: Option<ResMut<State<S>>>,
5353
next_state: Option<ResMut<NextState<S>>>,
5454
) {
55-
let Some(next_state) = take_next_state(next_state) else {
55+
let Some((next_state, same_state_enforced)) = take_next_state(next_state) else {
5656
return;
5757
};
5858
let Some(current_state) = current_state else {
5959
return;
6060
};
61-
internal_apply_state_transition(event, commands, Some(current_state), Some(next_state));
61+
internal_apply_state_transition(
62+
event,
63+
commands,
64+
Some(current_state),
65+
Some(next_state),
66+
same_state_enforced,
67+
);
6268
}

crates/bevy_state/src/state/resources.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,31 @@ pub enum NextState<S: FreelyMutableState> {
127127
Unchanged,
128128
/// There is a pending transition for state `S`
129129
Pending(S),
130+
/// There is a pending transition for state `S`
131+
///
132+
/// This will trigger state transitions schedules even if the target state is the same as the current one.
133+
ForcedPending(S),
130134
}
131135

132136
impl<S: FreelyMutableState> NextState<S> {
133137
/// Tentatively set a pending state transition to `Some(state)`.
138+
///
139+
/// If `state` is the same as the current state, this will *not* trigger state
140+
/// transition [`OnEnter`](crate::state::OnEnter) and [`OnExit`](crate::state::OnExit) schedules.
141+
///
142+
/// If [`set_forced`](Self::set_forced) has already been called in the same frame with the same state, its behavior is kept.
134143
pub fn set(&mut self, state: S) {
135-
*self = Self::Pending(state);
144+
if !matches!(self, Self::ForcedPending(s) if s == &state) {
145+
*self = Self::Pending(state);
146+
}
147+
}
148+
149+
/// Tentatively set a pending state transition to `Some(state)`.
150+
///
151+
/// If `state` is the same as the current state, this will trigger state
152+
/// transition [`OnEnter`](crate::state::OnEnter) and [`OnExit`](crate::state::OnExit) schedules.
153+
pub fn set_forced(&mut self, state: S) {
154+
*self = Self::ForcedPending(state);
136155
}
137156

138157
/// Remove any pending changes to [`State<S>`]
@@ -143,13 +162,17 @@ impl<S: FreelyMutableState> NextState<S> {
143162

144163
pub(crate) fn take_next_state<S: FreelyMutableState>(
145164
next_state: Option<ResMut<NextState<S>>>,
146-
) -> Option<S> {
165+
) -> Option<(S, bool)> {
147166
let mut next_state = next_state?;
148167

149168
match core::mem::take(next_state.bypass_change_detection()) {
150169
NextState::Pending(x) => {
151170
next_state.set_changed();
152-
Some(x)
171+
Some((x, false))
172+
}
173+
NextState::ForcedPending(x) => {
174+
next_state.set_changed();
175+
Some((x, true))
153176
}
154177
NextState::Unchanged => None,
155178
}

crates/bevy_state/src/state/state_set.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ impl<S: InnerStateSet> StateSet for S {
112112
None
113113
};
114114

115-
internal_apply_state_transition(event, commands, current_state, new_state);
115+
internal_apply_state_transition(event, commands, current_state, new_state, false);
116116
};
117117

118118
schedule.configure_sets((
@@ -190,9 +190,26 @@ impl<S: InnerStateSet> StateSet for S {
190190
} else {
191191
current_state.clone()
192192
};
193-
let new_state = initial_state.map(|x| next_state.or(current_state).unwrap_or(x));
194-
195-
internal_apply_state_transition(event, commands, current_state_res, new_state);
193+
let same_state_enforced = next_state
194+
.as_ref()
195+
.map(|(_, same_state_enforced)| same_state_enforced)
196+
.cloned()
197+
.unwrap_or_default();
198+
199+
let new_state = initial_state.map(|x| {
200+
next_state
201+
.map(|(next, _)| next)
202+
.or(current_state)
203+
.unwrap_or(x)
204+
});
205+
206+
internal_apply_state_transition(
207+
event,
208+
commands,
209+
current_state_res,
210+
new_state,
211+
same_state_enforced,
212+
);
196213
};
197214

198215
schedule.configure_sets((
@@ -259,7 +276,7 @@ macro_rules! impl_state_set_sealed_tuples {
259276
None
260277
};
261278

262-
internal_apply_state_transition(message, commands, current_state, new_state);
279+
internal_apply_state_transition(message, commands, current_state, new_state, false);
263280
};
264281

265282
schedule.configure_sets((
@@ -311,9 +328,21 @@ macro_rules! impl_state_set_sealed_tuples {
311328
} else {
312329
current_state.clone()
313330
};
314-
let new_state = initial_state.map(|x| next_state.or(current_state).unwrap_or(x));
315331

316-
internal_apply_state_transition(message, commands, current_state_res, new_state);
332+
let same_state_enforced = next_state
333+
.as_ref()
334+
.map(|(_, same_state_enforced)| same_state_enforced)
335+
.cloned()
336+
.unwrap_or_default();
337+
338+
let new_state = initial_state.map(|x| {
339+
next_state
340+
.map(|(next, _)| next)
341+
.or(current_state)
342+
.unwrap_or(x)
343+
});
344+
345+
internal_apply_state_transition(message, commands, current_state_res, new_state, same_state_enforced);
317346
};
318347

319348
schedule.configure_sets((

crates/bevy_state/src/state/transitions.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub struct StateTransitionEvent<S: States> {
6767
pub exited: Option<S>,
6868
/// The state being entered.
6969
pub entered: Option<S>,
70+
/// Enforce this transition even if `exited` and `entered` are the same
71+
pub same_state_enforced: bool,
7072
}
7173

7274
/// Applies state transitions and runs transitions schedules in order.
@@ -135,6 +137,7 @@ pub(crate) fn internal_apply_state_transition<S: States>(
135137
mut commands: Commands,
136138
current_state: Option<ResMut<State<S>>>,
137139
new_state: Option<S>,
140+
same_state_enforced: bool,
138141
) {
139142
match new_state {
140143
Some(entered) => {
@@ -153,6 +156,7 @@ pub(crate) fn internal_apply_state_transition<S: States>(
153156
event.write(StateTransitionEvent {
154157
exited: Some(exited.clone()),
155158
entered: Some(entered.clone()),
159+
same_state_enforced,
156160
});
157161
}
158162
None => {
@@ -162,6 +166,7 @@ pub(crate) fn internal_apply_state_transition<S: States>(
162166
event.write(StateTransitionEvent {
163167
exited: None,
164168
entered: Some(entered.clone()),
169+
same_state_enforced,
165170
});
166171
}
167172
};
@@ -174,6 +179,7 @@ pub(crate) fn internal_apply_state_transition<S: States>(
174179
event.write(StateTransitionEvent {
175180
exited: Some(resource.get().clone()),
176181
entered: None,
182+
same_state_enforced,
177183
});
178184
}
179185
}
@@ -217,7 +223,7 @@ pub(crate) fn run_enter<S: States>(
217223
let Some(transition) = transition.0 else {
218224
return;
219225
};
220-
if transition.entered == transition.exited {
226+
if transition.entered == transition.exited && !transition.same_state_enforced {
221227
return;
222228
}
223229
let Some(entered) = transition.entered else {
@@ -234,7 +240,7 @@ pub(crate) fn run_exit<S: States>(
234240
let Some(transition) = transition.0 else {
235241
return;
236242
};
237-
if transition.entered == transition.exited {
243+
if transition.entered == transition.exited && !transition.same_state_enforced {
238244
return;
239245
}
240246
let Some(exited) = transition.exited else {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
title: Same State Transitions
3+
pull_requests: [19363]
4+
---
5+
6+
It is now possible to change to the same state, triggering state transitions.
7+
8+
```rust
9+
// Before: did nothing if the state was already `State::Menu`
10+
next_state.set(State::Menu);
11+
// After: trigger state transitions even if the state is already `State::Menu`
12+
next_state.set_forced(State::Menu);
13+
```

0 commit comments

Comments
 (0)