Separate state crate (#13216)

# Objective

Extracts the state mechanisms into a new crate called "bevy_state".

This comes with a few goals:

- state wasn't really an inherent machinery of the ecs system, and so
keeping it within bevy_ecs felt forced
- by mixing it in with bevy_ecs, the maintainability of our more robust
state system was significantly compromised

moving state into a new crate makes it easier to encapsulate as it's own
feature, and easier to read and understand since it's no longer a
single, massive file.

## Solution

move the state-related elements from bevy_ecs to a new crate

## Testing

- Did you test these changes? If so, how? all the automated tests
migrated and passed, ran the pre-existing examples without changes to
validate.

---

## Migration Guide

Since bevy_state is now gated behind the `bevy_state` feature, projects
that use state but don't use the `default-features` will need to add
that feature flag.

Since it is no longer part of bevy_ecs, projects that use bevy_ecs
directly will need to manually pull in `bevy_state`, trigger the
StateTransition schedule, and handle any of the elements that bevy_app
currently sets up.

---------

Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
This commit is contained in:
Lee-Orr 2024-05-09 14:06:05 -04:00 committed by GitHub
parent 3f2cc244d7
commit 42ba9dfaea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2080 additions and 1748 deletions

View file

@ -55,6 +55,7 @@ workspace = true
default = [ default = [
"animation", "animation",
"bevy_asset", "bevy_asset",
"bevy_state",
"bevy_audio", "bevy_audio",
"bevy_color", "bevy_color",
"bevy_gilrs", "bevy_gilrs",
@ -334,6 +335,9 @@ meshlet_processor = ["bevy_internal/meshlet_processor"]
# Enable support for the ios_simulator by downgrading some rendering capabilities # Enable support for the ios_simulator by downgrading some rendering capabilities
ios_simulator = ["bevy_internal/ios_simulator"] ios_simulator = ["bevy_internal/ios_simulator"]
# Enable built in global state machines
bevy_state = ["bevy_internal/bevy_state"]
[dependencies] [dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false } bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false }
@ -1729,35 +1733,35 @@ wasm = false
[[example]] [[example]]
name = "state" name = "state"
path = "examples/ecs/state.rs" path = "examples/state/state.rs"
doc-scrape-examples = true doc-scrape-examples = true
[package.metadata.example.state] [package.metadata.example.state]
name = "State" name = "State"
description = "Illustrates how to use States to control transitioning from a Menu state to an InGame 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 wasm = false
[[example]] [[example]]
name = "sub_states" name = "sub_states"
path = "examples/ecs/sub_states.rs" path = "examples/state/sub_states.rs"
doc-scrape-examples = true doc-scrape-examples = true
[package.metadata.example.sub_states] [package.metadata.example.sub_states]
name = "Sub States" name = "Sub States"
description = "Using Sub States for hierarchical state handling." description = "Using Sub States for hierarchical state handling."
category = "ECS (Entity Component System)" category = "State"
wasm = false wasm = false
[[example]] [[example]]
name = "computed_states" name = "computed_states"
path = "examples/ecs/computed_states.rs" path = "examples/state/computed_states.rs"
doc-scrape-examples = true doc-scrape-examples = true
[package.metadata.example.computed_states] [package.metadata.example.computed_states]
name = "Computed States" name = "Computed States"
description = "Advanced state patterns using Computed States" description = "Advanced state patterns using Computed States"
category = "ECS (Entity Component System)" category = "State"
wasm = false wasm = false
[[example]] [[example]]

View file

@ -11,9 +11,10 @@ keywords = ["bevy"]
[features] [features]
trace = [] trace = []
bevy_debug_stepping = [] bevy_debug_stepping = []
default = ["bevy_reflect"] default = ["bevy_reflect", "bevy_state"]
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]
serialize = ["bevy_ecs/serde"] serialize = ["bevy_ecs/serde"]
bevy_state = ["dep:bevy_state"]
[dependencies] [dependencies]
# bevy # 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_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", 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 # other
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }

View file

@ -7,9 +7,11 @@ use bevy_ecs::{
event::{event_update_system, ManualEventReader}, event::{event_update_system, ManualEventReader},
intern::Interned, intern::Interned,
prelude::*, prelude::*,
schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel}, schedule::{ScheduleBuildSettings, ScheduleLabel},
system::SystemId, system::SystemId,
}; };
#[cfg(feature = "bevy_state")]
use bevy_state::{prelude::*, state::FreelyMutableState};
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
use bevy_utils::tracing::info_span; use bevy_utils::tracing::info_span;
use bevy_utils::{tracing::debug, HashMap}; use bevy_utils::{tracing::debug, HashMap};
@ -264,6 +266,7 @@ impl App {
self.sub_apps.iter().any(|s| s.is_building_plugins()) self.sub_apps.iter().any(|s| s.is_building_plugins())
} }
#[cfg(feature = "bevy_state")]
/// Initializes a [`State`] with standard starting values. /// Initializes a [`State`] with standard starting values.
/// ///
/// This method is idempotent: it has no effect when called again using the same generic type. /// This method is idempotent: it has no effect when called again using the same generic type.
@ -281,6 +284,7 @@ impl App {
self self
} }
#[cfg(feature = "bevy_state")]
/// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously
/// added of the same type. /// added of the same type.
/// ///
@ -297,23 +301,19 @@ impl App {
self self
} }
#[cfg(feature = "bevy_state")]
/// Sets up a type implementing [`ComputedStates`]. /// Sets up a type implementing [`ComputedStates`].
/// ///
/// This method is idempotent: it has no effect when called again using the same generic type. /// 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<Source>`](bevy_ecs::schedule::ComputeDependantStates<S>) schedule.
pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self { pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self {
self.main_mut().add_computed_state::<S>(); self.main_mut().add_computed_state::<S>();
self self
} }
#[cfg(feature = "bevy_state")]
/// Sets up a type implementing [`SubStates`]. /// Sets up a type implementing [`SubStates`].
/// ///
/// This method is idempotent: it has no effect when called again using the same generic type. /// 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<Source>`](bevy_ecs::schedule::ComputeDependantStates<S>) schedule.
pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self { pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self {
self.main_mut().add_sub_state::<S>(); self.main_mut().add_sub_state::<S>();
self self
@ -983,10 +983,7 @@ impl Termination for AppExit {
mod tests { mod tests {
use std::{marker::PhantomData, mem}; use std::{marker::PhantomData, mem};
use bevy_ecs::{ use bevy_ecs::{schedule::ScheduleLabel, system::Commands};
schedule::{OnEnter, States},
system::Commands,
};
use crate::{App, AppExit, Plugin}; use crate::{App, AppExit, Plugin};
@ -1059,11 +1056,9 @@ mod tests {
App::new().add_plugins(PluginRun); App::new().add_plugins(PluginRun);
} }
#[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] #[derive(ScheduleLabel, Hash, Clone, PartialEq, Eq, Debug)]
enum AppState { struct EnterMainMenu;
#[default]
MainMenu,
}
fn bar(mut commands: Commands) { fn bar(mut commands: Commands) {
commands.spawn_empty(); commands.spawn_empty();
} }
@ -1075,20 +1070,9 @@ mod tests {
#[test] #[test]
fn add_systems_should_create_schedule_if_it_does_not_exist() { fn add_systems_should_create_schedule_if_it_does_not_exist() {
let mut app = App::new(); let mut app = App::new();
app.init_state::<AppState>() app.add_systems(EnterMainMenu, (foo, bar));
.add_systems(OnEnter(AppState::MainMenu), (foo, bar));
app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); app.world_mut().run_schedule(EnterMainMenu);
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::<AppState>();
app.world_mut().run_schedule(OnEnter(AppState::MainMenu));
assert_eq!(app.world().entities().len(), 2); assert_eq!(app.world().entities().len(), 2);
} }

View file

@ -1,9 +1,11 @@
use crate::{App, Plugin}; use crate::{App, Plugin};
use bevy_ecs::{ use bevy_ecs::{
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition}, schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel},
system::{Local, Resource}, system::{Local, Resource},
world::{Mut, World}, 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()`]. /// The schedule that contains the app logic that is evaluated each tick of [`App::update()`].
/// ///

View file

@ -2,12 +2,15 @@ use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup};
use bevy_ecs::{ use bevy_ecs::{
event::EventRegistry, event::EventRegistry,
prelude::*, prelude::*,
schedule::{ schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel},
setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel,
ScheduleBuildSettings, ScheduleLabel,
},
system::SystemId, system::SystemId,
}; };
#[cfg(feature = "bevy_state")]
use bevy_state::{
prelude::*,
state::{setup_state_transitions_in_world, FreelyMutableState},
};
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
use bevy_utils::tracing::info_span; use bevy_utils::tracing::info_span;
use bevy_utils::{HashMap, HashSet}; use bevy_utils::{HashMap, HashSet};
@ -295,6 +298,7 @@ impl SubApp {
self self
} }
#[cfg(feature = "bevy_state")]
/// See [`App::init_state`]. /// See [`App::init_state`].
pub fn init_state<S: FreelyMutableState + FromWorld>(&mut self) -> &mut Self { pub fn init_state<S: FreelyMutableState + FromWorld>(&mut self) -> &mut Self {
if !self.world.contains_resource::<State<S>>() { if !self.world.contains_resource::<State<S>>() {
@ -309,6 +313,7 @@ impl SubApp {
self self
} }
#[cfg(feature = "bevy_state")]
/// See [`App::insert_state`]. /// See [`App::insert_state`].
pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self { pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self {
if !self.world.contains_resource::<State<S>>() { if !self.world.contains_resource::<State<S>>() {
@ -324,6 +329,7 @@ impl SubApp {
self self
} }
#[cfg(feature = "bevy_state")]
/// See [`App::add_computed_state`]. /// See [`App::add_computed_state`].
pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self { pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self {
if !self if !self
@ -339,6 +345,7 @@ impl SubApp {
self self
} }
#[cfg(feature = "bevy_state")]
/// See [`App::add_sub_state`]. /// See [`App::add_sub_state`].
pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self { pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self {
if !self if !self

View file

@ -49,10 +49,8 @@ pub mod prelude {
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
removal_detection::RemovedComponents, removal_detection::RemovedComponents,
schedule::{ schedule::{
apply_deferred, apply_state_transition, common_conditions::*, ComputedStates, apply_deferred, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet,
Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, IntoSystemSetConfigs, Schedule, Schedules, SystemSet,
OnExit, OnTransition, Schedule, Schedules, State, StateSet, StateTransition,
StateTransitionEvent, States, SubStates, SystemSet,
}, },
system::{ system::{
Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands,

View file

@ -194,15 +194,12 @@ mod sealed {
/// A collection of [run conditions](Condition) that may be useful in any bevy app. /// A collection of [run conditions](Condition) that may be useful in any bevy app.
pub mod common_conditions { pub mod common_conditions {
use bevy_utils::warn_once;
use super::NotSystem; use super::NotSystem;
use crate::{ use crate::{
change_detection::DetectChanges, change_detection::DetectChanges,
event::{Event, EventReader}, event::{Event, EventReader},
prelude::{Component, Query, With}, prelude::{Component, Query, With},
removal_detection::RemovedComponents, removal_detection::RemovedComponents,
schedule::{State, States},
system::{IntoSystem, Res, Resource, System}, 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::<Counter>();
/// #[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::<GameState>),
/// );
///
/// fn my_system(mut counter: ResMut<Counter>) {
/// counter.0 += 1;
/// }
///
/// // `GameState` does not yet exist `my_system` won't run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 0);
///
/// world.init_resource::<State<GameState>>();
///
/// // `GameState` now exists so `my_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
/// ```
pub fn state_exists<S: States>(current_state: Option<Res<State<S>>>) -> 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::<Counter>();
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
/// enum GameState {
/// #[default]
/// Playing,
/// Paused,
/// }
///
/// world.init_resource::<State<GameState>>();
///
/// 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>) {
/// counter.0 += 1;
/// }
///
/// fn pause_system(mut counter: ResMut<Counter>) {
/// counter.0 -= 1;
/// }
///
/// // We default to `GameState::Playing` so `play_system` runs
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
///
/// *world.resource_mut::<State<GameState>>() = State::new(GameState::Paused);
///
/// // Now that we are in `GameState::Pause`, `pause_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 0);
/// ```
pub fn in_state<S: States>(state: S) -> impl FnMut(Option<Res<State<S>>>) -> bool + Clone {
move |current_state: Option<Res<State<S>>>| 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::<Counter>();
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
/// enum GameState {
/// #[default]
/// Playing,
/// Paused,
/// }
///
/// world.init_resource::<State<GameState>>();
///
/// 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::<GameState>),
/// );
///
/// fn my_system(mut counter: ResMut<Counter>) {
/// counter.0 += 1;
/// }
///
/// // `GameState` has just been added so `my_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
///
/// // `GameState` has not been updated so `my_system` will not run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
///
/// *world.resource_mut::<State<GameState>>() = State::new(GameState::Paused);
///
/// // Now that `GameState` has been updated `my_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 2);
/// ```
pub fn state_changed<S: States>(current_state: Option<Res<State<S>>>) -> bool {
let Some(current_state) = current_state else {
return false;
};
current_state.is_changed()
}
/// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// 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. /// 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 as bevy_ecs;
use crate::component::Component; use crate::component::Component;
use crate::schedule::IntoSystemConfigs; use crate::schedule::IntoSystemConfigs;
use crate::schedule::{State, States};
use crate::system::Local; use crate::system::Local;
use crate::{change_detection::ResMut, schedule::Schedule, world::World}; use crate::{change_detection::ResMut, schedule::Schedule, world::World};
use bevy_ecs_macros::Event; use bevy_ecs_macros::Event;
@ -1131,20 +960,15 @@ mod tests {
schedule.run(&mut world); schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 0); assert_eq!(world.resource::<Counter>().0, 0);
} }
#[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)]
enum TestState {
#[default]
A,
B,
}
#[derive(Component)] #[derive(Component)]
struct TestComponent; struct TestComponent;
#[derive(Event)] #[derive(Event)]
struct TestEvent; struct TestEvent;
#[derive(Resource)]
struct TestResource(());
fn test_system() {} fn test_system() {}
// Ensure distributive_run_if compiles with the common conditions. // Ensure distributive_run_if compiles with the common conditions.
@ -1153,15 +977,12 @@ mod tests {
Schedule::default().add_systems( Schedule::default().add_systems(
(test_system, test_system) (test_system, test_system)
.distributive_run_if(run_once()) .distributive_run_if(run_once())
.distributive_run_if(resource_exists::<State<TestState>>) .distributive_run_if(resource_exists::<TestResource>)
.distributive_run_if(resource_added::<State<TestState>>) .distributive_run_if(resource_added::<TestResource>)
.distributive_run_if(resource_changed::<State<TestState>>) .distributive_run_if(resource_changed::<TestResource>)
.distributive_run_if(resource_exists_and_changed::<State<TestState>>) .distributive_run_if(resource_exists_and_changed::<TestResource>)
.distributive_run_if(resource_changed_or_removed::<State<TestState>>()) .distributive_run_if(resource_changed_or_removed::<TestResource>())
.distributive_run_if(resource_removed::<State<TestState>>()) .distributive_run_if(resource_removed::<TestResource>())
.distributive_run_if(state_exists::<TestState>)
.distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B)))
.distributive_run_if(state_changed::<TestState>)
.distributive_run_if(on_event::<TestEvent>()) .distributive_run_if(on_event::<TestEvent>())
.distributive_run_if(any_with_component::<TestComponent>) .distributive_run_if(any_with_component::<TestComponent>)
.distributive_run_if(not(run_once())), .distributive_run_if(not(run_once())),

View file

@ -7,7 +7,6 @@ mod graph_utils;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod schedule; mod schedule;
mod set; mod set;
mod state;
mod stepping; mod stepping;
pub use self::condition::*; pub use self::condition::*;
@ -16,7 +15,6 @@ pub use self::executor::*;
use self::graph_utils::*; use self::graph_utils::*;
pub use self::schedule::*; pub use self::schedule::*;
pub use self::set::*; pub use self::set::*;
pub use self::state::*;
pub use self::graph_utils::NodeId; pub use self::graph_utils::NodeId;

File diff suppressed because it is too large Load diff

View file

@ -5,10 +5,6 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_utils::HashSet; use bevy_utils::HashSet;
use std::hash::Hash; 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`. /// A "press-able" input of type `T`.
/// ///
/// ## Usage /// ## Usage
@ -23,8 +19,8 @@ use bevy_ecs::schedule::State;
/// ## Multiple systems /// ## Multiple systems
/// ///
/// In case multiple systems are checking for [`ButtonInput::just_pressed`] or [`ButtonInput::just_released`] /// 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 /// but only one should react, for example when modifying a
/// [`State`] change, you should consider clearing the input state, either by: /// [`Resource`], you should consider clearing the input state, either by:
/// ///
/// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead. /// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead.
/// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change. /// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change.

View file

@ -179,6 +179,9 @@ bevy_dev_tools = ["dep:bevy_dev_tools"]
# Enable support for the ios_simulator by downgrading some rendering capabilities # Enable support for the ios_simulator by downgrading some rendering capabilities
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] 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] [dependencies]
# bevy # bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" } 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_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", 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_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_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_input = { path = "../bevy_input", 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" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }

View file

@ -52,6 +52,8 @@ pub use bevy_render as render;
pub use bevy_scene as scene; pub use bevy_scene as scene;
#[cfg(feature = "bevy_sprite")] #[cfg(feature = "bevy_sprite")]
pub use bevy_sprite as sprite; pub use bevy_sprite as sprite;
#[cfg(feature = "bevy_state")]
pub use bevy_state as state;
pub use bevy_tasks as tasks; pub use bevy_tasks as tasks;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
pub use bevy_text as text; pub use bevy_text as text;

View file

@ -62,3 +62,7 @@ pub use crate::gizmos::prelude::*;
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "bevy_gilrs")] #[cfg(feature = "bevy_gilrs")]
pub use crate::gilrs::*; pub use crate::gilrs::*;
#[doc(hidden)]
#[cfg(feature = "bevy_state")]
pub use crate::state::prelude::*;

View file

@ -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

View file

@ -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

View file

@ -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")
}

View file

@ -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<Source> {
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::<Result<Vec<_>>>()?;
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<Self> {
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 = <Self as #trait_path>::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()
}

View file

@ -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::<Counter>();
/// #[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::<GameState>),
/// );
///
/// fn my_system(mut counter: ResMut<Counter>) {
/// counter.0 += 1;
/// }
///
/// // `GameState` does not yet exist `my_system` won't run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 0);
///
/// world.init_resource::<State<GameState>>();
///
/// // `GameState` now exists so `my_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
/// ```
pub fn state_exists<S: States>(current_state: Option<Res<State<S>>>) -> 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::<Counter>();
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
/// enum GameState {
/// #[default]
/// Playing,
/// Paused,
/// }
///
/// world.init_resource::<State<GameState>>();
///
/// 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>) {
/// counter.0 += 1;
/// }
///
/// fn pause_system(mut counter: ResMut<Counter>) {
/// counter.0 -= 1;
/// }
///
/// // We default to `GameState::Playing` so `play_system` runs
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
///
/// *world.resource_mut::<State<GameState>>() = State::new(GameState::Paused);
///
/// // Now that we are in `GameState::Pause`, `pause_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 0);
/// ```
pub fn in_state<S: States>(state: S) -> impl FnMut(Option<Res<State<S>>>) -> bool + Clone {
move |current_state: Option<Res<State<S>>>| 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::<Counter>();
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
/// enum GameState {
/// #[default]
/// Playing,
/// Paused,
/// }
///
/// world.init_resource::<State<GameState>>();
///
/// 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::<GameState>),
/// );
///
/// fn my_system(mut counter: ResMut<Counter>) {
/// counter.0 += 1;
/// }
///
/// // `GameState` has just been added so `my_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
///
/// // `GameState` has not been updated so `my_system` will not run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 1);
///
/// *world.resource_mut::<State<GameState>>() = State::new(GameState::Paused);
///
/// // Now that `GameState` has been updated `my_system` will run
/// app.run(&mut world);
/// assert_eq!(world.resource::<Counter>().0, 2);
/// ```
pub fn state_changed<S: States>(current_state: Option<Res<State<S>>>) -> 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::<TestState>)
.distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B)))
.distributive_run_if(state_changed::<TestState>),
);
}
}

View file

@ -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<S>`](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<S>`](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<S>`](crate::state::OnEnter), [`OnExit<S>`](crate::state::OnExit) and [`OnTransition<S>`](crate::state::OnTransition) - which are used
//! to trigger systems specifically during matching transitions.
//! - A [`StateTransitionEvent<S>`](crate::state::StateTransitionEvent) that gets fired when a given state changes.
//! - The [`in_state<S>`](crate::condition::in_state) and [`state_changed<S>`](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,
};
}

View file

@ -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<Self> {
/// 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<InGame>` 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<S>(&mut self) -> &mut Self {self}
/// # fn add_computed_state<S>(&mut self) -> &mut Self {self}
/// # }
/// # struct AppState;
/// # struct InGame;
///
/// App::new()
/// .init_state::<AppState>()
/// .add_computed_state::<InGame>();
/// ```
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<EnemyState>)`
type SourceStates: StateSet;
/// Computes the next value of [`State<Self>`](crate::state::State).
/// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes.
///
/// If the result is [`None`], the [`State<Self>`](crate::state::State) resource will be removed from the world.
fn compute(sources: Self::SourceStates) -> Option<Self>;
/// 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::<Self>(schedule);
}
}
impl<S: ComputedStates> States for S {
const DEPENDENCY_DEPTH: usize = S::SourceStates::SET_DEPENDENCY_DEPTH + 1;
}

View file

@ -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<S>`](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::<Self>.in_set(ApplyStateTransition::<Self>::apply()),
)
.add_systems(
should_run_transition::<Self, OnEnter<Self>>
.pipe(run_enter::<Self>)
.in_set(StateTransitionSteps::EnterSchedules),
)
.add_systems(
should_run_transition::<Self, OnExit<Self>>
.pipe(run_exit::<Self>)
.in_set(StateTransitionSteps::ExitSchedules),
)
.add_systems(
should_run_transition::<Self, OnTransition<Self>>
.pipe(run_transition::<Self>)
.in_set(StateTransitionSteps::TransitionSchedules),
)
.configure_sets(
ApplyStateTransition::<Self>::apply()
.in_set(StateTransitionSteps::ManualTransitions),
);
}
}

