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

View file

@ -11,9 +11,10 @@ keywords = ["bevy"]
[features]
trace = []
bevy_debug_stepping = []
default = ["bevy_reflect"]
default = ["bevy_reflect", "bevy_state"]
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]
serialize = ["bevy_ecs/serde"]
bevy_state = ["dep:bevy_state"]
[dependencies]
# bevy
@ -22,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = fa
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" }
# other
serde = { version = "1.0", features = ["derive"], optional = true }

View file

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

View file

@ -1,9 +1,11 @@
use crate::{App, Plugin};
use bevy_ecs::{
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition},
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel},
system::{Local, Resource},
world::{Mut, World},
};
#[cfg(feature = "bevy_state")]
use bevy_state::state::StateTransition;
/// The schedule that contains the app logic that is evaluated each tick of [`App::update()`].
///

View file

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

View file

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

View file

@ -194,15 +194,12 @@ mod sealed {
/// A collection of [run conditions](Condition) that may be useful in any bevy app.
pub mod common_conditions {
use bevy_utils::warn_once;
use super::NotSystem;
use crate::{
change_detection::DetectChanges,
event::{Event, EventReader},
prelude::{Component, Query, With},
removal_detection::RemovedComponents,
schedule::{State, States},
system::{IntoSystem, Res, Resource, System},
};
@ -657,173 +654,6 @@ pub mod common_conditions {
}
}
/// A [`Condition`](super::Condition)-satisfying system that returns `true`
/// if the state machine exists.
///
/// # Example
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # #[derive(Resource, Default)]
/// # struct Counter(u8);
/// # let mut app = Schedule::default();
/// # let mut world = World::new();
/// # world.init_resource::<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`
/// if there are any new events of the given type since it was last called.
///
@ -1032,7 +862,6 @@ mod tests {
use crate as bevy_ecs;
use crate::component::Component;
use crate::schedule::IntoSystemConfigs;
use crate::schedule::{State, States};
use crate::system::Local;
use crate::{change_detection::ResMut, schedule::Schedule, world::World};
use bevy_ecs_macros::Event;
@ -1131,20 +960,15 @@ mod tests {
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 0);
}
#[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)]
enum TestState {
#[default]
A,
B,
}
#[derive(Component)]
struct TestComponent;
#[derive(Event)]
struct TestEvent;
#[derive(Resource)]
struct TestResource(());
fn test_system() {}
// Ensure distributive_run_if compiles with the common conditions.
@ -1153,15 +977,12 @@ mod tests {
Schedule::default().add_systems(
(test_system, test_system)
.distributive_run_if(run_once())
.distributive_run_if(resource_exists::<State<TestState>>)
.distributive_run_if(resource_added::<State<TestState>>)
.distributive_run_if(resource_changed::<State<TestState>>)
.distributive_run_if(resource_exists_and_changed::<State<TestState>>)
.distributive_run_if(resource_changed_or_removed::<State<TestState>>())
.distributive_run_if(resource_removed::<State<TestState>>())
.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(resource_exists::<TestResource>)
.distributive_run_if(resource_added::<TestResource>)
.distributive_run_if(resource_changed::<TestResource>)
.distributive_run_if(resource_exists_and_changed::<TestResource>)
.distributive_run_if(resource_changed_or_removed::<TestResource>())
.distributive_run_if(resource_removed::<TestResource>())
.distributive_run_if(on_event::<TestEvent>())
.distributive_run_if(any_with_component::<TestComponent>)
.distributive_run_if(not(run_once())),

View file

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

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 std::hash::Hash;
// unused import, but needed for intra doc link to work
#[allow(unused_imports)]
use bevy_ecs::schedule::State;
/// A "press-able" input of type `T`.
///
/// ## Usage
@ -23,8 +19,8 @@ use bevy_ecs::schedule::State;
/// ## Multiple systems
///
/// In case multiple systems are checking for [`ButtonInput::just_pressed`] or [`ButtonInput::just_released`]
/// but only one should react, for example in the case of triggering
/// [`State`] change, you should consider clearing the input state, either by:
/// but only one should react, for example when modifying a
/// [`Resource`], you should consider clearing the input state, either by:
///
/// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead.
/// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change.

View file

@ -179,6 +179,9 @@ bevy_dev_tools = ["dep:bevy_dev_tools"]
# Enable support for the ios_simulator by downgrading some rendering capabilities
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
# Enable built in global state machines
bevy_state = ["dep:bevy_state", "bevy_app/bevy_state"]
[dependencies]
# bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" }
@ -187,6 +190,7 @@ bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }

View file

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

View file

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

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_scene|Provides scene functionality|
|bevy_sprite|Provides sprite functionality|
|bevy_state|Enable built in global state machines|
|bevy_text|Provides text functionality|
|bevy_ui|A custom ECS-driven UI framework|
|bevy_winit|winit window and input backend|

View file

@ -55,6 +55,7 @@ git checkout v0.4.0
- [Reflection](#reflection)
- [Scene](#scene)
- [Shaders](#shaders)
- [State](#state)
- [Stress Tests](#stress-tests)
- [Time](#time)
- [Tools](#tools)
@ -251,7 +252,6 @@ Example | Description
--- | ---
[Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components
[Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events
[Computed States](../examples/ecs/computed_states.rs) | Advanced state patterns using Computed States
[Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type
[Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules
[Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components
@ -268,8 +268,6 @@ Example | Description
[Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met
[Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system
[Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up)
[State](../examples/ecs/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state
[Sub States](../examples/ecs/sub_states.rs) | Using Sub States for hierarchical state handling.
[System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state
[System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam`
[System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully
@ -361,6 +359,14 @@ Example | Description
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures).
## State
Example | Description
--- | ---
[Computed States](../examples/state/computed_states.rs) | Advanced state patterns using Computed States
[State](../examples/state/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state
[Sub States](../examples/state/sub_states.rs) | Using Sub States for hierarchical state handling.
## Stress Tests
These examples are used to test the performance and stability of various parts of the engine in an isolated way.

View file

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