//! This example illustrates the use of [`ComputedStates`] for more complex state handling patterns. //! //! In this case, we'll be implementing the following pattern: //! - The game will start in a `Menu` state, which we can return to with `Esc` //! - From there, we can enter the game - where our bevy symbol moves around and changes color //! - While in game, we can pause and unpause the game using `Space` //! - We can also toggle "Turbo Mode" with the `T` key - where the movement and color changes are all faster. This //! is retained between pauses, but not if we exit to the main menu. //! //! In addition, we want to enable a "tutorial" mode, which will involve it's own state that is toggled in the main menu. //! This will display instructions about movement and turbo mode when in game and unpaused, and instructions on how to unpause when paused. //! //! To implement this, we will create 2 root-level states: [`AppState`] and [`TutorialState`]. //! We will then create some computed states that derive from [`AppState`]: [`InGame`] and [`TurboMode`] are marker states implemented //! as Zero-Sized Structs (ZSTs), while [`IsPaused`] is an enum with 2 distinct states. //! And lastly, we'll add [`Tutorial`], a computed state deriving from [`TutorialState`], [`InGame`] and [`IsPaused`], with 2 distinct //! states to display the 2 tutorial texts. use bevy::prelude::*; use ui::*; // To begin, we want to define our state objects. #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] enum AppState { #[default] Menu, // Unlike in the `states` example, we're adding more data in this // version of our AppState. In this case, we actually have // 4 distinct "InGame" states - unpaused and no turbo, paused and no // turbo, unpaused and turbo and paused and turbo. InGame { paused: bool, turbo: bool, }, } // The tutorial state object, on the other hand, is a fairly simple enum. #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] enum TutorialState { #[default] Active, Inactive, } // Because we have 4 distinct values of `AppState` that mean we're "InGame", we're going to define // a separate "InGame" type and implement `ComputedStates` for it. // This allows us to only need to check against one type // when otherwise we'd need to check against multiple. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] struct InGame; impl ComputedStates for InGame { // Our computed state depends on `AppState`, so we need to specify it as the SourceStates type. type SourceStates = AppState; // The compute function takes in the `SourceStates` fn compute(sources: AppState) -> Option { // You might notice that InGame has no values - instead, in this case, the `State` resource only exists // if the `compute` function would return `Some` - so only when we are in game. match sources { // No matter what the value of `paused` or `turbo` is, we're still in the game rather than a menu AppState::InGame { .. } => Some(Self), _ => None, } } } // Similarly, we want to have the TurboMode state - so we'll define that now. // // Having it separate from [`InGame`] and [`AppState`] like this allows us to check each of them separately, rather than // needing to compare against every version of the AppState that could involve them. // // In addition, it allows us to still maintain a strict type representation - you can't Turbo // if you aren't in game, for example - while still having the // flexibility to check for the states as if they were completely unrelated. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] struct TurboMode; impl ComputedStates for TurboMode { type SourceStates = AppState; fn compute(sources: AppState) -> Option { match sources { AppState::InGame { turbo: true, .. } => Some(Self), _ => None, } } } // For the [`IsPaused`] state, we'll actually use an `enum` - because the difference between `Paused` and `NotPaused` // involve activating different systems. // // To clarify the difference, `InGame` and `TurboMode` both activate systems if they exist, and there is // no variation within them. So we defined them as Zero-Sized Structs. // // In contrast, pausing actually involve 3 distinct potential situations: // - it doesn't exist - this is when being paused is meaningless, like in the menu. // - it is `NotPaused` - in which elements like the movement system are active. // - it is `Paused` - in which those game systems are inactive, and a pause screen is shown. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] enum IsPaused { NotPaused, Paused, } impl ComputedStates for IsPaused { type SourceStates = AppState; fn compute(sources: AppState) -> Option { // Here we convert from our [`AppState`] to all potential [`IsPaused`] versions. match sources { AppState::InGame { paused: true, .. } => Some(Self::Paused), AppState::InGame { paused: false, .. } => Some(Self::NotPaused), // If `AppState` is not `InGame`, pausing is meaningless, and so we set it to `None`. _ => None, } } } // Lastly, we have our tutorial, which actually has a more complex derivation. // // Like `IsPaused`, the tutorial has a few fully distinct possible states, so we want to represent them // as an Enum. However - in this case they are all dependant on multiple states: the root [`TutorialState`], // and both [`InGame`] and [`IsPaused`] - which are in turn derived from [`AppState`]. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] enum Tutorial { MovementInstructions, PauseInstructions, } impl ComputedStates for Tutorial { // We can also use tuples of types that implement [`States`] as our [`SourceStates`]. // That includes other [`ComputedStates`] - though circular dependencies are not supported // and will produce a compile error. // // We could define this as relying on [`TutorialState`] and [`AppState`] instead, but // then we would need to duplicate the derivation logic for [`InGame`] and [`IsPaused`]. // In this example that is not a significant undertaking, but as a rule it is likely more // effective to rely on the already derived states to avoid the logic drifting apart. // // Notice that you can wrap any of the [`States`] here in [`Option`]s. If you do so, // the the computation will get called even if the state does not exist. type SourceStates = (TutorialState, InGame, Option); // Notice that we aren't using InGame - we're just using it as a source state to // prevent the computation from executing if we're not in game. Instead - this // ComputedState will just not exist in that situation. fn compute( (tutorial_state, _in_game, is_paused): (TutorialState, InGame, Option), ) -> Option { // If the tutorial is inactive we don't need to worry about it. if !matches!(tutorial_state, TutorialState::Active) { return None; } // If we're paused, we're in the PauseInstructions tutorial // Otherwise, we're in the MovementInstructions tutorial match is_paused? { IsPaused::NotPaused => Some(Tutorial::MovementInstructions), IsPaused::Paused => Some(Tutorial::PauseInstructions), } } } fn main() { // We start the setup like we did in the states example. App::new() .add_plugins(DefaultPlugins) .init_state::() .init_state::() // After initializing the normal states, we'll use `.add_computed_state::()` to initialize our `ComputedStates` .add_computed_state::() .add_computed_state::() .add_computed_state::() .add_computed_state::() // we can then resume adding systems just like we would in any other case, // using our states as normal. .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 only want to run the [`setup_game`] function when we enter the [`AppState::InGame`] state, regardless // of whether the game is paused or not. .add_systems(OnEnter(InGame), setup_game) // And we only want to run the [`clear_game`] function when we leave the [`AppState::InGame`] state, regardless // of whether we're paused. .add_systems(OnExit(InGame), clear_state_bound_entities(InGame)) // We want the color change, toggle_pause and quit_to_menu systems to ignore the paused condition, so we can use the [`InGame`] derived // state here as well. .add_systems( Update, (toggle_pause, change_color, quit_to_menu).run_if(in_state(InGame)), ) // However, we only want to move or toggle turbo mode if we are not in a paused state. .add_systems( Update, (toggle_turbo, movement).run_if(in_state(IsPaused::NotPaused)), ) // We can continue setting things up, following all the same patterns used above and in the `states` example. .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen) .add_systems( OnExit(IsPaused::Paused), clear_state_bound_entities(IsPaused::Paused), ) .add_systems(OnEnter(TurboMode), setup_turbo_text) .add_systems(OnExit(TurboMode), clear_state_bound_entities(TurboMode)) .add_systems( OnEnter(Tutorial::MovementInstructions), movement_instructions, ) .add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions) .add_systems( OnExit(Tutorial::MovementInstructions), clear_state_bound_entities(Tutorial::MovementInstructions), ) .add_systems( OnExit(Tutorial::PauseInstructions), clear_state_bound_entities(Tutorial::PauseInstructions), ) .add_systems(Update, log_transitions) .run(); } fn menu( mut next_state: ResMut>, tutorial_state: Res>, mut next_tutorial: ResMut>, mut interaction_query: Query< (&Interaction, &mut UiImage, &MenuButton), (Changed, With