mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +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 = [
|
||||
"animation",
|
||||
"bevy_asset",
|
||||
"bevy_state",
|
||||
"bevy_audio",
|
||||
"bevy_color",
|
||||
"bevy_gilrs",
|
||||
|
@ -334,6 +335,9 @@ meshlet_processor = ["bevy_internal/meshlet_processor"]
|
|||
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||
ios_simulator = ["bevy_internal/ios_simulator"]
|
||||
|
||||
# Enable built in global state machines
|
||||
bevy_state = ["bevy_internal/bevy_state"]
|
||||
|
||||
[dependencies]
|
||||
bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false }
|
||||
|
||||
|
@ -1729,35 +1733,35 @@ wasm = false
|
|||
|
||||
[[example]]
|
||||
name = "state"
|
||||
path = "examples/ecs/state.rs"
|
||||
path = "examples/state/state.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.state]
|
||||
name = "State"
|
||||
description = "Illustrates how to use States to control transitioning from a Menu state to an InGame state"
|
||||
category = "ECS (Entity Component System)"
|
||||
category = "State"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "sub_states"
|
||||
path = "examples/ecs/sub_states.rs"
|
||||
path = "examples/state/sub_states.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.sub_states]
|
||||
name = "Sub States"
|
||||
description = "Using Sub States for hierarchical state handling."
|
||||
category = "ECS (Entity Component System)"
|
||||
category = "State"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "computed_states"
|
||||
path = "examples/ecs/computed_states.rs"
|
||||
path = "examples/state/computed_states.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.computed_states]
|
||||
name = "Computed States"
|
||||
description = "Advanced state patterns using Computed States"
|
||||
category = "ECS (Entity Component System)"
|
||||
category = "State"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
|
|
|
@ -11,9 +11,10 @@ keywords = ["bevy"]
|
|||
[features]
|
||||
trace = []
|
||||
bevy_debug_stepping = []
|
||||
default = ["bevy_reflect"]
|
||||
default = ["bevy_reflect", "bevy_state"]
|
||||
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]
|
||||
serialize = ["bevy_ecs/serde"]
|
||||
bevy_state = ["dep:bevy_state"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
|
@ -22,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = fa
|
|||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" }
|
||||
|
||||
# other
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
|
|
@ -7,9 +7,11 @@ use bevy_ecs::{
|
|||
event::{event_update_system, ManualEventReader},
|
||||
intern::Interned,
|
||||
prelude::*,
|
||||
schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel},
|
||||
schedule::{ScheduleBuildSettings, ScheduleLabel},
|
||||
system::SystemId,
|
||||
};
|
||||
#[cfg(feature = "bevy_state")]
|
||||
use bevy_state::{prelude::*, state::FreelyMutableState};
|
||||
#[cfg(feature = "trace")]
|
||||
use bevy_utils::tracing::info_span;
|
||||
use bevy_utils::{tracing::debug, HashMap};
|
||||
|
@ -264,6 +266,7 @@ impl App {
|
|||
self.sub_apps.iter().any(|s| s.is_building_plugins())
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// Initializes a [`State`] with standard starting values.
|
||||
///
|
||||
/// This method is idempotent: it has no effect when called again using the same generic type.
|
||||
|
@ -281,6 +284,7 @@ impl App {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously
|
||||
/// added of the same type.
|
||||
///
|
||||
|
@ -297,23 +301,19 @@ impl App {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// Sets up a type implementing [`ComputedStates`].
|
||||
///
|
||||
/// This method is idempotent: it has no effect when called again using the same generic type.
|
||||
///
|
||||
/// For each source state the derived state depends on, it adds this state's derivation
|
||||
/// to it's [`ComputeDependantStates<Source>`](bevy_ecs::schedule::ComputeDependantStates<S>) schedule.
|
||||
pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self {
|
||||
self.main_mut().add_computed_state::<S>();
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// Sets up a type implementing [`SubStates`].
|
||||
///
|
||||
/// This method is idempotent: it has no effect when called again using the same generic type.
|
||||
///
|
||||
/// For each source state the derived state depends on, it adds this state's existence check
|
||||
/// to it's [`ComputeDependantStates<Source>`](bevy_ecs::schedule::ComputeDependantStates<S>) schedule.
|
||||
pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self {
|
||||
self.main_mut().add_sub_state::<S>();
|
||||
self
|
||||
|
@ -983,10 +983,7 @@ impl Termination for AppExit {
|
|||
mod tests {
|
||||
use std::{marker::PhantomData, mem};
|
||||
|
||||
use bevy_ecs::{
|
||||
schedule::{OnEnter, States},
|
||||
system::Commands,
|
||||
};
|
||||
use bevy_ecs::{schedule::ScheduleLabel, system::Commands};
|
||||
|
||||
use crate::{App, AppExit, Plugin};
|
||||
|
||||
|
@ -1059,11 +1056,9 @@ mod tests {
|
|||
App::new().add_plugins(PluginRun);
|
||||
}
|
||||
|
||||
#[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
MainMenu,
|
||||
}
|
||||
#[derive(ScheduleLabel, Hash, Clone, PartialEq, Eq, Debug)]
|
||||
struct EnterMainMenu;
|
||||
|
||||
fn bar(mut commands: Commands) {
|
||||
commands.spawn_empty();
|
||||
}
|
||||
|
@ -1075,20 +1070,9 @@ mod tests {
|
|||
#[test]
|
||||
fn add_systems_should_create_schedule_if_it_does_not_exist() {
|
||||
let mut app = App::new();
|
||||
app.init_state::<AppState>()
|
||||
.add_systems(OnEnter(AppState::MainMenu), (foo, bar));
|
||||
app.add_systems(EnterMainMenu, (foo, bar));
|
||||
|
||||
app.world_mut().run_schedule(OnEnter(AppState::MainMenu));
|
||||
assert_eq!(app.world().entities().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_systems_should_create_schedule_if_it_does_not_exist2() {
|
||||
let mut app = App::new();
|
||||
app.add_systems(OnEnter(AppState::MainMenu), (foo, bar))
|
||||
.init_state::<AppState>();
|
||||
|
||||
app.world_mut().run_schedule(OnEnter(AppState::MainMenu));
|
||||
app.world_mut().run_schedule(EnterMainMenu);
|
||||
assert_eq!(app.world().entities().len(), 2);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use crate::{App, Plugin};
|
||||
use bevy_ecs::{
|
||||
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition},
|
||||
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel},
|
||||
system::{Local, Resource},
|
||||
world::{Mut, World},
|
||||
};
|
||||
#[cfg(feature = "bevy_state")]
|
||||
use bevy_state::state::StateTransition;
|
||||
|
||||
/// The schedule that contains the app logic that is evaluated each tick of [`App::update()`].
|
||||
///
|
||||
|
|
|
@ -2,12 +2,15 @@ use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup};
|
|||
use bevy_ecs::{
|
||||
event::EventRegistry,
|
||||
prelude::*,
|
||||
schedule::{
|
||||
setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel,
|
||||
ScheduleBuildSettings, ScheduleLabel,
|
||||
},
|
||||
schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel},
|
||||
system::SystemId,
|
||||
};
|
||||
#[cfg(feature = "bevy_state")]
|
||||
use bevy_state::{
|
||||
prelude::*,
|
||||
state::{setup_state_transitions_in_world, FreelyMutableState},
|
||||
};
|
||||
|
||||
#[cfg(feature = "trace")]
|
||||
use bevy_utils::tracing::info_span;
|
||||
use bevy_utils::{HashMap, HashSet};
|
||||
|
@ -295,6 +298,7 @@ impl SubApp {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// See [`App::init_state`].
|
||||
pub fn init_state<S: FreelyMutableState + FromWorld>(&mut self) -> &mut Self {
|
||||
if !self.world.contains_resource::<State<S>>() {
|
||||
|
@ -309,6 +313,7 @@ impl SubApp {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// See [`App::insert_state`].
|
||||
pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self {
|
||||
if !self.world.contains_resource::<State<S>>() {
|
||||
|
@ -324,6 +329,7 @@ impl SubApp {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// See [`App::add_computed_state`].
|
||||
pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self {
|
||||
if !self
|
||||
|
@ -339,6 +345,7 @@ impl SubApp {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_state")]
|
||||
/// See [`App::add_sub_state`].
|
||||
pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self {
|
||||
if !self
|
||||
|
|
|
@ -49,10 +49,8 @@ pub mod prelude {
|
|||
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
|
||||
removal_detection::RemovedComponents,
|
||||
schedule::{
|
||||
apply_deferred, apply_state_transition, common_conditions::*, ComputedStates,
|
||||
Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter,
|
||||
OnExit, OnTransition, Schedule, Schedules, State, StateSet, StateTransition,
|
||||
StateTransitionEvent, States, SubStates, SystemSet,
|
||||
apply_deferred, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet,
|
||||
IntoSystemSetConfigs, Schedule, Schedules, SystemSet,
|
||||
},
|
||||
system::{
|
||||
Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands,
|
||||
|
|
|
@ -194,15 +194,12 @@ mod sealed {
|
|||
|
||||
/// A collection of [run conditions](Condition) that may be useful in any bevy app.
|
||||
pub mod common_conditions {
|
||||
use bevy_utils::warn_once;
|
||||
|
||||
use super::NotSystem;
|
||||
use crate::{
|
||||
change_detection::DetectChanges,
|
||||
event::{Event, EventReader},
|
||||
prelude::{Component, Query, With},
|
||||
removal_detection::RemovedComponents,
|
||||
schedule::{State, States},
|
||||
system::{IntoSystem, Res, Resource, System},
|
||||
};
|
||||
|
||||
|
@ -657,173 +654,6 @@ pub mod common_conditions {
|
|||
}
|
||||
}
|
||||
|
||||
/// A [`Condition`](super::Condition)-satisfying system that returns `true`
|
||||
/// if the state machine exists.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # #[derive(Resource, Default)]
|
||||
/// # struct Counter(u8);
|
||||
/// # let mut app = Schedule::default();
|
||||
/// # let mut world = World::new();
|
||||
/// # world.init_resource::<Counter>();
|
||||
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
|
||||
/// enum GameState {
|
||||
/// #[default]
|
||||
/// Playing,
|
||||
/// Paused,
|
||||
/// }
|
||||
///
|
||||
/// app.add_systems(
|
||||
/// // `state_exists` will only return true if the
|
||||
/// // given state exists
|
||||
/// my_system.run_if(state_exists::<GameState>),
|
||||
/// );
|
||||
///
|
||||
/// fn my_system(mut counter: ResMut<Counter>) {
|
||||
/// counter.0 += 1;
|
||||
/// }
|
||||
///
|
||||
/// // `GameState` does not yet exist `my_system` won't run
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 0);
|
||||
///
|
||||
/// world.init_resource::<State<GameState>>();
|
||||
///
|
||||
/// // `GameState` now exists so `my_system` will run
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 1);
|
||||
/// ```
|
||||
pub fn state_exists<S: States>(current_state: Option<Res<State<S>>>) -> bool {
|
||||
current_state.is_some()
|
||||
}
|
||||
|
||||
/// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true`
|
||||
/// if the state machine is currently in `state`.
|
||||
///
|
||||
/// Will return `false` if the state does not exist or if not in `state`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # #[derive(Resource, Default)]
|
||||
/// # struct Counter(u8);
|
||||
/// # let mut app = Schedule::default();
|
||||
/// # let mut world = World::new();
|
||||
/// # world.init_resource::<Counter>();
|
||||
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
|
||||
/// enum GameState {
|
||||
/// #[default]
|
||||
/// Playing,
|
||||
/// Paused,
|
||||
/// }
|
||||
///
|
||||
/// world.init_resource::<State<GameState>>();
|
||||
///
|
||||
/// app.add_systems((
|
||||
/// // `in_state` will only return true if the
|
||||
/// // given state equals the given value
|
||||
/// play_system.run_if(in_state(GameState::Playing)),
|
||||
/// pause_system.run_if(in_state(GameState::Paused)),
|
||||
/// ));
|
||||
///
|
||||
/// fn play_system(mut counter: ResMut<Counter>) {
|
||||
/// counter.0 += 1;
|
||||
/// }
|
||||
///
|
||||
/// fn pause_system(mut counter: ResMut<Counter>) {
|
||||
/// counter.0 -= 1;
|
||||
/// }
|
||||
///
|
||||
/// // We default to `GameState::Playing` so `play_system` runs
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 1);
|
||||
///
|
||||
/// *world.resource_mut::<State<GameState>>() = State::new(GameState::Paused);
|
||||
///
|
||||
/// // Now that we are in `GameState::Pause`, `pause_system` will run
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 0);
|
||||
/// ```
|
||||
pub fn in_state<S: States>(state: S) -> impl FnMut(Option<Res<State<S>>>) -> bool + Clone {
|
||||
move |current_state: Option<Res<State<S>>>| match current_state {
|
||||
Some(current_state) => *current_state == state,
|
||||
None => {
|
||||
warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", {
|
||||
let debug_state = format!("{state:?}");
|
||||
let result = debug_state
|
||||
.split("::")
|
||||
.next()
|
||||
.unwrap_or("Unknown State Type");
|
||||
result.to_string()
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Condition`](super::Condition)-satisfying system that returns `true`
|
||||
/// if the state machine changed state.
|
||||
///
|
||||
/// To do things on transitions to/from specific states, use their respective OnEnter/OnExit
|
||||
/// schedules. Use this run condition if you want to detect any change, regardless of the value.
|
||||
///
|
||||
/// Returns false if the state does not exist or the state has not changed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # #[derive(Resource, Default)]
|
||||
/// # struct Counter(u8);
|
||||
/// # let mut app = Schedule::default();
|
||||
/// # let mut world = World::new();
|
||||
/// # world.init_resource::<Counter>();
|
||||
/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)]
|
||||
/// enum GameState {
|
||||
/// #[default]
|
||||
/// Playing,
|
||||
/// Paused,
|
||||
/// }
|
||||
///
|
||||
/// world.init_resource::<State<GameState>>();
|
||||
///
|
||||
/// app.add_systems(
|
||||
/// // `state_changed` will only return true if the
|
||||
/// // given states value has just been updated or
|
||||
/// // the state has just been added
|
||||
/// my_system.run_if(state_changed::<GameState>),
|
||||
/// );
|
||||
///
|
||||
/// fn my_system(mut counter: ResMut<Counter>) {
|
||||
/// counter.0 += 1;
|
||||
/// }
|
||||
///
|
||||
/// // `GameState` has just been added so `my_system` will run
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 1);
|
||||
///
|
||||
/// // `GameState` has not been updated so `my_system` will not run
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 1);
|
||||
///
|
||||
/// *world.resource_mut::<State<GameState>>() = State::new(GameState::Paused);
|
||||
///
|
||||
/// // Now that `GameState` has been updated `my_system` will run
|
||||
/// app.run(&mut world);
|
||||
/// assert_eq!(world.resource::<Counter>().0, 2);
|
||||
/// ```
|
||||
pub fn state_changed<S: States>(current_state: Option<Res<State<S>>>) -> bool {
|
||||
let Some(current_state) = current_state else {
|
||||
return false;
|
||||
};
|
||||
current_state.is_changed()
|
||||
}
|
||||
|
||||
/// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true`
|
||||
/// if there are any new events of the given type since it was last called.
|
||||
///
|
||||
|
@ -1032,7 +862,6 @@ mod tests {
|
|||
use crate as bevy_ecs;
|
||||
use crate::component::Component;
|
||||
use crate::schedule::IntoSystemConfigs;
|
||||
use crate::schedule::{State, States};
|
||||
use crate::system::Local;
|
||||
use crate::{change_detection::ResMut, schedule::Schedule, world::World};
|
||||
use bevy_ecs_macros::Event;
|
||||
|
@ -1131,20 +960,15 @@ mod tests {
|
|||
schedule.run(&mut world);
|
||||
assert_eq!(world.resource::<Counter>().0, 0);
|
||||
}
|
||||
|
||||
#[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)]
|
||||
enum TestState {
|
||||
#[default]
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct TestComponent;
|
||||
|
||||
#[derive(Event)]
|
||||
struct TestEvent;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct TestResource(());
|
||||
|
||||
fn test_system() {}
|
||||
|
||||
// Ensure distributive_run_if compiles with the common conditions.
|
||||
|
@ -1153,15 +977,12 @@ mod tests {
|
|||
Schedule::default().add_systems(
|
||||
(test_system, test_system)
|
||||
.distributive_run_if(run_once())
|
||||
.distributive_run_if(resource_exists::<State<TestState>>)
|
||||
.distributive_run_if(resource_added::<State<TestState>>)
|
||||
.distributive_run_if(resource_changed::<State<TestState>>)
|
||||
.distributive_run_if(resource_exists_and_changed::<State<TestState>>)
|
||||
.distributive_run_if(resource_changed_or_removed::<State<TestState>>())
|
||||
.distributive_run_if(resource_removed::<State<TestState>>())
|
||||
.distributive_run_if(state_exists::<TestState>)
|
||||
.distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B)))
|
||||
.distributive_run_if(state_changed::<TestState>)
|
||||
.distributive_run_if(resource_exists::<TestResource>)
|
||||
.distributive_run_if(resource_added::<TestResource>)
|
||||
.distributive_run_if(resource_changed::<TestResource>)
|
||||
.distributive_run_if(resource_exists_and_changed::<TestResource>)
|
||||
.distributive_run_if(resource_changed_or_removed::<TestResource>())
|
||||
.distributive_run_if(resource_removed::<TestResource>())
|
||||
.distributive_run_if(on_event::<TestEvent>())
|
||||
.distributive_run_if(any_with_component::<TestComponent>)
|
||||
.distributive_run_if(not(run_once())),
|
||||
|
|
|
@ -7,7 +7,6 @@ mod graph_utils;
|
|||
#[allow(clippy::module_inception)]
|
||||
mod schedule;
|
||||
mod set;
|
||||
mod state;
|
||||
mod stepping;
|
||||
|
||||
pub use self::condition::*;
|
||||
|
@ -16,7 +15,6 @@ pub use self::executor::*;
|
|||
use self::graph_utils::*;
|
||||
pub use self::schedule::*;
|
||||
pub use self::set::*;
|
||||
pub use self::state::*;
|
||||
|
||||
pub use self::graph_utils::NodeId;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,10 +5,6 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|||
use bevy_utils::HashSet;
|
||||
use std::hash::Hash;
|
||||
|
||||
// unused import, but needed for intra doc link to work
|
||||
#[allow(unused_imports)]
|
||||
use bevy_ecs::schedule::State;
|
||||
|
||||
/// A "press-able" input of type `T`.
|
||||
///
|
||||
/// ## Usage
|
||||
|
@ -23,8 +19,8 @@ use bevy_ecs::schedule::State;
|
|||
/// ## Multiple systems
|
||||
///
|
||||
/// In case multiple systems are checking for [`ButtonInput::just_pressed`] or [`ButtonInput::just_released`]
|
||||
/// but only one should react, for example in the case of triggering
|
||||
/// [`State`] change, you should consider clearing the input state, either by:
|
||||
/// but only one should react, for example when modifying a
|
||||
/// [`Resource`], you should consider clearing the input state, either by:
|
||||
///
|
||||
/// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead.
|
||||
/// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change.
|
||||
|
|
|
@ -179,6 +179,9 @@ bevy_dev_tools = ["dep:bevy_dev_tools"]
|
|||
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||
|
||||
# Enable built in global state machines
|
||||
bevy_state = ["dep:bevy_state", "bevy_app/bevy_state"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" }
|
||||
|
@ -187,6 +190,7 @@ bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
|||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }
|
||||
|
|
|
@ -52,6 +52,8 @@ pub use bevy_render as render;
|
|||
pub use bevy_scene as scene;
|
||||
#[cfg(feature = "bevy_sprite")]
|
||||
pub use bevy_sprite as sprite;
|
||||
#[cfg(feature = "bevy_state")]
|
||||
pub use bevy_state as state;
|
||||
pub use bevy_tasks as tasks;
|
||||
#[cfg(feature = "bevy_text")]
|
||||
pub use bevy_text as text;
|
||||
|
|
|
@ -62,3 +62,7 @@ pub use crate::gizmos::prelude::*;
|
|||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_gilrs")]
|
||||
pub use crate::gilrs::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_state")]
|
||||
pub use crate::state::prelude::*;
|
||||
|
|
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_scene|Provides scene functionality|
|
||||
|bevy_sprite|Provides sprite functionality|
|
||||
|bevy_state|Enable built in global state machines|
|
||||
|bevy_text|Provides text functionality|
|
||||
|bevy_ui|A custom ECS-driven UI framework|
|
||||
|bevy_winit|winit window and input backend|
|
||||
|
|
|
@ -55,6 +55,7 @@ git checkout v0.4.0
|
|||
- [Reflection](#reflection)
|
||||
- [Scene](#scene)
|
||||
- [Shaders](#shaders)
|
||||
- [State](#state)
|
||||
- [Stress Tests](#stress-tests)
|
||||
- [Time](#time)
|
||||
- [Tools](#tools)
|
||||
|
@ -251,7 +252,6 @@ Example | Description
|
|||
--- | ---
|
||||
[Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components
|
||||
[Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events
|
||||
[Computed States](../examples/ecs/computed_states.rs) | Advanced state patterns using Computed States
|
||||
[Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type
|
||||
[Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules
|
||||
[Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components
|
||||
|
@ -268,8 +268,6 @@ Example | Description
|
|||
[Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met
|
||||
[Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system
|
||||
[Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up)
|
||||
[State](../examples/ecs/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state
|
||||
[Sub States](../examples/ecs/sub_states.rs) | Using Sub States for hierarchical state handling.
|
||||
[System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state
|
||||
[System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam`
|
||||
[System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully
|
||||
|
@ -361,6 +359,14 @@ Example | Description
|
|||
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
|
||||
[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures).
|
||||
|
||||
## State
|
||||
|
||||
Example | Description
|
||||
--- | ---
|
||||
[Computed States](../examples/state/computed_states.rs) | Advanced state patterns using Computed States
|
||||
[State](../examples/state/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state
|
||||
[Sub States](../examples/state/sub_states.rs) | Using Sub States for hierarchical state handling.
|
||||
|
||||
## Stress Tests
|
||||
|
||||
These examples are used to test the performance and stability of various parts of the engine in an isolated way.
|
||||
|
|
|
@ -11,6 +11,8 @@ crates=(
|
|||
bevy_reflect
|
||||
bevy_ecs/macros
|
||||
bevy_ecs
|
||||
bevy_state/macros
|
||||
bevy_state
|
||||
bevy_app
|
||||
bevy_time
|
||||
bevy_log
|
||||
|
|
Loading…
Reference in a new issue