View file

@ -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<SimpleState>;
fn compute(sources: Option<SimpleState>) -> Option<Self> {
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::<StateTransitionEvent<SimpleState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<TestComputedState>>(&mut world);
world.init_resource::<State<SimpleState>>();
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::<State<SimpleState>>().0, SimpleState::A);
assert!(!world.contains_resource::<State<TestComputedState>>());
world.insert_resource(NextState::Pending(SimpleState::B(true)));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(true)
);
assert_eq!(
world.resource::<State<TestComputedState>>().0,
TestComputedState::BisTrue
);
world.insert_resource(NextState::Pending(SimpleState::B(false)));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(false)
);
assert_eq!(
world.resource::<State<TestComputedState>>().0,
TestComputedState::BisFalse
);
world.insert_resource(NextState::Pending(SimpleState::A));
world.run_schedule(StateTransition);
assert_eq!(world.resource::<State<SimpleState>>().0, SimpleState::A);
assert!(!world.contains_resource::<State<TestComputedState>>());
}
#[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::<StateTransitionEvent<SimpleState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<SubState>>(&mut world);
world.init_resource::<State<SimpleState>>();
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::<State<SimpleState>>().0, SimpleState::A);
assert!(!world.contains_resource::<State<SubState>>());
world.insert_resource(NextState::Pending(SubState::Two));
world.run_schedule(StateTransition);
assert_eq!(world.resource::<State<SimpleState>>().0, SimpleState::A);
assert!(!world.contains_resource::<State<SubState>>());
world.insert_resource(NextState::Pending(SimpleState::B(true)));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(true)
);
assert_eq!(world.resource::<State<SubState>>().0, SubState::One);
world.insert_resource(NextState::Pending(SubState::Two));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(true)
);
assert_eq!(world.resource::<State<SubState>>().0, SubState::Two);
world.insert_resource(NextState::Pending(SimpleState::B(false)));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(false)
);
assert!(!world.contains_resource::<State<SubState>>());
}
#[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::<StateTransitionEvent<SimpleState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<TestComputedState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<SubStateOfComputed>>(&mut world);
world.init_resource::<State<SimpleState>>();
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::<State<SimpleState>>().0, SimpleState::A);
assert!(!world.contains_resource::<State<SubStateOfComputed>>());
world.insert_resource(NextState::Pending(SubStateOfComputed::Two));
world.run_schedule(StateTransition);
assert_eq!(world.resource::<State<SimpleState>>().0, SimpleState::A);
assert!(!world.contains_resource::<State<SubStateOfComputed>>());
world.insert_resource(NextState::Pending(SimpleState::B(true)));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(true)
);
assert_eq!(
world.resource::<State<SubStateOfComputed>>().0,
SubStateOfComputed::One
);
world.insert_resource(NextState::Pending(SubStateOfComputed::Two));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(true)
);
assert_eq!(
world.resource::<State<SubStateOfComputed>>().0,
SubStateOfComputed::Two
);
world.insert_resource(NextState::Pending(SimpleState::B(false)));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<SimpleState>>().0,
SimpleState::B(false)
);
assert!(!world.contains_resource::<State<SubStateOfComputed>>());
}
#[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<SimpleState>, Option<OtherState>);
fn compute(sources: (Option<SimpleState>, Option<OtherState>)) -> Option<Self> {
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::<StateTransitionEvent<SimpleState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<OtherState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<ComplexComputedState>>(&mut world);
world.init_resource::<State<SimpleState>>();
world.init_resource::<State<OtherState>>();
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::<State<SimpleState>>().0, SimpleState::A);
assert_eq!(
world.resource::<State<OtherState>>().0,
OtherState::default()
);
assert!(!world.contains_resource::<State<ComplexComputedState>>());
world.insert_resource(NextState::Pending(SimpleState::B(true)));
world.run_schedule(StateTransition);
assert!(!world.contains_resource::<State<ComplexComputedState>>());
world.insert_resource(NextState::Pending(OtherState {
a_flexible_value: "felix",
another_value: 13,
}));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<ComplexComputedState>>().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::<State<ComplexComputedState>>().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::<State<ComplexComputedState>>());
}
#[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<SimpleState>, Option<SimpleState2>);
fn compute((s1, s2): (Option<SimpleState>, Option<SimpleState2>)) -> Option<Self> {
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::<StateTransitionEvent<SimpleState>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<SimpleState2>>(&mut world);
EventRegistry::register_event::<StateTransitionEvent<TestNewcomputedState>>(&mut world);
world.init_resource::<State<SimpleState>>();
world.init_resource::<State<SimpleState2>>();
world.init_resource::<Schedules>();
setup_state_transitions_in_world(&mut world, Some(Startup.intern()));
let mut schedules = world
.get_resource_mut::<Schedules>()
.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<ComputedStateTransitionCounter>| {
count.enter += 1;
});
schedule
});
schedules.insert({
let mut schedule = Schedule::new(OnExit(TestNewcomputedState::A1));
schedule.add_systems(|mut count: ResMut<ComputedStateTransitionCounter>| {
count.exit += 1;
});
schedule
});
schedules.insert({
let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B1));
schedule.add_systems(|mut count: ResMut<ComputedStateTransitionCounter>| {
count.enter += 1;
});
schedule
});
schedules.insert({
let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B1));
schedule.add_systems(|mut count: ResMut<ComputedStateTransitionCounter>| {
count.exit += 1;
});
schedule
});
schedules.insert({
let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B2));
schedule.add_systems(|mut count: ResMut<ComputedStateTransitionCounter>| {
count.enter += 1;
});
schedule
});
schedules.insert({
let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B2));
schedule.add_systems(|mut count: ResMut<ComputedStateTransitionCounter>| {
count.exit += 1;
});
schedule
});
world.init_resource::<ComputedStateTransitionCounter>();
setup_state_transitions_in_world(&mut world, None);
assert_eq!(world.resource::<State<SimpleState>>().0, SimpleState::A);
assert_eq!(world.resource::<State<SimpleState2>>().0, SimpleState2::A1);
assert!(!world.contains_resource::<State<TestNewcomputedState>>());
world.insert_resource(NextState::Pending(SimpleState::B(true)));
world.insert_resource(NextState::Pending(SimpleState2::B2));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<TestNewcomputedState>>().0,
TestNewcomputedState::B2
);
assert_eq!(world.resource::<ComputedStateTransitionCounter>().enter, 1);
assert_eq!(world.resource::<ComputedStateTransitionCounter>().exit, 0);
world.insert_resource(NextState::Pending(SimpleState2::A1));
world.insert_resource(NextState::Pending(SimpleState::A));
world.run_schedule(StateTransition);
assert_eq!(
world.resource::<State<TestNewcomputedState>>().0,
TestNewcomputedState::A1
);
assert_eq!(
world.resource::<ComputedStateTransitionCounter>().enter,
2,
"Should Only Enter Twice"
);
assert_eq!(
world.resource::<ComputedStateTransitionCounter>().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::<State<TestNewcomputedState>>().0,
TestNewcomputedState::B2
);
assert_eq!(
world.resource::<ComputedStateTransitionCounter>().enter,
3,
"Should Only Enter Three Times"
);
assert_eq!(
world.resource::<ComputedStateTransitionCounter>().exit,
2,
"Should Only Exit Twice"
);
world.insert_resource(NextState::Pending(SimpleState::A));
world.run_schedule(StateTransition);
assert!(!world.contains_resource::<State<TestNewcomputedState>>());
assert_eq!(
world.resource::<ComputedStateTransitionCounter>().enter,
3,
"Should Only Enter Three Times"
);
assert_eq!(
world.resource::<ComputedStateTransitionCounter>().exit,
3,
"Should Only Exit Twice"
);
}
}

View file

@ -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<S>`] resource, and it will be applied by the next
/// [`apply_state_transition::<S>`](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<State<GameState>>) {
/// 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: States>(pub(crate) S);
impl<S: States> State<S> {
/// Creates a new state with a specific value.
///
/// To change the state use [`NextState<S>`] rather than using this to modify the `State<S>`.
pub fn new(state: S) -> Self {
Self(state)
}
/// Get the current state.
pub fn get(&self) -> &S {
&self.0
}
}
impl<S: States + FromWorld> FromWorld for State<S> {
fn from_world(world: &mut World) -> Self {
Self(S::from_world(world))
}
}
impl<S: States> PartialEq<S> for State<S> {
fn eq(&self, other: &S) -> bool {
self.get() == other
}
}
impl<S: States> Deref for State<S> {
type Target = S;
fn deref(&self) -> &Self::Target {
self.get()
}
}
/// The next state of [`State<S>`].
///
/// 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<NextState<GameState>>) {
/// next_game_state.set(GameState::InGame);
/// }
/// ```
#[derive(Resource, Debug, Default)]
#[cfg_attr(
feature = "bevy_reflect",
derive(bevy_reflect::Reflect),
reflect(Resource)
)]
pub enum NextState<S: FreelyMutableState> {
/// No state transition is pending
#[default]
Unchanged,
/// There is a pending transition for state `S`
Pending(S),
}
impl<S: FreelyMutableState> NextState<S> {
/// 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<S>`]
pub fn reset(&mut self) {
*self = Self::Unchanged;
}
}

