mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
3f2cc244d7
commit
42ba9dfaea
33 changed files with 2080 additions and 1748 deletions
16
Cargo.toml
16
Cargo.toml
|
@ -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]]
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()`].
|
||||||
///
|
///
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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())),
|
||||||
|
|
|
@ -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
|
@ -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.
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
29
crates/bevy_state/Cargo.toml
Normal file
29
crates/bevy_state/Cargo.toml
Normal 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
|
23
crates/bevy_state/macros/Cargo.toml
Normal file
23
crates/bevy_state/macros/Cargo.toml
Normal 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
|
24
crates/bevy_state/macros/src/lib.rs
Normal file
24
crates/bevy_state/macros/src/lib.rs
Normal 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")
|
||||||
|
}
|
140
crates/bevy_state/macros/src/states.rs
Normal file
140
crates/bevy_state/macros/src/states.rs
Normal 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()
|
||||||
|
}
|
204
crates/bevy_state/src/condition.rs
Normal file
204
crates/bevy_state/src/condition.rs
Normal 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>),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
crates/bevy_state/src/lib.rs
Normal file
44
crates/bevy_state/src/lib.rs
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
97
crates/bevy_state/src/state/computed_states.rs
Normal file
97
crates/bevy_state/src/state/computed_states.rs
Normal 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;
|
||||||
|
}
|
39
crates/bevy_state/src/state/freely_mutable_state.rs
Normal file
39
crates/bevy_state/src/state/freely_mutable_state.rs
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
502
crates/bevy_state/src/state/mod.rs
Normal file
502
crates/bevy_state/src/state/mod.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
132
crates/bevy_state/src/state/resources.rs
Normal file
132
crates/bevy_state/src/state/resources.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
287
crates/bevy_state/src/state/state_set.rs
Normal file
287
crates/bevy_state/src/state/state_set.rs
Normal 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);
|
41
crates/bevy_state/src/state/states.rs
Normal file
41
crates/bevy_state/src/state/states.rs
Normal 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;
|
||||||
|
}
|
167
crates/bevy_state/src/state/sub_states.rs
Normal file
167
crates/bevy_state/src/state/sub_states.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
276
crates/bevy_state/src/state/transitions.rs
Normal file
276
crates/bevy_state/src/state/transitions.rs
Normal 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 });
|
||||||
|
}
|
|
@ -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|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue