From b8832dc86224e316784a380c73bb711098de6af5 Mon Sep 17 00:00:00 2001 From: Lee-Orr Date: Thu, 2 May 2024 15:36:23 -0400 Subject: [PATCH] Computed State & Sub States (#11426) ## Summary/Description This PR extends states to allow support for a wider variety of state types and patterns, by providing 3 distinct types of state: - Standard [`States`] can only be changed by manually setting the [`NextState`] resource. These states are the baseline on which the other state types are built, and can be used on their own for many simple patterns. See the [state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs) for a simple use case - these are the states that existed so far in Bevy. - [`SubStates`] are children of other states - they can be changed manually using [`NextState`], but are removed from the [`World`] if the source states aren't in the right state. See the [sub_states example](https://github.com/lee-orr/bevy/blob/derived_state/examples/ecs/sub_states.rs) for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. - [`ComputedStates`] are fully derived from other states - they provide a [`compute`](ComputedStates::compute) method that takes in the source states and returns their derived value. They are particularly useful for situations where a simplified view of the source states is necessary - such as having an `InAMenu` computed state derived from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/lee-orr/bevy/blob/derived_state/examples/ecs/computed_states.rscomputed_states.rs) to see a sampling of uses for these states. # Objective This PR is another attempt at allowing Bevy to better handle complex state objects in a manner that doesn't rely on strict equality. While my previous attempts (https://github.com/bevyengine/bevy/pull/10088 and https://github.com/bevyengine/bevy/pull/9957) relied on complex matching capacities at the point of adding a system to application, this one instead relies on deterministically deriving simple states from more complex ones. As a result, it does not require any special macros, nor does it change any other interactions with the state system once you define and add your derived state. It also maintains a degree of distinction between `State` and just normal application state - your derivations have to end up being discreet pre-determined values, meaning there is less of a risk/temptation to place a significant amount of logic and data within a given state. ### Addition - Sub States closes #9942 After some conversation with Maintainers & SMEs, a significant concern was that people might attempt to use this feature as if it were sub-states, and find themselves unable to use it appropriately. Since `ComputedState` is mainly a state matching feature, while `SubStates` are more of a state mutation related feature - but one that is easy to add with the help of the machinery introduced by `ComputedState`, it was added here as well. The relevant discussion is here: https://discord.com/channels/691052431525675048/1200556329803186316 ## Solution closes #11358 The solution is to create a new type of state - one implementing `ComputedStates` - which is deterministically tied to one or more other states. Implementors write a function to transform the source states into the computed state, and it gets triggered whenever one of the source states changes. In addition, we added the `FreelyMutableState` trait , which is implemented as part of the derive macro for `States`. This allows us to limit use of `NextState` to states that are actually mutable, preventing mis-use of `ComputedStates`. --- ## Changelog - Added `ComputedStates` trait - Added `FreelyMutableState` trait - Converted `NextState` resource to an Enum, with `Unchanged` and `Pending` - Added `App::add_computed_state::()`, to allow for easily adding derived states to an App. - Moved the `StateTransition` schedule label from `bevy_app` to `bevy_ecs` - but maintained the export in `bevy_app` for continuity. - Modified the process for updating states. Instead of just having an `apply_state_transition` system that can be added anywhere, we now have a multi-stage process that has to run within the `StateTransition` label. First, all the state changes are calculated - manual transitions rely on `apply_state_transition`, while computed transitions run their computation process before both call `internal_apply_state_transition` to apply the transition, send out the transition event, trigger dependent states, and record which exit/transition/enter schedules need to occur. Once all the states have been updated, the transition schedules are called - first the exit schedules, then transition schedules and finally enter schedules. - Added `SubStates` trait - Adjusted `apply_state_transition` to be a no-op if the `State` resource doesn't exist ## Migration Guide If the user accessed the NextState resource's value directly or created them from scratch they will need to adjust to use the new enum variants: - if they created a `NextState(Some(S))` - they should now use `NextState::Pending(S)` - if they created a `NextState(None)` -they should now use `NextState::Unchanged` - if they matched on the `NextState` value, they would need to make the adjustments above If the user manually utilized `apply_state_transition`, they should instead use systems that trigger the `StateTransition` schedule. --- ## Future Work There is still some future potential work in the area, but I wanted to keep these potential features and changes separate to keep the scope here contained, and keep the core of it easy to understand and use. However, I do want to note some of these things, both as inspiration to others and an illustration of what this PR could unlock. - `NextState::Remove` - Now that the `State` related mechanisms all utilize options (#11417), it's fairly easy to add support for explicit state removal. And while `ComputedStates` can add and remove themselves, right now `FreelyMutableState`s can't be removed from within the state system. While it existed originally in this PR, it is a different question with a separate scope and usability concerns - so having it as it's own future PR seems like the best approach. This feature currently lives in a separate branch in my fork, and the differences between it and this PR can be seen here: https://github.com/lee-orr/bevy/pull/5 - `NextState::ReEnter` - this would allow you to trigger exit & entry systems for the current state type. We can potentially also add a `NextState::ReEnterRecirsive` to also re-trigger any states that depend on the current one. - More mechanisms for `State` updates - This PR would finally make states that aren't a set of exclusive Enums useful, and with that comes the question of setting state more effectively. Right now, to update a state you either need to fully create the new state, or include the `Res>>` resource in your system, clone the state, mutate it, and then use `NextState.set(my_mutated_state)` to make it the pending next state. There are a few other potential methods that could be implemented in future PRs: - Inverse Compute States - these would essentially be compute states that have an additional (manually defined) function that can be used to nudge the source states so that they result in the computed states having a given value. For example, you could use set the `IsPaused` state, and it would attempt to pause or unpause the game by modifying the `AppState` as needed. - Closure-based state modification - this would involve adding a `NextState.modify(f: impl Fn(Option -> Option)` method, and then you can pass in closures or function pointers to adjust the state as needed. - Message-based state modification - this would involve either creating states that can respond to specific messages, similar to Elm or Redux. These could either use the `NextState` mechanism or the Event mechanism. - ~`SubStates` - which are essentially a hybrid of computed and manual states. In the simplest (and most likely) version, they would work by having a computed element that determines whether the state should exist, and if it should has the capacity to add a new version in, but then any changes to it's content would be freely mutated.~ this feature is now part of this PR. See above. - Lastly, since states are getting more complex there might be value in moving them out of `bevy_ecs` and into their own crate, or at least out of the `schedule` module into a `states` module. #11087 As mentioned, all these future work elements are TBD and are explicitly not part of this PR - I just wanted to provide them as potential explorations for the future. --------- Co-authored-by: Alice Cecile Co-authored-by: Marcel Champagne Co-authored-by: MiniaczQ --- Cargo.toml | 22 + crates/bevy_app/src/app.rs | 68 +- crates/bevy_app/src/lib.rs | 3 +- crates/bevy_app/src/main_schedule.rs | 8 +- crates/bevy_app/src/sub_app.rs | 77 +- crates/bevy_ecs/macros/Cargo.toml | 2 +- crates/bevy_ecs/macros/src/lib.rs | 5 + crates/bevy_ecs/macros/src/states.rs | 129 ++- crates/bevy_ecs/src/lib.rs | 7 +- crates/bevy_ecs/src/schedule/state.rs | 1393 ++++++++++++++++++++++++- examples/README.md | 2 + examples/ecs/computed_states.rs | 686 ++++++++++++ examples/ecs/sub_states.rs | 275 +++++ 13 files changed, 2543 insertions(+), 134 deletions(-) create mode 100644 examples/ecs/computed_states.rs create mode 100644 examples/ecs/sub_states.rs diff --git a/Cargo.toml b/Cargo.toml index c05a4e52d9..a3e23ca0ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1692,6 +1692,28 @@ description = "Illustrates how to use States to control transitioning from a Men category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "sub_states" +path = "examples/ecs/sub_states.rs" +doc-scrape-examples = true + +[package.metadata.example.sub_states] +name = "Sub States" +description = "Using Sub States for hierarchical state handling." +category = "ECS (Entity Component System)" +wasm = false + +[[example]] +name = "computed_states" +path = "examples/ecs/computed_states.rs" +doc-scrape-examples = true + +[package.metadata.example.computed_states] +name = "Computed States" +description = "Advanced state patterns using Computed States" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "system_piping" path = "examples/ecs/system_piping.rs" diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index c8f229cf41..80fb05f2b9 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ event::{event_update_system, ManualEventReader}, intern::Interned, prelude::*, - schedule::{ScheduleBuildSettings, ScheduleLabel}, + schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; #[cfg(feature = "trace")] @@ -266,26 +266,17 @@ impl App { /// Initializes a [`State`] with standard starting values. /// - /// If the [`State`] already exists, nothing happens. + /// This method is idempotent: it has no effect when called again using the same generic type. /// - /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules for - /// each state variant (if they don't already exist), an instance of [`apply_state_transition::`] - /// in [`StateTransition`] so that transitions happen before [`Update`] and an instance of - /// [`run_enter_schedule::`] in [`StateTransition`] with a [`run_once`] condition to run the - /// on enter schedule of the initial state. + /// Adds [`State`] and [`NextState`] resources, and enables use of the [`OnEnter`], [`OnTransition`] and [`OnExit`] schedules. + /// These schedules are triggered before [`Update`](crate::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can /// emulate this behavior using the [`in_state`] [`Condition`]. /// - /// Note that you can also apply state transitions at other points in the schedule by adding - /// the [`apply_state_transition::`] system manually. - /// - /// [`StateTransition`]: crate::StateTransition - /// [`Update`]: crate::Update - /// [`run_once`]: bevy_ecs::schedule::common_conditions::run_once - /// [`run_enter_schedule::`]: bevy_ecs::schedule::run_enter_schedule - /// [`apply_state_transition::`]: bevy_ecs::schedule::apply_state_transition - pub fn init_state(&mut self) -> &mut Self { + /// Note that you can also apply state transitions at other points in the schedule + /// by triggering the [`StateTransition`](`bevy_ecs::schedule::StateTransition`) schedule manually. + pub fn init_state(&mut self) -> &mut Self { self.main_mut().init_state::(); self } @@ -293,29 +284,42 @@ impl App { /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// added of the same type. /// - /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules for - /// each state variant (if they don't already exist), an instance of [`apply_state_transition::`] - /// in [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and an - /// instance of [`run_enter_schedule::`] in [`StateTransition`] with a [`run_once`] - /// condition to run the on enter schedule of the initial state. + /// Adds [`State`] and [`NextState`] resources, and enables use of the [`OnEnter`], [`OnTransition`] and [`OnExit`] schedules. + /// These schedules are triggered before [`Update`](crate::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can /// emulate this behavior using the [`in_state`] [`Condition`]. /// - /// Note that you can also apply state transitions at other points in the schedule by adding - /// the [`apply_state_transition::`] system manually. - /// - /// [`StateTransition`]: crate::StateTransition - /// [`Update`]: crate::Update - /// [`run_once`]: bevy_ecs::schedule::common_conditions::run_once - /// [`run_enter_schedule::`]: bevy_ecs::schedule::run_enter_schedule - /// [`apply_state_transition::`]: bevy_ecs::schedule::apply_state_transition - pub fn insert_state(&mut self, state: S) -> &mut Self { - self.main_mut().insert_state(state); + /// Note that you can also apply state transitions at other points in the schedule + /// by triggering the [`StateTransition`](`bevy_ecs::schedule::StateTransition`) schedule manually. + pub fn insert_state(&mut self, state: S) -> &mut Self { + self.main_mut().insert_state::(state); self } - /// Adds a collection of systems to `schedule` (stored in the main world's [`Schedules`]). + /// Sets up a type implementing [`ComputedStates`]. + /// + /// This method is idempotent: it has no effect when called again using the same generic type. + /// + /// For each source state the derived state depends on, it adds this state's derivation + /// to it's [`ComputeDependantStates`](bevy_ecs::schedule::ComputeDependantStates) schedule. + pub fn add_computed_state(&mut self) -> &mut Self { + self.main_mut().add_computed_state::(); + self + } + + /// Sets up a type implementing [`SubStates`]. + /// + /// This method is idempotent: it has no effect when called again using the same generic type. + /// + /// For each source state the derived state depends on, it adds this state's existence check + /// to it's [`ComputeDependantStates`](bevy_ecs::schedule::ComputeDependantStates) schedule. + pub fn add_sub_state(&mut self) -> &mut Self { + self.main_mut().add_sub_state::(); + self + } + + /// Adds one or more systems to the given schedule in this app's [`Schedules`]. /// /// # Examples /// diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index e338a45664..38d8d0e280 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -31,8 +31,7 @@ pub mod prelude { app::{App, AppExit}, main_schedule::{ First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main, - PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, StateTransition, - Update, + PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, Update, }, sub_app::SubApp, DynamicPlugin, Plugin, PluginGroup, diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 0d3a804df7..2399cf01c1 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -1,6 +1,6 @@ use crate::{App, Plugin}; use bevy_ecs::{ - schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel}, + schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition}, system::{Local, Resource}, world::{Mut, World}, }; @@ -73,12 +73,6 @@ pub struct First; #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct PreUpdate; -/// Runs [state transitions](bevy_ecs::schedule::States). -/// -/// See the [`Main`] schedule for some details about how schedules are run. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct StateTransition; - /// Runs the [`FixedMain`] schedule in a loop according until all relevant elapsed time has been "consumed". /// /// See the [`Main`] schedule for some details about how schedules are run. diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 3f44fafd92..4b0eab2927 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -1,10 +1,10 @@ -use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, StateTransition}; +use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup}; use bevy_ecs::{ event::EventRegistry, prelude::*, schedule::{ - common_conditions::run_once as run_once_condition, run_enter_schedule, - InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel, + setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel, + ScheduleBuildSettings, ScheduleLabel, }, system::SystemId, }; @@ -317,40 +317,61 @@ impl SubApp { } /// See [`App::init_state`]. - pub fn init_state(&mut self) -> &mut Self { + pub fn init_state(&mut self) -> &mut Self { if !self.world.contains_resource::>() { + setup_state_transitions_in_world(&mut self.world, Some(Startup.intern())); self.init_resource::>() .init_resource::>() - .add_event::>() - .add_systems( - StateTransition, - ( - run_enter_schedule::.run_if(run_once_condition()), - apply_state_transition::, - ) - .chain(), - ); + .add_event::>(); + let schedule = self.get_schedule_mut(StateTransition).unwrap(); + S::register_state(schedule); } - // The OnEnter, OnExit, and OnTransition schedules are lazily initialized - // (i.e. when the first system is added to them), so World::try_run_schedule - // is used to fail gracefully if they aren't present. self } /// See [`App::insert_state`]. - pub fn insert_state(&mut self, state: S) -> &mut Self { - self.insert_resource(State::new(state)) - .init_resource::>() - .add_event::>() - .add_systems( - StateTransition, - ( - run_enter_schedule::.run_if(run_once_condition()), - apply_state_transition::, - ) - .chain(), - ); + pub fn insert_state(&mut self, state: S) -> &mut Self { + if !self.world.contains_resource::>() { + setup_state_transitions_in_world(&mut self.world, Some(Startup.intern())); + self.insert_resource::>(State::new(state)) + .init_resource::>() + .add_event::>(); + + let schedule = self.get_schedule_mut(StateTransition).unwrap(); + S::register_state(schedule); + } + + self + } + + /// See [`App::add_computed_state`]. + pub fn add_computed_state(&mut self) -> &mut Self { + if !self + .world + .contains_resource::>>() + { + setup_state_transitions_in_world(&mut self.world, Some(Startup.intern())); + self.add_event::>(); + let schedule = self.get_schedule_mut(StateTransition).unwrap(); + S::register_computed_state_systems(schedule); + } + + self + } + + /// See [`App::add_sub_state`]. + pub fn add_sub_state(&mut self) -> &mut Self { + if !self + .world + .contains_resource::>>() + { + setup_state_transitions_in_world(&mut self.world, Some(Startup.intern())); + self.init_resource::>(); + self.add_event::>(); + let schedule = self.get_schedule_mut(StateTransition).unwrap(); + S::register_sub_state_systems(schedule); + } self } diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index abc6647a8b..b2896fbeaf 100644 --- a/crates/bevy_ecs/macros/Cargo.toml +++ b/crates/bevy_ecs/macros/Cargo.toml @@ -11,7 +11,7 @@ proc-macro = true [dependencies] bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } -syn = "2.0" +syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 75a43e8e48..dbca96db17 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -521,3 +521,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream { pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } + +#[proc_macro_derive(SubStates, attributes(source))] +pub fn derive_substates(input: TokenStream) -> TokenStream { + states::derive_substates(input) +} diff --git a/crates/bevy_ecs/macros/src/states.rs b/crates/bevy_ecs/macros/src/states.rs index 0eb516c315..ff69812aea 100644 --- a/crates/bevy_ecs/macros/src/states.rs +++ b/crates/bevy_ecs/macros/src/states.rs @@ -1,21 +1,144 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; use crate::bevy_ecs_path; pub fn derive_states(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); + let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut trait_path = bevy_ecs_path(); - trait_path.segments.push(format_ident!("schedule").into()); + let mut base_trait_path = bevy_ecs_path(); + base_trait_path + .segments + .push(format_ident!("schedule").into()); + + let mut trait_path = base_trait_path.clone(); trait_path.segments.push(format_ident!("States").into()); + + let mut state_mutation_trait_path = base_trait_path.clone(); + state_mutation_trait_path + .segments + .push(format_ident!("FreelyMutableState").into()); + let struct_name = &ast.ident; quote! { impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} + + impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { + } } .into() } + +struct Source { + source_type: Path, + source_value: Pat, +} + +fn parse_sources_attr(ast: &DeriveInput) -> Result { + let mut result = ast + .attrs + .iter() + .filter(|a| a.path().is_ident("source")) + .map(|meta| { + let mut source = None; + let value = meta.parse_nested_meta(|nested| { + let source_type = nested.path.clone(); + let source_value = Pat::parse_multi(nested.value()?)?; + source = Some(Source { + source_type, + source_value, + }); + Ok(()) + }); + match source { + Some(value) => Ok(value), + None => match value { + Ok(_) => Err(syn::Error::new( + ast.span(), + "Couldn't parse SubStates source", + )), + Err(e) => Err(e), + }, + } + }) + .collect::>>()?; + + if result.len() > 1 { + return Err(syn::Error::new( + ast.span(), + "Only one source is allowed for SubStates", + )); + } + + let Some(result) = result.pop() else { + return Err(syn::Error::new(ast.span(), "SubStates require a source")); + }; + + Ok(result) +} + +pub fn derive_substates(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let sources = parse_sources_attr(&ast).expect("Failed to parse substate sources"); + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut base_trait_path = bevy_ecs_path(); + base_trait_path + .segments + .push(format_ident!("schedule").into()); + + let mut trait_path = base_trait_path.clone(); + trait_path.segments.push(format_ident!("SubStates").into()); + + let mut state_set_trait_path = base_trait_path.clone(); + state_set_trait_path + .segments + .push(format_ident!("StateSet").into()); + + let mut state_trait_path = base_trait_path.clone(); + state_trait_path + .segments + .push(format_ident!("States").into()); + + let mut state_mutation_trait_path = base_trait_path.clone(); + state_mutation_trait_path + .segments + .push(format_ident!("FreelyMutableState").into()); + + let struct_name = &ast.ident; + + let source_state_type = sources.source_type; + let source_state_value = sources.source_value; + + let result = quote! { + impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { + type SourceStates = #source_state_type; + + fn should_exist(sources: #source_state_type) -> Option { + if matches!(sources, #source_state_value) { + Some(Self::default()) + } else { + None + } + } + } + + impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { + const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { + } + }; + + // panic!("Got Result\n{}", result.to_string()); + + result.into() +} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 42f580f591..89ea9f8fb9 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -49,9 +49,10 @@ pub mod prelude { query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, removal_detection::RemovedComponents, schedule::{ - apply_deferred, apply_state_transition, common_conditions::*, Condition, - IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, OnExit, - OnTransition, Schedule, Schedules, State, StateTransitionEvent, States, SystemSet, + apply_deferred, apply_state_transition, common_conditions::*, ComputedStates, + Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, + OnExit, OnTransition, Schedule, Schedules, State, StateSet, StateTransition, + StateTransitionEvent, States, SubStates, SystemSet, }, system::{ Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs index 4f9d9c38bc..51b70d8949 100644 --- a/crates/bevy_ecs/src/schedule/state.rs +++ b/crates/bevy_ecs/src/schedule/state.rs @@ -1,21 +1,54 @@ +//! In Bevy, states are app-wide interdependent, finite state machines that are generally used to model the large scale structure of your program: whether a game is paused, if the player is in combat, if assets are loaded and so on. +//! +//! This module provides 3 distinct types of state, all of which implement the [`States`] trait: +//! +//! - Standard [`States`] can only be changed by manually setting the [`NextState`] resource. +//! These states are the baseline on which the other state types are built, and can be used on +//! their own for many simple patterns. See the [state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs) +//! for a simple use case. +//! - [`SubStates`] are children of other states - they can be changed manually using [`NextState`], +//! but are removed from the [`World`] if the source states aren't in the right state. See the [sub_states example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/sub_states.rs) +//! for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. +//! - [`ComputedStates`] are fully derived from other states - they provide a [`compute`](ComputedStates::compute) method +//! that takes in the source states and returns their derived value. They are particularly useful for situations +//! where a simplified view of the source states is necessary - such as having an `InAMenu` computed state, derived +//! from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/computed_states.rs) +//! to see usage samples for these states. +//! +//! Most of the utilities around state involve running systems during transitions between states, or +//! determining whether to run certain systems, though they can be used more directly as well. This +//! makes it easier to transition between menus, add loading screens, pause games, and the more. +//! +//! Specifically, Bevy provides the following utilities: +//! +//! - 3 Transition Schedules - [`OnEnter`], [`OnExit`] and [`OnTransition`] - which are used +//! to trigger systems specifically during matching transitions. +//! - A [`StateTransitionEvent`] that gets fired when a given state changes. +//! - The [`in_state`](crate::schedule::condition::in_state) and [`state_changed`](crate::schedule::condition:state_changed) run conditions - which are used +//! to determine whether a system should run based on the current state. + use std::fmt::Debug; use std::hash::Hash; +use std::marker::PhantomData; use std::mem; use std::ops::Deref; use crate as bevy_ecs; -use crate::change_detection::DetectChangesMut; -use crate::event::Event; -use crate::prelude::FromWorld; +use crate::event::{Event, EventReader, EventWriter}; +use crate::prelude::{FromWorld, Local, Res, ResMut}; #[cfg(feature = "bevy_reflect")] use crate::reflect::ReflectResource; use crate::schedule::ScheduleLabel; -use crate::system::Resource; +use crate::system::{Commands, In, IntoSystem, Resource}; use crate::world::World; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::std_traits::ReflectDefault; -pub use bevy_ecs_macros::States; +use bevy_ecs_macros::SystemSet; +pub use bevy_ecs_macros::{States, SubStates}; +use bevy_utils::all_tuples; + +use self::sealed::StateSetSealed; + +use super::{InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, Schedule, Schedules}; /// Types that can define world-wide states in a finite-state machine. /// @@ -26,7 +59,11 @@ pub use bevy_ecs_macros::States; /// and the queued state with the [`NextState`] resource. /// /// State transitions typically occur in the [`OnEnter`] and [`OnExit`] schedules, -/// which can be run via the [`apply_state_transition::`] system. +/// which can be run by triggering the [`StateTransition`] schedule. +/// +/// Types used as [`ComputedStates`] do not need to and should not derive [`States`]. +/// [`ComputedStates`] should not be manually mutated: functionality provided +/// by the [`States`] derive and the associated [`FreelyMutableState`] trait. /// /// # Example /// @@ -42,19 +79,57 @@ pub use bevy_ecs_macros::States; /// } /// /// ``` -pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug {} +pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + /// How many other states this state depends on. + /// Used to help order transitions and de-duplicate [`ComputedStates`], as well as prevent cyclical + /// `ComputedState` dependencies. + const DEPENDENCY_DEPTH: usize = 1; +} -/// The label of a [`Schedule`](super::Schedule) that runs whenever [`State`] +/// This trait allows a state to be mutated directly using the [`NextState`] resource. +/// +/// While ordinary states are freely mutable (and implement this trait as part of their derive macro), +/// computed states are not: instead, they can *only* change when the states that drive them do. +pub trait FreelyMutableState: States { + /// This function registers all the necessary systems to apply state changes and run transition schedules + fn register_state(schedule: &mut Schedule) { + schedule + .add_systems( + apply_state_transition::.in_set(ApplyStateTransition::::apply()), + ) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::ManualTransitions), + ); + } +} + +/// The label of a [`Schedule`] that runs whenever [`State`] /// enters this state. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct OnEnter(pub S); -/// The label of a [`Schedule`](super::Schedule) that runs whenever [`State`] +/// The label of a [`Schedule`] that runs whenever [`State`] /// exits this state. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct OnExit(pub S); -/// The label of a [`Schedule`](super::Schedule) that **only** runs whenever [`State`] +/// The label of a [`Schedule`] that **only** runs whenever [`State`] /// exits the `from` state, AND enters the `to` state. /// /// Systems added to this schedule are always ran *after* [`OnExit`], and *before* [`OnEnter`]. @@ -158,24 +233,29 @@ impl Deref for State { /// next_game_state.set(GameState::InGame); /// } /// ``` -#[derive(Resource, Debug)] +#[derive(Resource, Debug, Default)] #[cfg_attr( feature = "bevy_reflect", derive(bevy_reflect::Reflect), - reflect(Resource, Default) + reflect(Resource) )] -pub struct NextState(pub Option); - -impl Default for NextState { - fn default() -> Self { - Self(None) - } +pub enum NextState { + /// No state transition is pending + #[default] + Unchanged, + /// There is a pending transition for state `S` + Pending(S), } -impl NextState { - /// Tentatively set a planned state transition to `Some(state)`. +impl NextState { + /// Tentatively set a pending state transition to `Some(state)`. pub fn set(&mut self, state: S) { - self.0 = Some(state); + *self = Self::Pending(state); + } + + /// Remove any pending changes to [`State`] + pub fn reset(&mut self) { + *self = Self::Unchanged; } } @@ -185,17 +265,133 @@ impl NextState { #[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] pub struct StateTransitionEvent { /// the state we were in before - pub before: S, + pub before: Option, /// the state we're in now - pub after: S, + pub after: Option, } -/// Run the enter schedule (if it exists) for the current state. -pub fn run_enter_schedule(world: &mut World) { - let Some(state) = world.get_resource::>() else { +/// Runs [state transitions](States). +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct StateTransition; + +/// Applies manual state transitions using [`NextState`]. +/// +/// These system sets are run sequentially, in the order of the enum variants. +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +enum StateTransitionSteps { + ManualTransitions, + DependentTransitions, + ExitSchedules, + TransitionSchedules, + EnterSchedules, +} + +/// Defines a system set to aid with dependent state ordering +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ApplyStateTransition(PhantomData); + +impl ApplyStateTransition { + fn apply() -> Self { + Self(PhantomData) + } +} + +/// This function actually applies a state change, and registers the required +/// schedules for downstream computed states and transition schedules. +/// +/// The `new_state` is an option to allow for removal - `None` will trigger the +/// removal of the `State` resource from the [`World`]. +fn internal_apply_state_transition( + mut event: EventWriter>, + mut commands: Commands, + current_state: Option>>, + new_state: Option, +) { + match new_state { + Some(entered) => { + match current_state { + // If the [`State`] resource exists, and the state is not the one we are + // entering - we need to set the new value, compute dependant states, send transition events + // and register transition schedules. + Some(mut state_resource) => { + if *state_resource != entered { + let exited = mem::replace(&mut state_resource.0, entered.clone()); + + event.send(StateTransitionEvent { + before: Some(exited.clone()), + after: Some(entered.clone()), + }); + } + } + None => { + // If the [`State`] resource does not exist, we create it, compute dependant states, send a transition event and register the `OnEnter` schedule. + commands.insert_resource(State(entered.clone())); + + event.send(StateTransitionEvent { + before: None, + after: Some(entered.clone()), + }); + } + }; + } + None => { + // We first remove the [`State`] resource, and if one existed we compute dependant states, send a transition event and run the `OnExit` schedule. + if let Some(resource) = current_state { + commands.remove_resource::>(); + + event.send(StateTransitionEvent { + before: Some(resource.get().clone()), + after: None, + }); + } + } + } +} + +/// Sets up the schedules and systems for handling state transitions +/// within a [`World`]. +/// +/// Runs automatically when using `App` to insert states, but needs to +/// be added manually in other situations. +pub fn setup_state_transitions_in_world( + world: &mut World, + startup_label: Option, +) { + let mut schedules = world.get_resource_or_insert_with(Schedules::default); + if schedules.contains(StateTransition) { return; - }; - world.try_run_schedule(OnEnter(state.0.clone())).ok(); + } + let mut schedule = Schedule::new(StateTransition); + schedule.configure_sets( + ( + StateTransitionSteps::ManualTransitions, + StateTransitionSteps::DependentTransitions, + StateTransitionSteps::ExitSchedules, + StateTransitionSteps::TransitionSchedules, + StateTransitionSteps::EnterSchedules, + ) + .chain(), + ); + schedules.insert(schedule); + + if let Some(startup) = startup_label { + match schedules.get_mut(startup) { + Some(schedule) => { + schedule.add_systems(|world: &mut World| { + let _ = world.try_run_schedule(StateTransition); + }); + } + None => { + let mut schedule = Schedule::new(startup); + + schedule.add_systems(|world: &mut World| { + let _ = world.try_run_schedule(StateTransition); + }); + + schedules.insert(schedule); + } + } + } } /// If a new state is queued in [`NextState`], this system: @@ -203,38 +399,1119 @@ pub fn run_enter_schedule(world: &mut World) { /// - 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. +/// - Derive any dependent states through the [`ComputeDependantStates::`] schedule, if it exists. /// - Runs the [`OnEnter(entered_state)`] schedule, if it exists. -pub fn apply_state_transition(world: &mut World) { - // We want to take the `NextState` resource, - // but only mark it as changed if it wasn't empty. - let Some(mut next_state_resource) = world.get_resource_mut::>() else { +/// +/// If the [`State`] resource does not exist, it does nothing. Removing or adding states +/// should be done at App creation or at your own risk. +/// +/// For [`SubStates`] - it only applies the state if the `SubState` currently exists. Otherwise, it is wiped. +/// When a `SubState` is re-created, it will use the result of it's `should_exist` method. +pub fn apply_state_transition( + event: EventWriter>, + commands: Commands, + current_state: Option>>, + next_state: Option>>, +) { + // We want to check if the State and NextState resources exist + let Some(next_state_resource) = next_state else { return; }; - if let Some(entered) = next_state_resource.bypass_change_detection().0.take() { - next_state_resource.set_changed(); - match world.get_resource_mut::>() { - Some(mut state_resource) => { - 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 - .try_run_schedule(OnTransition { - from: exited, - to: entered.clone(), - }) - .ok(); - world.try_run_schedule(OnEnter(entered)).ok(); + + match next_state_resource.as_ref() { + NextState::Pending(new_state) => { + if let Some(current_state) = current_state { + if new_state != current_state.get() { + let new_state = new_state.clone(); + internal_apply_state_transition( + event, + commands, + Some(current_state), + Some(new_state), + ); } } - None => { - world.insert_resource(State(entered.clone())); - world.try_run_schedule(OnEnter(entered)).ok(); - } - }; + } + NextState::Unchanged => { + // This is the default value, so we don't need to re-insert the resource + return; + } + } + + *next_state_resource.value = NextState::::Unchanged; +} + +fn should_run_transition( + first: Local, + res: Option>>, + mut event: EventReader>, +) -> (Option>, PhantomData) { + if !*first.0 { + *first.0 = true; + if let Some(res) = res { + event.clear(); + + return ( + Some(StateTransitionEvent { + before: None, + after: Some(res.get().clone()), + }), + PhantomData, + ); + } + } + (event.read().last().cloned(), PhantomData) +} + +fn run_enter( + In((transition, _)): In<(Option>, PhantomData>)>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + + let Some(after) = transition.after else { + return; + }; + + let _ = world.try_run_schedule(OnEnter(after)); +} + +fn run_exit( + In((transition, _)): In<(Option>, PhantomData>)>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + + let Some(before) = transition.before else { + return; + }; + + let _ = world.try_run_schedule(OnExit(before)); +} + +fn run_transition( + In((transition, _)): In<( + Option>, + PhantomData>, + )>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + let Some(from) = transition.before else { + return; + }; + let Some(to) = transition.after else { + return; + }; + + let _ = world.try_run_schedule(OnTransition { from, to }); +} + +/// A state whose value is automatically computed based on the values of other [`States`]. +/// +/// A **computed state** is a state that is deterministically derived from a set of `SourceStates`. +/// The [`StateSet`] is passed into the `compute` method whenever one of them changes, and the +/// result becomes the state's value. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// struct InGame; +/// +/// impl ComputedStates for InGame { +/// /// We set the source state to be the state, or a tuple of states, +/// /// we want to depend on. You can also wrap each state in an Option, +/// /// if you want the computed state to execute even if the state doesn't +/// /// currently exist in the world. +/// type SourceStates = AppState; +/// +/// /// We then define the compute function, which takes in +/// /// your SourceStates +/// fn compute(sources: AppState) -> Option { +/// match sources { +/// /// When we are in game, we want to return the InGame state +/// AppState::InGame { .. } => Some(InGame), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// ``` +/// +/// you can then add it to an App, and from there you use the state as normal +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// +/// # struct App; +/// # impl App { +/// # fn new() -> Self { App } +/// # fn init_state(&mut self) -> &mut Self {self} +/// # fn add_computed_state(&mut self) -> &mut Self {self} +/// # } +/// # struct AppState; +/// # struct InGame; +/// +/// App::new() +/// .init_state::() +/// .add_computed_state::(); +/// ``` +pub trait ComputedStates: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + /// The set of states from which the [`Self`] is derived. + /// + /// This can either be a single type that implements [`States`], an Option of a type + /// that implements [`States`], or a tuple + /// containing multiple types that implement [`States`] or Optional versions of them. + /// + /// For example, `(MapState, EnemyState)` is valid, as is `(MapState, Option)` + type SourceStates: StateSet; + + /// Computes the next value of [`State`]. + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// + /// If the result is [`None`], the [`State`] resource will be removed from the world. + fn compute(sources: Self::SourceStates) -> Option; + + /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) + /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not + /// used. + fn register_computed_state_systems(schedule: &mut Schedule) { + Self::SourceStates::register_computed_state_systems_in_schedule::(schedule); + } +} + +impl States for S { + const DEPENDENCY_DEPTH: usize = S::SourceStates::SET_DEPENDENCY_DEPTH + 1; +} + +mod sealed { + /// Sealed trait used to prevent external implementations of [`StateSet`](super::StateSet). + pub trait StateSetSealed {} +} + +/// A [`States`] type or tuple of types which implement [`States`]. +/// +/// This trait is used allow implementors of [`States`], as well +/// as tuples containing exclusively implementors of [`States`], to +/// be used as [`ComputedStates::SourceStates`]. +/// +/// It is sealed, and auto implemented for all [`States`] types and +/// tuples containing them. +pub trait StateSet: sealed::StateSetSealed { + /// The total [`DEPENDENCY_DEPTH`](`States::DEPENDENCY_DEPTH`) of all + /// the states that are part of this [`StateSet`], added together. + /// + /// Used to de-duplicate computed state executions and prevent cyclic + /// computed states. + const SET_DEPENDENCY_DEPTH: usize; + + /// Sets up the systems needed to compute `T` whenever any `State` in this + /// `StateSet` is changed. + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ); + + /// Sets up the systems needed to compute whether `T` exists whenever any `State` in this + /// `StateSet` is changed. + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ); +} + +/// The `InnerStateSet` trait is used to isolate [`ComputedStates`] & [`SubStates`] from +/// needing to wrap all state dependencies in an [`Option`]. +/// +/// Some [`ComputedStates`]'s might need to exist in different states based on the existence +/// of other states. So we needed the ability to use[`Option`] when appropriate. +/// +/// The isolation works because it is implemented for both S & [`Option`], and has the `RawState` associated type +/// that allows it to know what the resource in the world should be. We can then essentially "unwrap" it in our +/// `StateSet` implementation - and the behaviour of that unwrapping will depend on the arguments expected by the +/// the [`ComputedStates`] & [`SubStates]`. +trait InnerStateSet: Sized { + type RawState: States; + + const DEPENDENCY_DEPTH: usize; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option; +} + +impl InnerStateSet for S { + type RawState = Self; + + const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option { + wrapped.map(|v| v.0.clone()) + } +} + +impl InnerStateSet for Option { + type RawState = S; + + const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option { + Some(wrapped.map(|v| v.0.clone())) + } +} + +impl StateSetSealed for S {} + +impl StateSet for S { + const SET_DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |mut parent_changed: EventReader>, + event: EventWriter>, + commands: Commands, + current_state: Option>>, + state_set: Option>>| { + if parent_changed.is_empty() { + return; + } + parent_changed.clear(); + + let new_state = + if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { + T::compute(state_set) + } else { + None + }; + + internal_apply_state_transition(event, commands, current_state, new_state); + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + .after(ApplyStateTransition::::apply()), + ); + } + + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |mut parent_changed: EventReader>, + event: EventWriter>, + commands: Commands, + current_state: Option>>, + state_set: Option>>| { + if parent_changed.is_empty() { + return; + } + parent_changed.clear(); + + let new_state = + if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { + T::should_exist(state_set) + } else { + None + }; + + match new_state { + Some(value) => { + if current_state.is_none() { + internal_apply_state_transition( + event, + commands, + current_state, + Some(value), + ); + } + } + None => { + internal_apply_state_transition(event, commands, current_state, None); + } + }; + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems( + apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions), + ) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + .after(ApplyStateTransition::::apply()), + ); + } +} + +/// A sub-state is a state that exists only when the source state meet certain conditions, +/// but unlike [`ComputedStates`] - while they exist they can be manually modified. +/// +/// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state +/// and value to determine it's existence. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame +/// } +/// +/// +/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// #[source(AppState = AppState::InGame)] +/// enum GamePhase { +/// #[default] +/// Setup, +/// Battle, +/// Conclusion +/// } +/// ``` +/// +/// you can then add it to an App, and from there you use the state as normal: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// +/// # struct App; +/// # impl App { +/// # fn new() -> Self { App } +/// # fn init_state(&mut self) -> &mut Self {self} +/// # fn add_sub_state(&mut self) -> &mut Self {self} +/// # } +/// # struct AppState; +/// # struct GamePhase; +/// +/// App::new() +/// .init_state::() +/// .add_sub_state::(); +/// ``` +/// +/// In more complex situations, the recommendation is to use an intermediary computed state, like so: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// struct InGame; +/// +/// impl ComputedStates for InGame { +/// /// We set the source state to be the state, or set of states, +/// /// we want to depend on. Any of the states can be wrapped in an Option. +/// type SourceStates = Option; +/// +/// /// We then define the compute function, which takes in the AppState +/// fn compute(sources: Option) -> Option { +/// match sources { +/// /// When we are in game, we want to return the InGame state +/// Some(AppState::InGame { .. }) => Some(InGame), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// +/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// #[source(InGame = InGame)] +/// enum GamePhase { +/// #[default] +/// Setup, +/// Battle, +/// Conclusion +/// } +/// ``` +/// +/// However, you can also manually implement them. If you do so, you'll also need to manually implement the `States` & `FreelyMutableState` traits. +/// Unlike the derive, this does not require an implementation of [`Default`], since you are providing the `exists` function +/// directly. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_ecs::schedule::FreelyMutableState; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// enum GamePhase { +/// Setup, +/// Battle, +/// Conclusion +/// } +/// +/// impl SubStates for GamePhase { +/// /// We set the source state to be the state, or set of states, +/// /// we want to depend on. Any of the states can be wrapped in an Option. +/// type SourceStates = Option; +/// +/// /// We then define the compute function, which takes in the [`Self::SourceStates`] +/// fn should_exist(sources: Option) -> Option { +/// match sources { +/// /// When we are in game, so we want a GamePhase state to exist, and the default is +/// /// GamePhase::Setup +/// Some(AppState::InGame { .. }) => Some(GamePhase::Setup), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// +/// impl States for GamePhase { +/// const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; +/// } +/// +/// impl FreelyMutableState for GamePhase {} +/// ``` +pub trait SubStates: States + FreelyMutableState { + /// The set of states from which the [`Self`] is derived. + /// + /// This can either be a single type that implements [`States`], or a tuple + /// containing multiple types that implement [`States`], or any combination of + /// types implementing [`States`] and Options of types implementing [`States`] + type SourceStates: StateSet; + + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// The result is used to determine the existence of [`State`]. + /// + /// If the result is [`None`], the [`State`] resource will be removed from the world, otherwise + /// if the [`State`] resource doesn't exist - it will be created with the [`Some`] value. + fn should_exist(sources: Self::SourceStates) -> Option; + + /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) + /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not + /// used. + fn register_sub_state_systems(schedule: &mut Schedule) { + Self::SourceStates::register_sub_state_systems_in_schedule::(schedule); + } +} + +macro_rules! impl_state_set_sealed_tuples { + ($(($param: ident, $val: ident, $evt: ident)), *) => { + impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} + + impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { + + const SET_DEPENDENCY_DEPTH : usize = $($param::DEPENDENCY_DEPTH +)* 0; + + + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { + if ($($evt.is_empty())&&*) { + return; + } + $($evt.clear();)* + + let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { + T::compute(($($val),*, )) + } else { + None + }; + + internal_apply_state_transition(event, commands, current_state, new_state); + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) + .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) + .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + $(.after(ApplyStateTransition::<$param::RawState>::apply()))* + ); + } + + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { + if ($($evt.is_empty())&&*) { + return; + } + $($evt.clear();)* + + let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { + T::should_exist(($($val),*, )) + } else { + None + }; + match new_state { + Some(value) => { + if current_state.is_none() { + internal_apply_state_transition(event, commands, current_state, Some(value)); + } + } + None => { + internal_apply_state_transition(event, commands, current_state, None); + }, + }; + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems(apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions)) + .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) + .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) + .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + $(.after(ApplyStateTransition::<$param::RawState>::apply()))* + ); + } + } + }; +} + +all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader); + +#[cfg(test)] +mod tests { + use bevy_ecs_macros::SubStates; + + use super::*; + use crate as bevy_ecs; + + use crate::event::EventRegistry; + + use crate::prelude::ResMut; + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum SimpleState { + #[default] + A, + B(bool), + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum TestComputedState { + BisTrue, + BisFalse, + } + + impl ComputedStates for TestComputedState { + type SourceStates = Option; + + fn compute(sources: Option) -> Option { + sources.and_then(|source| match source { + SimpleState::A => None, + SimpleState::B(value) => Some(if value { Self::BisTrue } else { Self::BisFalse }), + }) + } + } + + #[test] + fn computed_state_with_a_single_source_is_correctly_derived() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + TestComputedState::register_computed_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + TestComputedState::BisTrue + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert_eq!( + world.resource::>().0, + TestComputedState::BisFalse + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + } + + #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] + #[source(SimpleState = SimpleState::B(true))] + enum SubState { + #[default] + One, + Two, + } + + #[test] + fn sub_state_exists_only_when_allowed_but_can_be_modified_freely() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SubState::register_sub_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SubState::Two)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SubState::One); + + world.insert_resource(NextState::Pending(SubState::Two)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SubState::Two); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert!(!world.contains_resource::>()); + } + + #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] + #[source(TestComputedState = TestComputedState::BisTrue)] + enum SubStateOfComputed { + #[default] + One, + Two, + } + + #[test] + fn substate_of_computed_states_works_appropriately() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + TestComputedState::register_computed_state_systems(&mut apply_changes); + SubStateOfComputed::register_sub_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + SubStateOfComputed::One + ); + + world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + SubStateOfComputed::Two + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert!(!world.contains_resource::>()); + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + struct OtherState { + a_flexible_value: &'static str, + another_value: u8, + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum ComplexComputedState { + InAAndStrIsBobOrJane, + InTrueBAndUsizeAbove8, + } + + impl ComputedStates for ComplexComputedState { + type SourceStates = (Option, Option); + + fn compute(sources: (Option, Option)) -> Option { + match sources { + (Some(simple), Some(complex)) => { + if simple == SimpleState::A + && (complex.a_flexible_value == "bob" || complex.a_flexible_value == "jane") + { + Some(ComplexComputedState::InAAndStrIsBobOrJane) + } else if simple == SimpleState::B(true) && complex.another_value > 8 { + Some(ComplexComputedState::InTrueBAndUsizeAbove8) + } else { + None + } + } + _ => None, + } + } + } + + #[test] + fn complex_computed_state_gets_derived_correctly() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + + ComplexComputedState::register_computed_state_systems(&mut apply_changes); + + SimpleState::register_state(&mut apply_changes); + OtherState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!( + world.resource::>().0, + OtherState::default() + ); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "felix", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + ComplexComputedState::InTrueBAndUsizeAbove8 + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "jane", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + ComplexComputedState::InAAndStrIsBobOrJane + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "jane", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + } + + #[derive(Resource, Default)] + struct ComputedStateTransitionCounter { + enter: usize, + exit: usize, + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum SimpleState2 { + #[default] + A1, + B2, + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum TestNewcomputedState { + A1, + B2, + B1, + } + + impl ComputedStates for TestNewcomputedState { + type SourceStates = (Option, Option); + + fn compute((s1, s2): (Option, Option)) -> Option { + match (s1, s2) { + (Some(SimpleState::A), Some(SimpleState2::A1)) => Some(TestNewcomputedState::A1), + (Some(SimpleState::B(true)), Some(SimpleState2::B2)) => { + Some(TestNewcomputedState::B2) + } + (Some(SimpleState::B(true)), _) => Some(TestNewcomputedState::B1), + _ => None, + } + } + } + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct Startup; + + #[test] + fn computed_state_transitions_are_produced_correctly() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + world.init_resource::>(); + world.init_resource::(); + + setup_state_transitions_in_world(&mut world, Some(Startup.intern())); + + let mut schedules = world + .get_resource_mut::() + .expect("Schedules don't exist in world"); + let apply_changes = schedules + .get_mut(StateTransition) + .expect("State Transition Schedule Doesn't Exist"); + + TestNewcomputedState::register_computed_state_systems(apply_changes); + + SimpleState::register_state(apply_changes); + SimpleState2::register_state(apply_changes); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::A1)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::A1)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B1)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B1)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B2)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B2)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + world.init_resource::(); + + setup_state_transitions_in_world(&mut world, None); + + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::B2 + ); + assert_eq!(world.resource::().enter, 1); + assert_eq!(world.resource::().exit, 0); + + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::A1 + ); + assert_eq!( + world.resource::().enter, + 2, + "Should Only Enter Twice" + ); + assert_eq!( + world.resource::().exit, + 1, + "Should Only Exit Once" + ); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::B2 + ); + assert_eq!( + world.resource::().enter, + 3, + "Should Only Enter Three Times" + ); + assert_eq!( + world.resource::().exit, + 2, + "Should Only Exit Twice" + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + assert_eq!( + world.resource::().enter, + 3, + "Should Only Enter Three Times" + ); + assert_eq!( + world.resource::().exit, + 3, + "Should Only Exit Twice" + ); } } diff --git a/examples/README.md b/examples/README.md index a4e9b95e1c..8e696659b8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -246,6 +246,7 @@ Example | Description --- | --- [Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components [Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events +[Computed States](../examples/ecs/computed_states.rs) | Advanced state patterns using Computed States [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components @@ -263,6 +264,7 @@ Example | Description [Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system [Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up) [State](../examples/ecs/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state +[Sub States](../examples/ecs/sub_states.rs) | Using Sub States for hierarchical state handling. [System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state [System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully diff --git a/examples/ecs/computed_states.rs b/examples/ecs/computed_states.rs new file mode 100644 index 0000000000..888d8de79e --- /dev/null +++ b/examples/ecs/computed_states.rs @@ -0,0 +1,686 @@ +//! 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::*; + +// 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(); +} + +#[derive(Resource)] +struct MenuData { + root_entity: Entity, +} + +#[derive(Component, PartialEq, Eq)] +enum MenuButton { + Play, + Tutorial, +} + +const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); +const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); + +const ACTIVE_BUTTON: Color = Color::srgb(0.15, 0.85, 0.15); +const HOVERED_ACTIVE_BUTTON: Color = Color::srgb(0.25, 0.55, 0.25); +const PRESSED_ACTIVE_BUTTON: Color = Color::srgb(0.35, 0.95, 0.35); + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); +} + +fn setup_menu(mut commands: Commands, tutorial_state: Res>) { + let button_entity = commands + .spawn(NodeBundle { + style: Style { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.), + ..default() + }, + ..default() + }) + .with_children(|parent| { + parent + .spawn(( + ButtonBundle { + style: Style { + width: Val::Px(200.), + height: Val::Px(65.), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + image: UiImage::default().with_color(NORMAL_BUTTON), + ..default() + }, + MenuButton::Play, + )) + .with_children(|parent| { + parent.spawn(TextBundle::from_section( + "Play", + TextStyle { + font_size: 40.0, + color: Color::srgb(0.9, 0.9, 0.9), + ..default() + }, + )); + }); + + parent + .spawn(( + ButtonBundle { + style: Style { + width: Val::Px(200.), + height: Val::Px(65.), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + image: UiImage::default().with_color(match tutorial_state.get() { + TutorialState::Active => ACTIVE_BUTTON, + TutorialState::Inactive => NORMAL_BUTTON, + }), + ..default() + }, + MenuButton::Tutorial, + )) + .with_children(|parent| { + parent.spawn(TextBundle::from_section( + "Tutorial", + TextStyle { + font_size: 40.0, + color: Color::srgb(0.9, 0.9, 0.9), + ..default() + }, + )); + }); + }) + .id(); + commands.insert_resource(MenuData { + root_entity: button_entity, + }); +} + +fn menu( + mut next_state: ResMut>, + tutorial_state: Res>, + mut next_tutorial: ResMut>, + mut interaction_query: Query< + (&Interaction, &mut UiImage, &MenuButton), + (Changed, With