diff --git a/Cargo.toml b/Cargo.toml index c05a4e52d92ed..a3e23ca0eeb7a 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 c8f229cf41d7a..80fb05f2b9ac2 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. + /// 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 + } + + /// 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. /// - /// [`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); + /// 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 a collection of systems to `schedule` (stored in the main world's [`Schedules`]). + /// 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 e338a45664609..38d8d0e280970 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 0d3a804df750b..2399cf01c1216 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 3f44fafd9269d..4b0eab29273ee 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 abc6647a8bc52..b2896fbeafe25 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 75a43e8e481f6..dbca96db17e76 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 0eb516c31563d..ff69812aea380 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 42f580f59178c..89ea9f8fb915a 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 4f9d9c38bccd1..51b70d894966f 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; +} + +/// 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`](super::Schedule) that runs whenever [`State`] +/// 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, +} + +/// 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, + }); + } + } + } } -/// 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 { +/// 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 a4e9b95e1ce3a..8e696659b8146 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 0000000000000..888d8de79e90a --- /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