//! This example illustrates how to register custom state transition behavior. //! //! In this case we are trying to add `OnReenter` and `OnReexit` //! which will work much like `OnEnter` and `OnExit`, //! but additionally trigger if the state changed into itself. //! //! While identity transitions exist internally in [`StateTransitionEvent`]s, //! the default schedules intentionally ignore them, as this behavior is not commonly needed or expected. //! //! While this example displays identity transitions for a single state, //! identity transitions are propagated through the entire state graph, //! meaning any change to parent state will be propagated to [`ComputedStates`] and [`SubStates`]. use std::marker::PhantomData; use bevy::{dev_tools::states::*, ecs::schedule::ScheduleLabel, prelude::*}; use custom_transitions::*; #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] enum AppState { #[default] Menu, InGame, } fn main() { App::new() // We insert the custom transitions plugin for `AppState`. .add_plugins(( DefaultPlugins, IdentityTransitionsPlugin::::default(), )) .init_state::() .add_systems(Startup, setup) .add_systems(OnEnter(AppState::Menu), setup_menu) .add_systems(Update, menu.run_if(in_state(AppState::Menu))) .add_systems(OnExit(AppState::Menu), cleanup_menu) // We will restart the game progress every time we re-enter into it. .add_systems(OnReenter(AppState::InGame), setup_game) .add_systems(OnReexit(AppState::InGame), teardown_game) // Doing it this way allows us to restart the game without any additional in-between states. .add_systems( Update, ((movement, change_color, trigger_game_restart).run_if(in_state(AppState::InGame)),), ) .add_systems(Update, log_transitions::) .run(); } /// This module provides the custom `OnReenter` and `OnReexit` transitions for easy installation. mod custom_transitions { use crate::*; /// The plugin registers the transitions for one specific state. /// If you use this for multiple states consider: /// - installing the plugin multiple times, /// - create an [`App`] extension method that inserts /// those transitions during state installation. #[derive(Default)] pub struct IdentityTransitionsPlugin(PhantomData); impl Plugin for IdentityTransitionsPlugin { fn build(&self, app: &mut App) { app.add_systems( StateTransition, // The internals can generate at most one transition event of specific type per frame. // We take the latest one and clear the queue. last_transition:: // We insert the optional event into our schedule runner. .pipe(run_reenter::) // State transitions are handled in three ordered steps, exposed as system sets. // We can add our systems to them, which will run the corresponding schedules when they're evaluated. // These are: // - [`ExitSchedules`] - Ran from leaf-states to root-states, // - [`TransitionSchedules`] - Ran in arbitrary order, // - [`EnterSchedules`] - Ran from root-states to leaf-states. .in_set(EnterSchedules::::default()), ) .add_systems( StateTransition, last_transition:: .pipe(run_reexit::) .in_set(ExitSchedules::::default()), ); } } /// Custom schedule that will behave like [`OnEnter`], but run on identity transitions. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct OnReenter(pub S); /// Schedule runner which checks conditions and if they're right /// runs out custom schedule. fn run_reenter(transition: In>>, world: &mut World) { // We return early if no transition event happened. let Some(transition) = transition.0 else { return; }; // If we wanted to ignore identity transitions, // we'd compare `exited` and `entered` here, // and return if they were the same. // We check if we actually entered a state. // A [`None`] would indicate that the state was removed from the world. // This only happens in the case of [`SubStates`] and [`ComputedStates`]. let Some(entered) = transition.entered else { return; }; // If all conditions are valid, we run our custom schedule. let _ = world.try_run_schedule(OnReenter(entered)); // If you want to overwrite the default `OnEnter` behavior to act like re-enter, // you can do so by running the `OnEnter` schedule here. Note that you don't want // to run `OnEnter` when the default behavior does so. // ``` // if transition.entered != transition.exited { // return; // } // let _ = world.try_run_schedule(OnReenter(entered)); // ``` } /// Custom schedule that will behave like [`OnExit`], but run on identity transitions. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct OnReexit(pub S); fn run_reexit(transition: In>>, world: &mut World) { let Some(transition) = transition.0 else { return; }; let Some(exited) = transition.exited else { return; }; let _ = world.try_run_schedule(OnReexit(exited)); } } fn menu( mut next_state: ResMut>, mut interaction_query: Query< (&Interaction, &mut UiImage), (Changed, With