View file

@ -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<T: ComputedStates<SourceStates = Self>>(
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<T: SubStates<SourceStates = Self>>(
schedule: &mut Schedule,
);
}
/// The `InnerStateSet` trait is used to isolate [`ComputedStates`] & [`SubStates`] from
/// needing to wrap all state dependencies in an [`Option<S>`].
///
/// 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<S>`] when appropriate.
///
/// The isolation works because it is implemented for both S & [`Option<S>`], 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<Self::RawState>>) -> Option<Self>;
}
impl<S: States> InnerStateSet for S {
type RawState = Self;
const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH;
fn convert_to_usable_state(wrapped: Option<&State<Self::RawState>>) -> Option<Self> {
wrapped.map(|v| v.0.clone())
}
}
impl<S: States> InnerStateSet for Option<S> {
type RawState = S;
const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH;
fn convert_to_usable_state(wrapped: Option<&State<Self::RawState>>) -> Option<Self> {
Some(wrapped.map(|v| v.0.clone()))
}
}
impl<S: InnerStateSet> StateSetSealed for S {}
impl<S: InnerStateSet> StateSet for S {
const SET_DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH;
fn register_computed_state_systems_in_schedule<T: ComputedStates<SourceStates = Self>>(
schedule: &mut Schedule,
) {
let system = |mut parent_changed: EventReader<StateTransitionEvent<S::RawState>>,
event: EventWriter<StateTransitionEvent<T>>,
commands: Commands,
current_state: Option<ResMut<State<T>>>,
state_set: Option<Res<State<S::RawState>>>| {
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::<T>::apply()))
.add_systems(
should_run_transition::<T, OnEnter<T>>
.pipe(run_enter::<T>)
.in_set(StateTransitionSteps::EnterSchedules),
)
.add_systems(
should_run_transition::<T, OnExit<T>>
.pipe(run_exit::<T>)
.in_set(StateTransitionSteps::ExitSchedules),
)
.add_systems(
should_run_transition::<T, OnTransition<T>>
.pipe(run_transition::<T>)
.in_set(StateTransitionSteps::TransitionSchedules),
)
.configure_sets(
ApplyStateTransition::<T>::apply()
.in_set(StateTransitionSteps::DependentTransitions)
.after(ApplyStateTransition::<S::RawState>::apply()),
);
}
fn register_sub_state_systems_in_schedule<T: SubStates<SourceStates = Self>>(
schedule: &mut Schedule,
) {
let system = |mut parent_changed: EventReader<StateTransitionEvent<S::RawState>>,
event: EventWriter<StateTransitionEvent<T>>,
commands: Commands,
current_state: Option<ResMut<State<T>>>,
state_set: Option<Res<State<S::RawState>>>| {
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::<T>::apply()))
.add_systems(
apply_state_transition::<T>.in_set(StateTransitionSteps::ManualTransitions),
)
.add_systems(
should_run_transition::<T, OnEnter<T>>
.pipe(run_enter::<T>)
.in_set(StateTransitionSteps::EnterSchedules),
)
.add_systems(
should_run_transition::<T, OnExit<T>>
.pipe(run_exit::<T>)
.in_set(StateTransitionSteps::ExitSchedules),
)
.add_systems(
should_run_transition::<T, OnTransition<T>>
.pipe(run_transition::<T>)
.in_set(StateTransitionSteps::TransitionSchedules),
)
.configure_sets(
ApplyStateTransition::<T>::apply()
.in_set(StateTransitionSteps::DependentTransitions)
.after(ApplyStateTransition::<S::RawState>::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<T: ComputedStates<SourceStates = Self>>(
schedule: &mut Schedule,
) {
let system = |($(mut $evt),*,): ($(EventReader<StateTransitionEvent<$param::RawState>>),*,), event: EventWriter<StateTransitionEvent<T>>, commands: Commands, current_state: Option<ResMut<State<T>>>, ($($val),*,): ($(Option<Res<State<$param::RawState>>>),*,)| {
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::<T>::apply()))
.add_systems(should_run_transition::<T, OnEnter<T>>.pipe(run_enter::<T>).in_set(StateTransitionSteps::EnterSchedules))
.add_systems(should_run_transition::<T, OnExit<T>>.pipe(run_exit::<T>).in_set(StateTransitionSteps::ExitSchedules))
.add_systems(should_run_transition::<T, OnTransition<T>>.pipe(run_transition::<T>).in_set(StateTransitionSteps::TransitionSchedules))
.configure_sets(
ApplyStateTransition::<T>::apply()
.in_set(StateTransitionSteps::DependentTransitions)
$(.after(ApplyStateTransition::<$param::RawState>::apply()))*
);
}
fn register_sub_state_systems_in_schedule<T: SubStates<SourceStates = Self>>(
schedule: &mut Schedule,
) {
let system = |($(mut $evt),*,): ($(EventReader<StateTransitionEvent<$param::RawState>>),*,), event: EventWriter<StateTransitionEvent<T>>, commands: Commands, current_state: Option<ResMut<State<T>>>, ($($val),*,): ($(Option<Res<State<$param::RawState>>>),*,)| {
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::<T>::apply()))
.add_systems(apply_state_transition::<T>.in_set(StateTransitionSteps::ManualTransitions))
.add_systems(should_run_transition::<T, OnEnter<T>>.pipe(run_enter::<T>).in_set(StateTransitionSteps::EnterSchedules))
.add_systems(should_run_transition::<T, OnExit<T>>.pipe(run_exit::<T>).in_set(StateTransitionSteps::ExitSchedules))
.add_systems(should_run_transition::<T, OnTransition<T>>.pipe(run_transition::<T>).in_set(StateTransitionSteps::TransitionSchedules))
.configure_sets(
ApplyStateTransition::<T>::apply()
.in_set(StateTransitionSteps::DependentTransitions)
$(.after(ApplyStateTransition::<$param::RawState>::apply()))*
);
}
}
};
}
all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader);

