diff --git a/Cargo.toml b/Cargo.toml index 6166492496..14a405ce4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ workspace = true default = [ "animation", "bevy_asset", + "bevy_state", "bevy_audio", "bevy_color", "bevy_gilrs", @@ -334,6 +335,9 @@ meshlet_processor = ["bevy_internal/meshlet_processor"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_internal/ios_simulator"] +# Enable built in global state machines +bevy_state = ["bevy_internal/bevy_state"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false } @@ -1729,35 +1733,35 @@ wasm = false [[example]] name = "state" -path = "examples/ecs/state.rs" +path = "examples/state/state.rs" doc-scrape-examples = true [package.metadata.example.state] name = "State" description = "Illustrates how to use States to control transitioning from a Menu state to an InGame state" -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] name = "sub_states" -path = "examples/ecs/sub_states.rs" +path = "examples/state/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)" +category = "State" wasm = false [[example]] name = "computed_states" -path = "examples/ecs/computed_states.rs" +path = "examples/state/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)" +category = "State" wasm = false [[example]] diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 64333bfc98..1b4cfdb791 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -11,9 +11,10 @@ keywords = ["bevy"] [features] trace = [] bevy_debug_stepping = [] -default = ["bevy_reflect"] +default = ["bevy_reflect", "bevy_state"] bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] serialize = ["bevy_ecs/serde"] +bevy_state = ["dep:bevy_state"] [dependencies] # bevy @@ -22,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = fa bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 80fb05f2b9..176c5caf60 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -7,9 +7,11 @@ use bevy_ecs::{ event::{event_update_system, ManualEventReader}, intern::Interned, prelude::*, - schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel}, + schedule::{ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; +#[cfg(feature = "bevy_state")] +use bevy_state::{prelude::*, state::FreelyMutableState}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::{tracing::debug, HashMap}; @@ -264,6 +266,7 @@ impl App { self.sub_apps.iter().any(|s| s.is_building_plugins()) } + #[cfg(feature = "bevy_state")] /// Initializes a [`State`] with standard starting values. /// /// This method is idempotent: it has no effect when called again using the same generic type. @@ -281,6 +284,7 @@ impl App { self } + #[cfg(feature = "bevy_state")] /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// added of the same type. /// @@ -297,23 +301,19 @@ impl App { self } + #[cfg(feature = "bevy_state")] /// 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 } + #[cfg(feature = "bevy_state")] /// 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 @@ -983,10 +983,7 @@ impl Termination for AppExit { mod tests { use std::{marker::PhantomData, mem}; - use bevy_ecs::{ - schedule::{OnEnter, States}, - system::Commands, - }; + use bevy_ecs::{schedule::ScheduleLabel, system::Commands}; use crate::{App, AppExit, Plugin}; @@ -1059,11 +1056,9 @@ mod tests { App::new().add_plugins(PluginRun); } - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum AppState { - #[default] - MainMenu, - } + #[derive(ScheduleLabel, Hash, Clone, PartialEq, Eq, Debug)] + struct EnterMainMenu; + fn bar(mut commands: Commands) { commands.spawn_empty(); } @@ -1075,20 +1070,9 @@ mod tests { #[test] fn add_systems_should_create_schedule_if_it_does_not_exist() { let mut app = App::new(); - app.init_state::() - .add_systems(OnEnter(AppState::MainMenu), (foo, bar)); + app.add_systems(EnterMainMenu, (foo, bar)); - app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); - assert_eq!(app.world().entities().len(), 2); - } - - #[test] - fn add_systems_should_create_schedule_if_it_does_not_exist2() { - let mut app = App::new(); - app.add_systems(OnEnter(AppState::MainMenu), (foo, bar)) - .init_state::(); - - app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); + app.world_mut().run_schedule(EnterMainMenu); assert_eq!(app.world().entities().len(), 2); } diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 2399cf01c1..16e5871e41 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -1,9 +1,11 @@ use crate::{App, Plugin}; use bevy_ecs::{ - schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition}, + schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel}, system::{Local, Resource}, world::{Mut, World}, }; +#[cfg(feature = "bevy_state")] +use bevy_state::state::StateTransition; /// The schedule that contains the app logic that is evaluated each tick of [`App::update()`]. /// diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 0aa2e60338..b6974cc60f 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -2,12 +2,15 @@ use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup}; use bevy_ecs::{ event::EventRegistry, prelude::*, - schedule::{ - setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel, - ScheduleBuildSettings, ScheduleLabel, - }, + schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; +#[cfg(feature = "bevy_state")] +use bevy_state::{ + prelude::*, + state::{setup_state_transitions_in_world, FreelyMutableState}, +}; + #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::{HashMap, HashSet}; @@ -295,6 +298,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::init_state`]. pub fn init_state(&mut self) -> &mut Self { if !self.world.contains_resource::>() { @@ -309,6 +313,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::insert_state`]. pub fn insert_state(&mut self, state: S) -> &mut Self { if !self.world.contains_resource::>() { @@ -324,6 +329,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::add_computed_state`]. pub fn add_computed_state(&mut self) -> &mut Self { if !self @@ -339,6 +345,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::add_sub_state`]. pub fn add_sub_state(&mut self) -> &mut Self { if !self diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 89ea9f8fb9..ca6e42f3c9 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -49,10 +49,8 @@ pub mod prelude { query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, removal_detection::RemovedComponents, schedule::{ - 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, + apply_deferred, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet, + IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, system::{ Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 4997bfd110..7d2f1ab617 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -194,15 +194,12 @@ mod sealed { /// A collection of [run conditions](Condition) that may be useful in any bevy app. pub mod common_conditions { - use bevy_utils::warn_once; - use super::NotSystem; use crate::{ change_detection::DetectChanges, event::{Event, EventReader}, prelude::{Component, Query, With}, removal_detection::RemovedComponents, - schedule::{State, States}, system::{IntoSystem, Res, Resource, System}, }; @@ -657,173 +654,6 @@ pub mod common_conditions { } } - /// A [`Condition`](super::Condition)-satisfying system that returns `true` - /// if the state machine exists. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// app.add_systems( - /// // `state_exists` will only return true if the - /// // given state exists - /// my_system.run_if(state_exists::), - /// ); - /// - /// fn my_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// // `GameState` does not yet exist `my_system` won't run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 0); - /// - /// world.init_resource::>(); - /// - /// // `GameState` now exists so `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// ``` - pub fn state_exists(current_state: Option>>) -> bool { - current_state.is_some() - } - - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` - /// if the state machine is currently in `state`. - /// - /// Will return `false` if the state does not exist or if not in `state`. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// world.init_resource::>(); - /// - /// app.add_systems(( - /// // `in_state` will only return true if the - /// // given state equals the given value - /// play_system.run_if(in_state(GameState::Playing)), - /// pause_system.run_if(in_state(GameState::Paused)), - /// )); - /// - /// fn play_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// fn pause_system(mut counter: ResMut) { - /// counter.0 -= 1; - /// } - /// - /// // We default to `GameState::Playing` so `play_system` runs - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// *world.resource_mut::>() = State::new(GameState::Paused); - /// - /// // Now that we are in `GameState::Pause`, `pause_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 0); - /// ``` - pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { - move |current_state: Option>>| match current_state { - Some(current_state) => *current_state == state, - None => { - warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", { - let debug_state = format!("{state:?}"); - let result = debug_state - .split("::") - .next() - .unwrap_or("Unknown State Type"); - result.to_string() - }); - - false - } - } - } - - /// A [`Condition`](super::Condition)-satisfying system that returns `true` - /// if the state machine changed state. - /// - /// To do things on transitions to/from specific states, use their respective OnEnter/OnExit - /// schedules. Use this run condition if you want to detect any change, regardless of the value. - /// - /// Returns false if the state does not exist or the state has not changed. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// world.init_resource::>(); - /// - /// app.add_systems( - /// // `state_changed` will only return true if the - /// // given states value has just been updated or - /// // the state has just been added - /// my_system.run_if(state_changed::), - /// ); - /// - /// fn my_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// // `GameState` has just been added so `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// // `GameState` has not been updated so `my_system` will not run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// *world.resource_mut::>() = State::new(GameState::Paused); - /// - /// // Now that `GameState` has been updated `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 2); - /// ``` - pub fn state_changed(current_state: Option>>) -> bool { - let Some(current_state) = current_state else { - return false; - }; - current_state.is_changed() - } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if there are any new events of the given type since it was last called. /// @@ -1032,7 +862,6 @@ mod tests { use crate as bevy_ecs; use crate::component::Component; use crate::schedule::IntoSystemConfigs; - use crate::schedule::{State, States}; use crate::system::Local; use crate::{change_detection::ResMut, schedule::Schedule, world::World}; use bevy_ecs_macros::Event; @@ -1131,20 +960,15 @@ mod tests { schedule.run(&mut world); assert_eq!(world.resource::().0, 0); } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum TestState { - #[default] - A, - B, - } - #[derive(Component)] struct TestComponent; #[derive(Event)] struct TestEvent; + #[derive(Resource)] + struct TestResource(()); + fn test_system() {} // Ensure distributive_run_if compiles with the common conditions. @@ -1153,15 +977,12 @@ mod tests { Schedule::default().add_systems( (test_system, test_system) .distributive_run_if(run_once()) - .distributive_run_if(resource_exists::>) - .distributive_run_if(resource_added::>) - .distributive_run_if(resource_changed::>) - .distributive_run_if(resource_exists_and_changed::>) - .distributive_run_if(resource_changed_or_removed::>()) - .distributive_run_if(resource_removed::>()) - .distributive_run_if(state_exists::) - .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) - .distributive_run_if(state_changed::) + .distributive_run_if(resource_exists::) + .distributive_run_if(resource_added::) + .distributive_run_if(resource_changed::) + .distributive_run_if(resource_exists_and_changed::) + .distributive_run_if(resource_changed_or_removed::()) + .distributive_run_if(resource_removed::()) .distributive_run_if(on_event::()) .distributive_run_if(any_with_component::) .distributive_run_if(not(run_once())), diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index b38f7adb67..184bef250a 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -7,7 +7,6 @@ mod graph_utils; #[allow(clippy::module_inception)] mod schedule; mod set; -mod state; mod stepping; pub use self::condition::*; @@ -16,7 +15,6 @@ pub use self::executor::*; use self::graph_utils::*; pub use self::schedule::*; pub use self::set::*; -pub use self::state::*; pub use self::graph_utils::NodeId; diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs deleted file mode 100644 index 0cec393d6f..0000000000 --- a/crates/bevy_ecs/src/schedule/state.rs +++ /dev/null @@ -1,1504 +0,0 @@ -//! 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::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::{Commands, In, IntoSystem, Resource}; -use crate::world::World; - -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. -/// -/// The [`Default`] trait defines the starting state. -/// Multiple states can be defined for the same world, -/// allowing you to classify the state of the world across orthogonal dimensions. -/// You can access the current state of type `T` with the [`State`] resource, -/// and the queued state with the [`NextState`] resource. -/// -/// State transitions typically occur in the [`OnEnter`] and [`OnExit`] schedules, -/// 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 -/// -/// ``` -/// use bevy_ecs::prelude::States; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// ``` -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`] that runs whenever [`State`] -/// enters this state. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnEnter(pub S); - -/// 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`] 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`]. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnTransition { - /// The state being exited. - pub from: S, - /// The state being entered. - pub to: S, -} - -/// A finite-state machine whose transitions have associated schedules -/// ([`OnEnter(state)`] and [`OnExit(state)`]). -/// -/// The current state value can be accessed through this resource. To *change* the state, -/// queue a transition in the [`NextState`] resource, and it will be applied by the next -/// [`apply_state_transition::`] system. -/// -/// The starting state is defined via the [`Default`] implementation for `S`. -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// fn game_logic(game_state: Res>) { -/// match game_state.get() { -/// GameState::InGame => { -/// // Run game logic here... -/// }, -/// _ => {}, -/// } -/// } -/// ``` -#[derive(Resource, Debug)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource) -)] -pub struct State(S); - -impl State { - /// Creates a new state with a specific value. - /// - /// To change the state use [`NextState`] rather than using this to modify the `State`. - pub fn new(state: S) -> Self { - Self(state) - } - - /// Get the current state. - pub fn get(&self) -> &S { - &self.0 - } -} - -impl FromWorld for State { - fn from_world(world: &mut World) -> Self { - Self(S::from_world(world)) - } -} - -impl PartialEq for State { - fn eq(&self, other: &S) -> bool { - self.get() == other - } -} - -impl Deref for State { - type Target = S; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -/// The next state of [`State`]. -/// -/// To queue a transition, just set the contained value to `Some(next_state)`. -/// Note that these transitions can be overridden by other systems: -/// only the actual value of this resource at the time of [`apply_state_transition`] matters. -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// fn start_game(mut next_game_state: ResMut>) { -/// next_game_state.set(GameState::InGame); -/// } -/// ``` -#[derive(Resource, Debug, Default)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource) -)] -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 pending state transition to `Some(state)`. - pub fn set(&mut self, state: S) { - *self = Self::Pending(state); - } - - /// Remove any pending changes to [`State`] - pub fn reset(&mut self) { - *self = Self::Unchanged; - } -} - -/// Event sent when any state transition of `S` happens. -/// -/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] -pub struct StateTransitionEvent { - /// the state we were in before - pub before: Option, - /// the state we're in now - 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, - }); - } - } - } -} - -/// 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; - } - 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 { - schedules.add_systems(startup, |world: &mut World| { - let _ = world.try_run_schedule(StateTransition); - }); - } -} - -/// If a new state is queued in [`NextState`], this system: -/// - Takes the new state value from [`NextState`] and updates [`State`]. -/// - 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. -/// -/// 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; - }; - - 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), - ); - } - } - } - 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/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 967bf850de..3bb22f9409 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -5,10 +5,6 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_utils::HashSet; use std::hash::Hash; -// unused import, but needed for intra doc link to work -#[allow(unused_imports)] -use bevy_ecs::schedule::State; - /// A "press-able" input of type `T`. /// /// ## Usage @@ -23,8 +19,8 @@ use bevy_ecs::schedule::State; /// ## Multiple systems /// /// In case multiple systems are checking for [`ButtonInput::just_pressed`] or [`ButtonInput::just_released`] -/// but only one should react, for example in the case of triggering -/// [`State`] change, you should consider clearing the input state, either by: +/// but only one should react, for example when modifying a +/// [`Resource`], you should consider clearing the input state, either by: /// /// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead. /// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 0e4713bba9..3703835038 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -179,6 +179,9 @@ bevy_dev_tools = ["dep:bevy_dev_tools"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] +# Enable built in global state machines +bevy_state = ["dep:bevy_state", "bevy_app/bevy_state"] + [dependencies] # bevy bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" } @@ -187,6 +190,7 @@ bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 7eb982634e..f7f828c986 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -52,6 +52,8 @@ pub use bevy_render as render; pub use bevy_scene as scene; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; +#[cfg(feature = "bevy_state")] +pub use bevy_state as state; pub use bevy_tasks as tasks; #[cfg(feature = "bevy_text")] pub use bevy_text as text; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index aa25672781..7566246296 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -62,3 +62,7 @@ pub use crate::gizmos::prelude::*; #[doc(hidden)] #[cfg(feature = "bevy_gilrs")] pub use crate::gilrs::*; + +#[doc(hidden)] +#[cfg(feature = "bevy_state")] +pub use crate::state::prelude::*; diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml new file mode 100644 index 0000000000..009d283eae --- /dev/null +++ b/crates/bevy_state/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "bevy_state" +version = "0.14.0-dev" +edition = "2021" +description = "Bevy Engine's entity component system" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["ecs", "game", "bevy"] +categories = ["game-engines", "data-structures"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["bevy_reflect"] + + +[dependencies] +bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_state_macros = { path = "macros", version = "0.14.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml new file mode 100644 index 0000000000..70ff618e47 --- /dev/null +++ b/crates/bevy_state/macros/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_state_macros" +version = "0.14.0-dev" +description = "Bevy ECS Macros" +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } + +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs new file mode 100644 index 0000000000..7d401a4793 --- /dev/null +++ b/crates/bevy_state/macros/src/lib.rs @@ -0,0 +1,24 @@ +// FIXME(3492): remove once docs are ready +#![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +extern crate proc_macro; + +mod states; + +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; + +#[proc_macro_derive(States)] +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) +} + +pub(crate) fn bevy_state_path() -> syn::Path { + BevyManifest::default().get_path("bevy_state") +} diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs new file mode 100644 index 0000000000..76a6cbcddf --- /dev/null +++ b/crates/bevy_state/macros/src/states.rs @@ -0,0 +1,140 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; + +use crate::bevy_state_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 base_trait_path = bevy_state_path(); + base_trait_path.segments.push(format_ident!("state").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_state_path(); + base_trait_path.segments.push(format_ident!("state").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_state/src/condition.rs b/crates/bevy_state/src/condition.rs new file mode 100644 index 0000000000..c0ff5abe49 --- /dev/null +++ b/crates/bevy_state/src/condition.rs @@ -0,0 +1,204 @@ +use bevy_ecs::{change_detection::DetectChanges, system::Res}; +use bevy_utils::warn_once; + +use crate::state::{State, States}; + +/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// if the state machine exists. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// app.add_systems( +/// // `state_exists` will only return true if the +/// // given state exists +/// my_system.run_if(state_exists::), +/// ); +/// +/// fn my_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// // `GameState` does not yet exist `my_system` won't run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 0); +/// +/// world.init_resource::>(); +/// +/// // `GameState` now exists so `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// ``` +pub fn state_exists(current_state: Option>>) -> bool { + current_state.is_some() +} + +/// Generates a [`Condition`](bevy_ecs::prelude::Condition)-satisfying closure that returns `true` +/// if the state machine is currently in `state`. +/// +/// Will return `false` if the state does not exist or if not in `state`. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// world.init_resource::>(); +/// +/// app.add_systems(( +/// // `in_state` will only return true if the +/// // given state equals the given value +/// play_system.run_if(in_state(GameState::Playing)), +/// pause_system.run_if(in_state(GameState::Paused)), +/// )); +/// +/// fn play_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// fn pause_system(mut counter: ResMut) { +/// counter.0 -= 1; +/// } +/// +/// // We default to `GameState::Playing` so `play_system` runs +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// *world.resource_mut::>() = State::new(GameState::Paused); +/// +/// // Now that we are in `GameState::Pause`, `pause_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 0); +/// ``` +pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { + move |current_state: Option>>| match current_state { + Some(current_state) => *current_state == state, + None => { + warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", { + let debug_state = format!("{state:?}"); + let result = debug_state + .split("::") + .next() + .unwrap_or("Unknown State Type"); + result.to_string() + }); + + false + } + } +} + +/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// if the state machine changed state. +/// +/// To do things on transitions to/from specific states, use their respective OnEnter/OnExit +/// schedules. Use this run condition if you want to detect any change, regardless of the value. +/// +/// Returns false if the state does not exist or the state has not changed. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// world.init_resource::>(); +/// +/// app.add_systems( +/// // `state_changed` will only return true if the +/// // given states value has just been updated or +/// // the state has just been added +/// my_system.run_if(state_changed::), +/// ); +/// +/// fn my_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// // `GameState` has just been added so `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// // `GameState` has not been updated so `my_system` will not run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// *world.resource_mut::>() = State::new(GameState::Paused); +/// +/// // Now that `GameState` has been updated `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 2); +/// ``` +pub fn state_changed(current_state: Option>>) -> bool { + let Some(current_state) = current_state else { + return false; + }; + current_state.is_changed() +} + +#[cfg(test)] +mod tests { + use crate as bevy_state; + + use bevy_ecs::schedule::{Condition, IntoSystemConfigs, Schedule}; + + use crate::prelude::*; + use bevy_state_macros::States; + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum TestState { + #[default] + A, + B, + } + + fn test_system() {} + + // Ensure distributive_run_if compiles with the common conditions. + #[test] + fn distributive_run_if_compiles() { + Schedule::default().add_systems( + (test_system, test_system) + .distributive_run_if(state_exists::) + .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) + .distributive_run_if(state_changed::), + ); + } +} diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs new file mode 100644 index 0000000000..2104dcedcd --- /dev/null +++ b/crates/bevy_state/src/lib.rs @@ -0,0 +1,44 @@ +//! 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`](state::States) trait: +//! +//! - Standard [`States`](state::States) can only be changed by manually setting the [`NextState`](state::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`](state::SubStates) are children of other states - they can be changed manually using [`NextState`](state::NextState), +//! but are removed from the [`World`](bevy_ecs::prelude::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`](state::ComputedStates) are fully derived from other states - they provide a [`compute`](state::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`](crate::state::OnEnter), [`OnExit`](crate::state::OnExit) and [`OnTransition`](crate::state::OnTransition) - which are used +//! to trigger systems specifically during matching transitions. +//! - A [`StateTransitionEvent`](crate::state::StateTransitionEvent) that gets fired when a given state changes. +//! - The [`in_state`](crate::condition::in_state) and [`state_changed`](crate::condition::state_changed) run conditions - which are used +//! to determine whether a system should run based on the current state. + +/// Provides definitions for the runtime conditions that interact with the state system +pub mod condition; +/// Provides definitions for the basic traits required by the state system +pub mod state; + +/// Most commonly used re-exported types. +pub mod prelude { + #[doc(hidden)] + pub use crate::condition::*; + #[doc(hidden)] + pub use crate::state::{ + apply_state_transition, ComputedStates, NextState, OnEnter, OnExit, OnTransition, State, + StateSet, StateTransition, StateTransitionEvent, States, SubStates, + }; +} diff --git a/crates/bevy_state/src/state/computed_states.rs b/crates/bevy_state/src/state/computed_states.rs new file mode 100644 index 0000000000..fda0f99d8c --- /dev/null +++ b/crates/bevy_state/src/state/computed_states.rs @@ -0,0 +1,97 @@ +use std::fmt::Debug; +use std::hash::Hash; + +use bevy_ecs::schedule::Schedule; + +use super::state_set::StateSet; +use super::states::States; + +/// 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_state::prelude::*; +/// # 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_state::prelude::*; +/// # 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`](crate::state::State). + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// + /// If the result is [`None`], the [`State`](crate::state::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; +} diff --git a/crates/bevy_state/src/state/freely_mutable_state.rs b/crates/bevy_state/src/state/freely_mutable_state.rs new file mode 100644 index 0000000000..1fc809f9e5 --- /dev/null +++ b/crates/bevy_state/src/state/freely_mutable_state.rs @@ -0,0 +1,39 @@ +use bevy_ecs::prelude::Schedule; +use bevy_ecs::schedule::{IntoSystemConfigs, IntoSystemSetConfigs}; +use bevy_ecs::system::IntoSystem; + +use super::states::States; +use super::transitions::*; + +/// This trait allows a state to be mutated directly using the [`NextState`](crate::state::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), + ); + } +} diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs new file mode 100644 index 0000000000..d434b8cca4 --- /dev/null +++ b/crates/bevy_state/src/state/mod.rs @@ -0,0 +1,502 @@ +mod computed_states; +mod freely_mutable_state; +mod resources; +mod state_set; +mod states; +mod sub_states; +mod transitions; + +pub use computed_states::*; +pub use freely_mutable_state::*; +pub use resources::*; +pub use state_set::*; +pub use states::*; +pub use sub_states::*; +pub use transitions::*; + +#[cfg(test)] +mod tests { + use bevy_ecs::prelude::*; + use bevy_ecs::schedule::ScheduleLabel; + use bevy_state_macros::SubStates; + + use super::*; + + use bevy_ecs::event::EventRegistry; + + use bevy_ecs::prelude::ResMut; + + use crate as bevy_state; + + #[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/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs new file mode 100644 index 0000000000..3d98c56367 --- /dev/null +++ b/crates/bevy_state/src/state/resources.rs @@ -0,0 +1,132 @@ +use std::ops::Deref; + +use bevy_ecs::{ + system::Resource, + world::{FromWorld, World}, +}; + +use super::{freely_mutable_state::FreelyMutableState, states::States}; + +#[cfg(feature = "bevy_reflect")] +use bevy_ecs::prelude::ReflectResource; + +/// A finite-state machine whose transitions have associated schedules +/// ([`OnEnter(state)`](crate::state::OnEnter) and [`OnExit(state)`](crate::state::OnExit)). +/// +/// The current state value can be accessed through this resource. To *change* the state, +/// queue a transition in the [`NextState`] resource, and it will be applied by the next +/// [`apply_state_transition::`](crate::state::apply_state_transition) system. +/// +/// The starting state is defined via the [`Default`] implementation for `S`. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn game_logic(game_state: Res>) { +/// match game_state.get() { +/// GameState::InGame => { +/// // Run game logic here... +/// }, +/// _ => {}, +/// } +/// } +/// ``` +#[derive(Resource, Debug)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource) +)] +pub struct State(pub(crate) S); + +impl State { + /// Creates a new state with a specific value. + /// + /// To change the state use [`NextState`] rather than using this to modify the `State`. + pub fn new(state: S) -> Self { + Self(state) + } + + /// Get the current state. + pub fn get(&self) -> &S { + &self.0 + } +} + +impl FromWorld for State { + fn from_world(world: &mut World) -> Self { + Self(S::from_world(world)) + } +} + +impl PartialEq for State { + fn eq(&self, other: &S) -> bool { + self.get() == other + } +} + +impl Deref for State { + type Target = S; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +/// The next state of [`State`]. +/// +/// To queue a transition, just set the contained value to `Some(next_state)`. +/// +/// Note that these transitions can be overridden by other systems: +/// only the actual value of this resource at the time of [`apply_state_transition`](crate::state::apply_state_transition) matters. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn start_game(mut next_game_state: ResMut>) { +/// next_game_state.set(GameState::InGame); +/// } +/// ``` +#[derive(Resource, Debug, Default)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource) +)] +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 pending state transition to `Some(state)`. + pub fn set(&mut self, state: S) { + *self = Self::Pending(state); + } + + /// Remove any pending changes to [`State`] + pub fn reset(&mut self) { + *self = Self::Unchanged; + } +} diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs new file mode 100644 index 0000000000..067711829d --- /dev/null +++ b/crates/bevy_state/src/state/state_set.rs @@ -0,0 +1,287 @@ +use bevy_ecs::{ + event::{EventReader, EventWriter}, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs, Schedule}, + system::{Commands, IntoSystem, Res, ResMut}, +}; +use bevy_utils::all_tuples; + +use self::sealed::StateSetSealed; + +use super::{ + apply_state_transition, computed_states::ComputedStates, internal_apply_state_transition, + run_enter, run_exit, run_transition, should_run_transition, sub_states::SubStates, + ApplyStateTransition, OnEnter, OnExit, OnTransition, State, StateTransitionEvent, + StateTransitionSteps, States, +}; + +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()), + ); + } +} + +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); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs new file mode 100644 index 0000000000..90c28cd93a --- /dev/null +++ b/crates/bevy_state/src/state/states.rs @@ -0,0 +1,41 @@ +use std::fmt::Debug; + +use std::hash::Hash; + +pub use bevy_state_macros::States; + +/// Types that can define world-wide states in a finite-state machine. +/// +/// The [`Default`] trait defines the starting state. +/// Multiple states can be defined for the same world, +/// allowing you to classify the state of the world across orthogonal dimensions. +/// You can access the current state of type `T` with the [`State`](crate::state::State) resource, +/// and the queued state with the [`NextState`](crate::state::NextState) resource. +/// +/// State transitions typically occur in the [`OnEnter`](crate::state::OnEnter) and [`OnExit`](crate::state::OnExit) schedules, +/// which can be run by triggering the [`StateTransition`](crate::state::StateTransition) schedule. +/// +/// Types used as [`ComputedStates`](crate::state::ComputedStates) do not need to and should not derive [`States`]. +/// [`ComputedStates`](crate::state::ComputedStates) should not be manually mutated: functionality provided +/// by the [`States`] derive and the associated [`FreelyMutableState`](crate::state::FreelyMutableState) trait. +/// +/// # Example +/// +/// ``` +/// use bevy_state::prelude::States; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// ``` +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`](crate::state::ComputedStates), as well as prevent cyclical + /// `ComputedState` dependencies. + const DEPENDENCY_DEPTH: usize = 1; +} diff --git a/crates/bevy_state/src/state/sub_states.rs b/crates/bevy_state/src/state/sub_states.rs new file mode 100644 index 0000000000..8046a059b9 --- /dev/null +++ b/crates/bevy_state/src/state/sub_states.rs @@ -0,0 +1,167 @@ +use bevy_ecs::schedule::Schedule; + +use super::{freely_mutable_state::FreelyMutableState, state_set::StateSet, states::States}; +pub use bevy_state_macros::SubStates; + +/// A sub-state is a state that exists only when the source state meet certain conditions, +/// but unlike [`ComputedStates`](crate::state::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::*; +/// # use bevy_state::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::*; +/// # use bevy_state::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::*; +/// # use bevy_state::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_state::prelude::*; +/// # use bevy_state::state::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`](crate::state::State). + /// + /// If the result is [`None`], the [`State`](crate::state::State) resource will be removed from the world, otherwise + /// if the [`State`](crate::state::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); + } +} diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs new file mode 100644 index 0000000000..5e15ae6293 --- /dev/null +++ b/crates/bevy_state/src/state/transitions.rs @@ -0,0 +1,276 @@ +use std::{marker::PhantomData, mem, ops::DerefMut}; + +use bevy_ecs::{ + event::{Event, EventReader, EventWriter}, + schedule::{ + InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel, Schedules, SystemSet, + }, + system::{Commands, In, Local, Res, ResMut}, + world::World, +}; + +use super::{ + freely_mutable_state::FreelyMutableState, + resources::{NextState, State}, + states::States, +}; + +/// 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`] that runs whenever [`State`] +/// exits this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnExit(pub S); + +/// 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`]. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnTransition { + /// The state being exited. + pub from: S, + /// The state being entered. + pub to: S, +} + +/// Runs [state transitions](States). +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct StateTransition; + +/// Event sent when any state transition of `S` happens. +/// +/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +pub struct StateTransitionEvent { + /// the state we were in before + pub before: Option, + /// the state we're in now + pub after: Option, +} + +/// 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)] +pub(crate) 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 { + pub(crate) 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`]. +pub(crate) 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; + } + 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 { + schedules.add_systems(startup, |world: &mut World| { + let _ = world.try_run_schedule(StateTransition); + }); + } +} + +/// If a new state is queued in [`NextState`], this system +/// takes the new state value from [`NextState`] and updates [`State`], as well as +/// sending the relevant [`StateTransitionEvent`]. +/// +/// 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`](crate::state::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(mut next_state_resource) = next_state else { + return; + }; + + 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), + ); + } + } + } + NextState::Unchanged => { + // This is the default value, so we don't need to re-insert the resource + return; + } + } + + *next_state_resource.as_mut() = NextState::::Unchanged; +} + +pub(crate) fn should_run_transition( + mut first: Local, + res: Option>>, + mut event: EventReader>, +) -> (Option>, PhantomData) { + let first_mut = first.deref_mut(); + if !*first_mut { + *first_mut = true; + if let Some(res) = res { + event.clear(); + + return ( + Some(StateTransitionEvent { + before: None, + after: Some(res.get().clone()), + }), + PhantomData, + ); + } + } + (event.read().last().cloned(), PhantomData) +} + +pub(crate) 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)); +} + +pub(crate) 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)); +} + +pub(crate) 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 }); +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 8a154ca433..8ccd806a64 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -25,6 +25,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_render|Provides rendering functionality| |bevy_scene|Provides scene functionality| |bevy_sprite|Provides sprite functionality| +|bevy_state|Enable built in global state machines| |bevy_text|Provides text functionality| |bevy_ui|A custom ECS-driven UI framework| |bevy_winit|winit window and input backend| diff --git a/examples/README.md b/examples/README.md index 4dd859c6b7..e441faa246 100644 --- a/examples/README.md +++ b/examples/README.md @@ -55,6 +55,7 @@ git checkout v0.4.0 - [Reflection](#reflection) - [Scene](#scene) - [Shaders](#shaders) + - [State](#state) - [Stress Tests](#stress-tests) - [Time](#time) - [Tools](#tools) @@ -251,7 +252,6 @@ 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 @@ -268,8 +268,6 @@ Example | Description [Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met [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 @@ -361,6 +359,14 @@ Example | Description [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) [Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). +## State + +Example | Description +--- | --- +[Computed States](../examples/state/computed_states.rs) | Advanced state patterns using Computed States +[State](../examples/state/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state +[Sub States](../examples/state/sub_states.rs) | Using Sub States for hierarchical state handling. + ## Stress Tests These examples are used to test the performance and stability of various parts of the engine in an isolated way. diff --git a/examples/ecs/computed_states.rs b/examples/state/computed_states.rs similarity index 100% rename from examples/ecs/computed_states.rs rename to examples/state/computed_states.rs diff --git a/examples/ecs/state.rs b/examples/state/state.rs similarity index 100% rename from examples/ecs/state.rs rename to examples/state/state.rs diff --git a/examples/ecs/sub_states.rs b/examples/state/sub_states.rs similarity index 100% rename from examples/ecs/sub_states.rs rename to examples/state/sub_states.rs diff --git a/tools/publish.sh b/tools/publish.sh index b020bad286..99dcf1e275 100644 --- a/tools/publish.sh +++ b/tools/publish.sh @@ -11,6 +11,8 @@ crates=( bevy_reflect bevy_ecs/macros bevy_ecs + bevy_state/macros + bevy_state bevy_app bevy_time bevy_log