StateTransitionEvent (#11089)

# Objective

- Make it possible to react to arbitrary state changes
- this will be useful regardless of the other changes to states
currently being discussed

## Solution

- added `StateTransitionEvent<S>` struct
- previously, this would have been impossible:

```rs
#[derive(States, Eq, PartialEq, Hash, Copy, Clone, Default)]
enum MyState {
  #[default]
  Foo,
  Bar(MySubState),
}

enum MySubState {
  Spam,
  Eggs,
}

app.add_system(Update, on_enter_bar);

fn on_enter_bar(trans: EventReader<StateTransition<MyState>>){
  for (befoare, after) in trans.read() {
    match before, after {
      MyState::Foo, MyState::Bar(_) => info!("detected transition foo => bar");
      _, _ => ();
    }
  }
}
```

---

## Changelog

- Added
  - `StateTransitionEvent<S>` - Fired on state changes of `S`

## Migration Guide

N/A no breaking changes

---------

Co-authored-by: Federico Rinaldi <gisquerin@gmail.com>
This commit is contained in:
Connor King 2024-01-08 17:27:00 -05:00 committed by GitHub
parent 8d9a0a883f
commit 1260b7bcf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 33 additions and 2 deletions

View file

@ -5,7 +5,7 @@ use bevy_ecs::{
schedule::{
apply_state_transition, common_conditions::run_once as run_once_condition,
run_enter_schedule, InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs,
ScheduleBuildSettings, ScheduleLabel,
ScheduleBuildSettings, ScheduleLabel, StateTransitionEvent,
},
};
use bevy_utils::{intern::Interned, thiserror::Error, tracing::debug, HashMap, HashSet};
@ -368,6 +368,7 @@ impl App {
if !self.world.contains_resource::<State<S>>() {
self.init_resource::<State<S>>()
.init_resource::<NextState<S>>()
.add_event::<StateTransitionEvent<S>>()
.add_systems(
StateTransition,
(
@ -403,6 +404,7 @@ impl App {
pub fn insert_state<S: States>(&mut self, state: S) -> &mut Self {
self.insert_resource(State::new(state))
.init_resource::<NextState<S>>()
.add_event::<StateTransitionEvent<S>>()
.add_systems(
StateTransition,
(

View file

@ -40,7 +40,7 @@ pub mod prelude {
schedule::{
apply_deferred, apply_state_transition, common_conditions::*, Condition,
IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, OnExit,
OnTransition, Schedule, Schedules, State, States, SystemSet,
OnTransition, Schedule, Schedules, State, StateTransitionEvent, States, SystemSet,
},
system::{
Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands,

View file

@ -5,6 +5,7 @@ use std::ops::Deref;
use crate as bevy_ecs;
use crate::change_detection::DetectChangesMut;
use crate::event::Event;
use crate::prelude::FromWorld;
#[cfg(feature = "bevy_reflect")]
use crate::reflect::ReflectResource;
@ -137,6 +138,17 @@ impl<S: States> NextState<S> {
}
}
/// Event sent when any state transition of `S` happens.
///
/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)]
pub struct StateTransitionEvent<S: States> {
/// the state we were in before
pub before: S,
/// the state we're in now
pub after: S,
}
/// Run the enter schedule (if it exists) for the current state.
pub fn run_enter_schedule<S: States>(world: &mut World) {
world
@ -146,6 +158,7 @@ pub fn run_enter_schedule<S: States>(world: &mut World) {
/// If a new state is queued in [`NextState<S>`], this system:
/// - Takes the new state value from [`NextState<S>`] and updates [`State<S>`].
/// - Sends a relevant [`StateTransitionEvent`]
/// - Runs the [`OnExit(exited_state)`] schedule, if it exists.
/// - Runs the [`OnTransition { from: exited_state, to: entered_state }`](OnTransition), if it exists.
/// - Runs the [`OnEnter(entered_state)`] schedule, if it exists.
@ -159,6 +172,10 @@ pub fn apply_state_transition<S: States>(world: &mut World) {
let mut state_resource = world.resource_mut::<State<S>>();
if *state_resource != entered {
let exited = mem::replace(&mut state_resource.0, entered.clone());
world.send_event(StateTransitionEvent {
before: exited.clone(),
after: entered.clone(),
});
// Try to run the schedules if they exist.
world.try_run_schedule(OnExit(exited.clone())).ok();
world

View file

@ -25,6 +25,7 @@ fn main() {
Update,
(movement, change_color).run_if(in_state(AppState::InGame)),
)
.add_systems(Update, log_transitions)
.run();
}
@ -159,3 +160,14 @@ fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
.set_b((time.elapsed_seconds() * 0.5).sin() + 2.0);
}
}
/// print when an `AppState` transition happens
/// also serves as an example of how to use `StateTransitionEvent`
fn log_transitions(mut transitions: EventReader<StateTransitionEvent<AppState>>) {
for transition in transitions.read() {
info!(
"transition: {:?} => {:?}",
transition.before, transition.after
);
}
}