View file

@ -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<T>`](crate::state::State) resource,
/// and the queued state with the [`NextState<T>`](crate::state::NextState) resource.
///
/// State transitions typically occur in the [`OnEnter<T::Variant>`](crate::state::OnEnter) and [`OnExit<T::Variant>`](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;
}

View file

@ -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<S>(&mut self) -> &mut Self {self}
/// # fn add_sub_state<S>(&mut self) -> &mut Self {self}
/// # }
/// # struct AppState;
/// # struct GamePhase;
///
/// App::new()
/// .init_state::<AppState>()
/// .add_sub_state::<GamePhase>();
/// ```
///
/// 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<AppState>;
///
/// /// We then define the compute function, which takes in the AppState
/// fn compute(sources: Option<AppState>) -> Option<Self> {
/// 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<InGame>` 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<AppState>;
///
/// /// We then define the compute function, which takes in the [`Self::SourceStates`]
/// fn should_exist(sources: Option<AppState>) -> Option<Self> {
/// 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<GamePhase>` resource to exist,
/// /// so we return None.
/// _ => None
/// }
/// }
/// }
///
/// impl States for GamePhase {
/// const DEPENDENCY_DEPTH : usize = <GamePhase as SubStates>::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<Self>`](crate::state::State).
///
/// If the result is [`None`], the [`State<Self>`](crate::state::State) resource will be removed from the world, otherwise
/// if the [`State<Self>`](crate::state::State) resource doesn't exist - it will be created with the [`Some`] value.
fn should_exist(sources: Self::SourceStates) -> Option<Self>;
/// 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::<Self>(schedule);
}
}

View file

@ -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<S>`]
/// enters this state.
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct OnEnter<S: States>(pub S);
/// The label of a [`Schedule`] that runs whenever [`State<S>`]
/// exits this state.
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct OnExit<S: States>(pub S);
/// The label of a [`Schedule`] that **only** runs whenever [`State<S>`]
/// 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<S: States> {
/// 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<S: States> {
/// the state we were in before
pub before: Option<S>,
/// the state we're in now
pub after: Option<S>,
}
/// Applies manual state transitions using [`NextState<S>`].
///
/// 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<S: States>(PhantomData<S>);
impl<S: States> ApplyStateTransition<S> {
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<S>` resource from the [`World`].
pub(crate) fn internal_apply_state_transition<S: States>(
mut event: EventWriter<StateTransitionEvent<S>>,
mut commands: Commands,
current_state: Option<ResMut<State<S>>>,
new_state: Option<S>,
) {
match new_state {
Some(entered) => {
match current_state {
// If the [`State<S>`] 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<S>`] 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<S>`] 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::<State<S>>();
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<InternedScheduleLabel>,
) {
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<S>`], this system
/// takes the new state value from [`NextState<S>`] and updates [`State<S>`], as well as
/// sending the relevant [`StateTransitionEvent`].
///
/// If the [`State<S>`] 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<S: FreelyMutableState>(
event: EventWriter<StateTransitionEvent<S>>,
commands: Commands,
current_state: Option<ResMut<State<S>>>,
next_state: Option<ResMut<NextState<S>>>,
) {
// 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::<S>::Unchanged;
}
pub(crate) fn should_run_transition<S: States, T: ScheduleLabel>(
mut first: Local<bool>,
res: Option<Res<State<S>>>,
mut event: EventReader<StateTransitionEvent<S>>,
) -> (Option<StateTransitionEvent<S>>, PhantomData<T>) {
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<S: States>(
In((transition, _)): In<(Option<StateTransitionEvent<S>>, PhantomData<OnEnter<S>>)>,
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<S: States>(
In((transition, _)): In<(Option<StateTransitionEvent<S>>, PhantomData<OnExit<S>>)>,
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<S: States>(
In((transition, _)): In<(
Option<StateTransitionEvent<S>>,
PhantomData<OnTransition<S>>,
)>,
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 });
}

View file

@ -25,6 +25,7 @@ The default feature set enables most of the expected features of a game engine,
|bevy_render|Provides rendering functionality| |bevy_render|Provides rendering functionality|
|bevy_scene|Provides scene functionality| |bevy_scene|Provides scene functionality|
|bevy_sprite|Provides sprite functionality| |bevy_sprite|Provides sprite functionality|
|bevy_state|Enable built in global state machines|
|bevy_text|Provides text functionality| |bevy_text|Provides text functionality|
|bevy_ui|A custom ECS-driven UI framework| |bevy_ui|A custom ECS-driven UI framework|
|bevy_winit|winit window and input backend| |bevy_winit|winit window and input backend|

View file

@ -55,6 +55,7 @@ git checkout v0.4.0
- [Reflection](#reflection) - [Reflection](#reflection)
- [Scene](#scene) - [Scene](#scene)
- [Shaders](#shaders) - [Shaders](#shaders)
- [State](#state)
- [Stress Tests](#stress-tests) - [Stress Tests](#stress-tests)
- [Time](#time) - [Time](#time)
- [Tools](#tools) - [Tools](#tools)
@ -251,7 +252,6 @@ Example | Description
--- | --- --- | ---
[Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components [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 [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 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 [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 [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 [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 [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) [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 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 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 [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) [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). [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 ## Stress Tests
These examples are used to test the performance and stability of various parts of the engine in an isolated way. These examples are used to test the performance and stability of various parts of the engine in an isolated way.

View file

@ -11,6 +11,8 @@ crates=(
bevy_reflect bevy_reflect
bevy_ecs/macros bevy_ecs/macros
bevy_ecs bevy_ecs
bevy_state/macros
bevy_state
bevy_app bevy_app
bevy_time bevy_time
bevy_log bevy_log