mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Computed State & Sub States (#11426)
## Summary/Description This PR extends states to allow support for a wider variety of state types and patterns, by providing 3 distinct types of state: - Standard [`States`] can only be changed by manually setting the [`NextState<S>`] 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 - these are the states that existed so far in Bevy. - [`SubStates`] are children of other states - they can be changed manually using [`NextState<S>`], but are removed from the [`World`] if the source states aren't in the right state. See the [sub_states example](https://github.com/lee-orr/bevy/blob/derived_state/examples/ecs/sub_states.rs) for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. - [`ComputedStates`] are fully derived from other states - they provide a [`compute`](ComputedStates::compute) method that takes in the source states and returns their derived value. They are particularly useful for situations where a simplified view of the source states is necessary - such as having an `InAMenu` computed state derived from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/lee-orr/bevy/blob/derived_state/examples/ecs/computed_states.rscomputed_states.rs) to see a sampling of uses for these states. # Objective This PR is another attempt at allowing Bevy to better handle complex state objects in a manner that doesn't rely on strict equality. While my previous attempts (https://github.com/bevyengine/bevy/pull/10088 and https://github.com/bevyengine/bevy/pull/9957) relied on complex matching capacities at the point of adding a system to application, this one instead relies on deterministically deriving simple states from more complex ones. As a result, it does not require any special macros, nor does it change any other interactions with the state system once you define and add your derived state. It also maintains a degree of distinction between `State` and just normal application state - your derivations have to end up being discreet pre-determined values, meaning there is less of a risk/temptation to place a significant amount of logic and data within a given state. ### Addition - Sub States closes #9942 After some conversation with Maintainers & SMEs, a significant concern was that people might attempt to use this feature as if it were sub-states, and find themselves unable to use it appropriately. Since `ComputedState` is mainly a state matching feature, while `SubStates` are more of a state mutation related feature - but one that is easy to add with the help of the machinery introduced by `ComputedState`, it was added here as well. The relevant discussion is here: https://discord.com/channels/691052431525675048/1200556329803186316 ## Solution closes #11358 The solution is to create a new type of state - one implementing `ComputedStates` - which is deterministically tied to one or more other states. Implementors write a function to transform the source states into the computed state, and it gets triggered whenever one of the source states changes. In addition, we added the `FreelyMutableState` trait , which is implemented as part of the derive macro for `States`. This allows us to limit use of `NextState<S>` to states that are actually mutable, preventing mis-use of `ComputedStates`. --- ## Changelog - Added `ComputedStates` trait - Added `FreelyMutableState` trait - Converted `NextState` resource to an Enum, with `Unchanged` and `Pending` - Added `App::add_computed_state::<S: ComputedStates>()`, to allow for easily adding derived states to an App. - Moved the `StateTransition` schedule label from `bevy_app` to `bevy_ecs` - but maintained the export in `bevy_app` for continuity. - Modified the process for updating states. Instead of just having an `apply_state_transition` system that can be added anywhere, we now have a multi-stage process that has to run within the `StateTransition` label. First, all the state changes are calculated - manual transitions rely on `apply_state_transition`, while computed transitions run their computation process before both call `internal_apply_state_transition` to apply the transition, send out the transition event, trigger dependent states, and record which exit/transition/enter schedules need to occur. Once all the states have been updated, the transition schedules are called - first the exit schedules, then transition schedules and finally enter schedules. - Added `SubStates` trait - Adjusted `apply_state_transition` to be a no-op if the `State<S>` resource doesn't exist ## Migration Guide If the user accessed the NextState resource's value directly or created them from scratch they will need to adjust to use the new enum variants: - if they created a `NextState(Some(S))` - they should now use `NextState::Pending(S)` - if they created a `NextState(None)` -they should now use `NextState::Unchanged` - if they matched on the `NextState` value, they would need to make the adjustments above If the user manually utilized `apply_state_transition`, they should instead use systems that trigger the `StateTransition` schedule. --- ## Future Work There is still some future potential work in the area, but I wanted to keep these potential features and changes separate to keep the scope here contained, and keep the core of it easy to understand and use. However, I do want to note some of these things, both as inspiration to others and an illustration of what this PR could unlock. - `NextState::Remove` - Now that the `State` related mechanisms all utilize options (#11417), it's fairly easy to add support for explicit state removal. And while `ComputedStates` can add and remove themselves, right now `FreelyMutableState`s can't be removed from within the state system. While it existed originally in this PR, it is a different question with a separate scope and usability concerns - so having it as it's own future PR seems like the best approach. This feature currently lives in a separate branch in my fork, and the differences between it and this PR can be seen here: https://github.com/lee-orr/bevy/pull/5 - `NextState::ReEnter` - this would allow you to trigger exit & entry systems for the current state type. We can potentially also add a `NextState::ReEnterRecirsive` to also re-trigger any states that depend on the current one. - More mechanisms for `State` updates - This PR would finally make states that aren't a set of exclusive Enums useful, and with that comes the question of setting state more effectively. Right now, to update a state you either need to fully create the new state, or include the `Res<Option<State<S>>>` resource in your system, clone the state, mutate it, and then use `NextState.set(my_mutated_state)` to make it the pending next state. There are a few other potential methods that could be implemented in future PRs: - Inverse Compute States - these would essentially be compute states that have an additional (manually defined) function that can be used to nudge the source states so that they result in the computed states having a given value. For example, you could use set the `IsPaused` state, and it would attempt to pause or unpause the game by modifying the `AppState` as needed. - Closure-based state modification - this would involve adding a `NextState.modify(f: impl Fn(Option<S> -> Option<S>)` method, and then you can pass in closures or function pointers to adjust the state as needed. - Message-based state modification - this would involve either creating states that can respond to specific messages, similar to Elm or Redux. These could either use the `NextState` mechanism or the Event mechanism. - ~`SubStates` - which are essentially a hybrid of computed and manual states. In the simplest (and most likely) version, they would work by having a computed element that determines whether the state should exist, and if it should has the capacity to add a new version in, but then any changes to it's content would be freely mutated.~ this feature is now part of this PR. See above. - Lastly, since states are getting more complex there might be value in moving them out of `bevy_ecs` and into their own crate, or at least out of the `schedule` module into a `states` module. #11087 As mentioned, all these future work elements are TBD and are explicitly not part of this PR - I just wanted to provide them as potential explorations for the future. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Marcel Champagne <voiceofmarcel@gmail.com> Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
This commit is contained in:
parent
e357b63448
commit
b8832dc862
13 changed files with 2543 additions and 134 deletions
22
Cargo.toml
22
Cargo.toml
|
@ -1692,6 +1692,28 @@ description = "Illustrates how to use States to control transitioning from a Men
|
|||
category = "ECS (Entity Component System)"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "sub_states"
|
||||
path = "examples/ecs/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)"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "computed_states"
|
||||
path = "examples/ecs/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)"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "system_piping"
|
||||
path = "examples/ecs/system_piping.rs"
|
||||
|
|
|
@ -7,7 +7,7 @@ use bevy_ecs::{
|
|||
event::{event_update_system, ManualEventReader},
|
||||
intern::Interned,
|
||||
prelude::*,
|
||||
schedule::{ScheduleBuildSettings, ScheduleLabel},
|
||||
schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel},
|
||||
system::SystemId,
|
||||
};
|
||||
#[cfg(feature = "trace")]
|
||||
|
@ -266,26 +266,17 @@ impl App {
|
|||
|
||||
/// Initializes a [`State`] with standard starting values.
|
||||
///
|
||||
/// If the [`State`] already exists, nothing happens.
|
||||
/// This method is idempotent: it has no effect when called again using the same generic type.
|
||||
///
|
||||
/// Adds [`State<S>`] and [`NextState<S>`] resources, [`OnEnter`] and [`OnExit`] schedules for
|
||||
/// each state variant (if they don't already exist), an instance of [`apply_state_transition::<S>`]
|
||||
/// in [`StateTransition`] so that transitions happen before [`Update`] and an instance of
|
||||
/// [`run_enter_schedule::<S>`] in [`StateTransition`] with a [`run_once`] condition to run the
|
||||
/// on enter schedule of the initial state.
|
||||
/// Adds [`State<S>`] and [`NextState<S>`] resources, and enables use of the [`OnEnter`], [`OnTransition`] and [`OnExit`] schedules.
|
||||
/// These schedules are triggered before [`Update`](crate::Update) and at startup.
|
||||
///
|
||||
/// If you would like to control how other systems run based on the current state, you can
|
||||
/// emulate this behavior using the [`in_state`] [`Condition`].
|
||||
///
|
||||
/// Note that you can also apply state transitions at other points in the schedule by adding
|
||||
/// the [`apply_state_transition::<S>`] system manually.
|
||||
///
|
||||
/// [`StateTransition`]: crate::StateTransition
|
||||
/// [`Update`]: crate::Update
|
||||
/// [`run_once`]: bevy_ecs::schedule::common_conditions::run_once
|
||||
/// [`run_enter_schedule::<S>`]: bevy_ecs::schedule::run_enter_schedule
|
||||
/// [`apply_state_transition::<S>`]: bevy_ecs::schedule::apply_state_transition
|
||||
pub fn init_state<S: States + FromWorld>(&mut self) -> &mut Self {
|
||||
/// Note that you can also apply state transitions at other points in the schedule
|
||||
/// by triggering the [`StateTransition`](`bevy_ecs::schedule::StateTransition`) schedule manually.
|
||||
pub fn init_state<S: FreelyMutableState + FromWorld>(&mut self) -> &mut Self {
|
||||
self.main_mut().init_state::<S>();
|
||||
self
|
||||
}
|
||||
|
@ -293,29 +284,42 @@ impl App {
|
|||
/// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously
|
||||
/// added of the same type.
|
||||
///
|
||||
/// Adds [`State<S>`] and [`NextState<S>`] resources, [`OnEnter`] and [`OnExit`] schedules for
|
||||
/// each state variant (if they don't already exist), an instance of [`apply_state_transition::<S>`]
|
||||
/// in [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and an
|
||||
/// instance of [`run_enter_schedule::<S>`] in [`StateTransition`] with a [`run_once`]
|
||||
/// condition to run the on enter schedule of the initial state.
|
||||
/// Adds [`State<S>`] and [`NextState<S>`] resources, and enables use of the [`OnEnter`], [`OnTransition`] and [`OnExit`] schedules.
|
||||
/// These schedules are triggered before [`Update`](crate::Update) and at startup.
|
||||
///
|
||||
/// If you would like to control how other systems run based on the current state, you can
|
||||
/// emulate this behavior using the [`in_state`] [`Condition`].
|
||||
///
|
||||
/// Note that you can also apply state transitions at other points in the schedule by adding
|
||||
/// the [`apply_state_transition::<S>`] system manually.
|
||||
///
|
||||
/// [`StateTransition`]: crate::StateTransition
|
||||
/// [`Update`]: crate::Update
|
||||
/// [`run_once`]: bevy_ecs::schedule::common_conditions::run_once
|
||||
/// [`run_enter_schedule::<S>`]: bevy_ecs::schedule::run_enter_schedule
|
||||
/// [`apply_state_transition::<S>`]: bevy_ecs::schedule::apply_state_transition
|
||||
pub fn insert_state<S: States>(&mut self, state: S) -> &mut Self {
|
||||
self.main_mut().insert_state(state);
|
||||
/// Note that you can also apply state transitions at other points in the schedule
|
||||
/// by triggering the [`StateTransition`](`bevy_ecs::schedule::StateTransition`) schedule manually.
|
||||
pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self {
|
||||
self.main_mut().insert_state::<S>(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a collection of systems to `schedule` (stored in the main world's [`Schedules`]).
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Adds one or more systems to the given schedule in this app's [`Schedules`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
|
|
@ -31,8 +31,7 @@ pub mod prelude {
|
|||
app::{App, AppExit},
|
||||
main_schedule::{
|
||||
First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main,
|
||||
PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, StateTransition,
|
||||
Update,
|
||||
PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, Update,
|
||||
},
|
||||
sub_app::SubApp,
|
||||
DynamicPlugin, Plugin, PluginGroup,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{App, Plugin};
|
||||
use bevy_ecs::{
|
||||
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel},
|
||||
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition},
|
||||
system::{Local, Resource},
|
||||
world::{Mut, World},
|
||||
};
|
||||
|
@ -73,12 +73,6 @@ pub struct First;
|
|||
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct PreUpdate;
|
||||
|
||||
/// Runs [state transitions](bevy_ecs::schedule::States).
|
||||
///
|
||||
/// See the [`Main`] schedule for some details about how schedules are run.
|
||||
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StateTransition;
|
||||
|
||||
/// Runs the [`FixedMain`] schedule in a loop according until all relevant elapsed time has been "consumed".
|
||||
///
|
||||
/// See the [`Main`] schedule for some details about how schedules are run.
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, StateTransition};
|
||||
use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup};
|
||||
use bevy_ecs::{
|
||||
event::EventRegistry,
|
||||
prelude::*,
|
||||
schedule::{
|
||||
common_conditions::run_once as run_once_condition, run_enter_schedule,
|
||||
InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel,
|
||||
setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel,
|
||||
ScheduleBuildSettings, ScheduleLabel,
|
||||
},
|
||||
system::SystemId,
|
||||
};
|
||||
|
@ -317,40 +317,61 @@ impl SubApp {
|
|||
}
|
||||
|
||||
/// See [`App::init_state`].
|
||||
pub fn init_state<S: States + FromWorld>(&mut self) -> &mut Self {
|
||||
pub fn init_state<S: FreelyMutableState + FromWorld>(&mut self) -> &mut Self {
|
||||
if !self.world.contains_resource::<State<S>>() {
|
||||
setup_state_transitions_in_world(&mut self.world, Some(Startup.intern()));
|
||||
self.init_resource::<State<S>>()
|
||||
.init_resource::<NextState<S>>()
|
||||
.add_event::<StateTransitionEvent<S>>()
|
||||
.add_systems(
|
||||
StateTransition,
|
||||
(
|
||||
run_enter_schedule::<S>.run_if(run_once_condition()),
|
||||
apply_state_transition::<S>,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
.add_event::<StateTransitionEvent<S>>();
|
||||
let schedule = self.get_schedule_mut(StateTransition).unwrap();
|
||||
S::register_state(schedule);
|
||||
}
|
||||
|
||||
// The OnEnter, OnExit, and OnTransition schedules are lazily initialized
|
||||
// (i.e. when the first system is added to them), so World::try_run_schedule
|
||||
// is used to fail gracefully if they aren't present.
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`App::insert_state`].
|
||||
pub fn insert_state<S: States>(&mut self, state: S) -> &mut Self {
|
||||
self.insert_resource(State::new(state))
|
||||
.init_resource::<NextState<S>>()
|
||||
.add_event::<StateTransitionEvent<S>>()
|
||||
.add_systems(
|
||||
StateTransition,
|
||||
(
|
||||
run_enter_schedule::<S>.run_if(run_once_condition()),
|
||||
apply_state_transition::<S>,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self {
|
||||
if !self.world.contains_resource::<State<S>>() {
|
||||
setup_state_transitions_in_world(&mut self.world, Some(Startup.intern()));
|
||||
self.insert_resource::<State<S>>(State::new(state))
|
||||
.init_resource::<NextState<S>>()
|
||||
.add_event::<StateTransitionEvent<S>>();
|
||||
|
||||
let schedule = self.get_schedule_mut(StateTransition).unwrap();
|
||||
S::register_state(schedule);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`App::add_computed_state`].
|
||||
pub fn add_computed_state<S: ComputedStates>(&mut self) -> &mut Self {
|
||||
if !self
|
||||
.world
|
||||
.contains_resource::<Events<StateTransitionEvent<S>>>()
|
||||
{
|
||||
setup_state_transitions_in_world(&mut self.world, Some(Startup.intern()));
|
||||
self.add_event::<StateTransitionEvent<S>>();
|
||||
let schedule = self.get_schedule_mut(StateTransition).unwrap();
|
||||
S::register_computed_state_systems(schedule);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`App::add_sub_state`].
|
||||
pub fn add_sub_state<S: SubStates>(&mut self) -> &mut Self {
|
||||
if !self
|
||||
.world
|
||||
.contains_resource::<Events<StateTransitionEvent<S>>>()
|
||||
{
|
||||
setup_state_transitions_in_world(&mut self.world, Some(Startup.intern()));
|
||||
self.init_resource::<NextState<S>>();
|
||||
self.add_event::<StateTransitionEvent<S>>();
|
||||
let schedule = self.get_schedule_mut(StateTransition).unwrap();
|
||||
S::register_sub_state_systems(schedule);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ proc-macro = true
|
|||
[dependencies]
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
|
||||
|
||||
syn = "2.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
|
||||
|
|
|
@ -521,3 +521,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -1,21 +1,144 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result};
|
||||
|
||||
use crate::bevy_ecs_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 trait_path = bevy_ecs_path();
|
||||
trait_path.segments.push(format_ident!("schedule").into());
|
||||
let mut base_trait_path = bevy_ecs_path();
|
||||
base_trait_path
|
||||
.segments
|
||||
.push(format_ident!("schedule").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_ecs_path();
|
||||
base_trait_path
|
||||
.segments
|
||||
.push(format_ident!("schedule").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()
|
||||
}
|
||||
|
|
|
@ -49,9 +49,10 @@ pub mod prelude {
|
|||
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
|
||||
removal_detection::RemovedComponents,
|
||||
schedule::{
|
||||
apply_deferred, apply_state_transition, common_conditions::*, Condition,
|
||||
IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, OnExit,
|
||||
OnTransition, Schedule, Schedules, State, StateTransitionEvent, States, SystemSet,
|
||||
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,
|
||||
},
|
||||
system::{
|
||||
Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -246,6 +246,7 @@ 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
|
||||
|
@ -263,6 +264,7 @@ Example | Description
|
|||
[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
|
||||
|
|
686
examples/ecs/computed_states.rs
Normal file
686
examples/ecs/computed_states.rs
Normal file
|
@ -0,0 +1,686 @@
|
|||
//! This example illustrates the use of [`ComputedStates`] for more complex state handling patterns.
|
||||
//!
|
||||
//! In this case, we'll be implementing the following pattern:
|
||||
//! - The game will start in a `Menu` state, which we can return to with `Esc`
|
||||
//! - From there, we can enter the game - where our bevy symbol moves around and changes color
|
||||
//! - While in game, we can pause and unpause the game using `Space`
|
||||
//! - We can also toggle "Turbo Mode" with the `T` key - where the movement and color changes are all faster. This
|
||||
//! is retained between pauses, but not if we exit to the main menu.
|
||||
//!
|
||||
//! In addition, we want to enable a "tutorial" mode, which will involve it's own state that is toggled in the main menu.
|
||||
//! This will display instructions about movement and turbo mode when in game and unpaused, and instructions on how to unpause when paused.
|
||||
//!
|
||||
//! To implement this, we will create 2 root-level states: [`AppState`] and [`TutorialState`].
|
||||
//! We will then create some computed states that derive from [`AppState`]: [`InGame`] and [`TurboMode`] are marker states implemented
|
||||
//! as Zero-Sized Structs (ZSTs), while [`IsPaused`] is an enum with 2 distinct states.
|
||||
//! And lastly, we'll add [`Tutorial`], a computed state deriving from [`TutorialState`], [`InGame`] and [`IsPaused`], with 2 distinct
|
||||
//! states to display the 2 tutorial texts.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
// To begin, we want to define our state objects.
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Menu,
|
||||
// Unlike in the `states` example, we're adding more data in this
|
||||
// version of our AppState. In this case, we actually have
|
||||
// 4 distinct "InGame" states - unpaused and no turbo, paused and no
|
||||
// turbo, unpaused and turbo and paused and turbo.
|
||||
InGame {
|
||||
paused: bool,
|
||||
turbo: bool,
|
||||
},
|
||||
}
|
||||
|
||||
// The tutorial state object, on the other hand, is a fairly simple enum.
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||
enum TutorialState {
|
||||
#[default]
|
||||
Active,
|
||||
Inactive,
|
||||
}
|
||||
|
||||
// Because we have 4 distinct values of `AppState` that mean we're "InGame", we're going to define
|
||||
// a separate "InGame" type and implement `ComputedStates` for it.
|
||||
// This allows us to only need to check against one type
|
||||
// when otherwise we'd need to check against multiple.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct InGame;
|
||||
|
||||
impl ComputedStates for InGame {
|
||||
// Our computed state depends on `AppState`, so we need to specify it as the SourceStates type.
|
||||
type SourceStates = AppState;
|
||||
|
||||
// The compute function takes in the `SourceStates`
|
||||
fn compute(sources: AppState) -> Option<Self> {
|
||||
// You might notice that InGame has no values - instead, in this case, the `State<InGame>` resource only exists
|
||||
// if the `compute` function would return `Some` - so only when we are in game.
|
||||
match sources {
|
||||
// No matter what the value of `paused` or `turbo` is, we're still in the game rather than a menu
|
||||
AppState::InGame { .. } => Some(Self),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Similarly, we want to have the TurboMode state - so we'll define that now.
|
||||
//
|
||||
// Having it separate from [`InGame`] and [`AppState`] like this allows us to check each of them separately, rather than
|
||||
// needing to compare against every version of the AppState that could involve them.
|
||||
//
|
||||
// In addition, it allows us to still maintain a strict type representation - you can't Turbo
|
||||
// if you aren't in game, for example - while still having the
|
||||
// flexibility to check for the states as if they were completely unrelated.
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct TurboMode;
|
||||
|
||||
impl ComputedStates for TurboMode {
|
||||
type SourceStates = AppState;
|
||||
|
||||
fn compute(sources: AppState) -> Option<Self> {
|
||||
match sources {
|
||||
AppState::InGame { turbo: true, .. } => Some(Self),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For the [`IsPaused`] state, we'll actually use an `enum` - because the difference between `Paused` and `NotPaused`
|
||||
// involve activating different systems.
|
||||
//
|
||||
// To clarify the difference, `InGame` and `TurboMode` both activate systems if they exist, and there is
|
||||
// no variation within them. So we defined them as Zero-Sized Structs.
|
||||
//
|
||||
// In contrast, pausing actually involve 3 distinct potential situations:
|
||||
// - it doesn't exist - this is when being paused is meaningless, like in the menu.
|
||||
// - it is `NotPaused` - in which elements like the movement system are active.
|
||||
// - it is `Paused` - in which those game systems are inactive, and a pause screen is shown.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
enum IsPaused {
|
||||
NotPaused,
|
||||
Paused,
|
||||
}
|
||||
|
||||
impl ComputedStates for IsPaused {
|
||||
type SourceStates = AppState;
|
||||
|
||||
fn compute(sources: AppState) -> Option<Self> {
|
||||
// Here we convert from our [`AppState`] to all potential [`IsPaused`] versions.
|
||||
match sources {
|
||||
AppState::InGame { paused: true, .. } => Some(Self::Paused),
|
||||
AppState::InGame { paused: false, .. } => Some(Self::NotPaused),
|
||||
// If `AppState` is not `InGame`, pausing is meaningless, and so we set it to `None`.
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, we have our tutorial, which actually has a more complex derivation.
|
||||
//
|
||||
// Like `IsPaused`, the tutorial has a few fully distinct possible states, so we want to represent them
|
||||
// as an Enum. However - in this case they are all dependant on multiple states: the root [`TutorialState`],
|
||||
// and both [`InGame`] and [`IsPaused`] - which are in turn derived from [`AppState`].
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
enum Tutorial {
|
||||
MovementInstructions,
|
||||
PauseInstructions,
|
||||
}
|
||||
|
||||
impl ComputedStates for Tutorial {
|
||||
// We can also use tuples of types that implement [`States`] as our [`SourceStates`].
|
||||
// That includes other [`ComputedStates`] - though circular dependencies are not supported
|
||||
// and will produce a compile error.
|
||||
//
|
||||
// We could define this as relying on [`TutorialState`] and [`AppState`] instead, but
|
||||
// then we would need to duplicate the derivation logic for [`InGame`] and [`IsPaused`].
|
||||
// In this example that is not a significant undertaking, but as a rule it is likely more
|
||||
// effective to rely on the already derived states to avoid the logic drifting apart.
|
||||
//
|
||||
// Notice that you can wrap any of the [`States`] here in [`Option`]s. If you do so,
|
||||
// the the computation will get called even if the state does not exist.
|
||||
type SourceStates = (TutorialState, InGame, Option<IsPaused>);
|
||||
|
||||
// Notice that we aren't using InGame - we're just using it as a source state to
|
||||
// prevent the computation from executing if we're not in game. Instead - this
|
||||
// ComputedState will just not exist in that situation.
|
||||
fn compute(
|
||||
(tutorial_state, _in_game, is_paused): (TutorialState, InGame, Option<IsPaused>),
|
||||
) -> Option<Self> {
|
||||
// If the tutorial is inactive we don't need to worry about it.
|
||||
if !matches!(tutorial_state, TutorialState::Active) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If we're paused, we're in the PauseInstructions tutorial
|
||||
// Otherwise, we're in the MovementInstructions tutorial
|
||||
match is_paused? {
|
||||
IsPaused::NotPaused => Some(Tutorial::MovementInstructions),
|
||||
IsPaused::Paused => Some(Tutorial::PauseInstructions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// We start the setup like we did in the states example.
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_state::<AppState>()
|
||||
.init_state::<TutorialState>()
|
||||
// After initializing the normal states, we'll use `.add_computed_state::<CS>()` to initialize our `ComputedStates`
|
||||
.add_computed_state::<InGame>()
|
||||
.add_computed_state::<IsPaused>()
|
||||
.add_computed_state::<TurboMode>()
|
||||
.add_computed_state::<Tutorial>()
|
||||
// we can then resume adding systems just like we would in any other case,
|
||||
// using our states as normal.
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(OnEnter(AppState::Menu), setup_menu)
|
||||
.add_systems(Update, menu.run_if(in_state(AppState::Menu)))
|
||||
.add_systems(OnExit(AppState::Menu), cleanup_menu)
|
||||
// We only want to run the [`setup_game`] function when we enter the [`AppState::InGame`] state, regardless
|
||||
// of whether the game is paused or not.
|
||||
.add_systems(OnEnter(InGame), setup_game)
|
||||
// And we only want to run the [`clear_game`] function when we leave the [`AppState::InGame`] state, regardless
|
||||
// of whether we're paused.
|
||||
.add_systems(OnExit(InGame), clear_state_bound_entities(InGame))
|
||||
// We want the color change, toggle_pause and quit_to_menu systems to ignore the paused condition, so we can use the [`InGame`] derived
|
||||
// state here as well.
|
||||
.add_systems(
|
||||
Update,
|
||||
(toggle_pause, change_color, quit_to_menu).run_if(in_state(InGame)),
|
||||
)
|
||||
// However, we only want to move or toggle turbo mode if we are not in a paused state.
|
||||
.add_systems(
|
||||
Update,
|
||||
(toggle_turbo, movement).run_if(in_state(IsPaused::NotPaused)),
|
||||
)
|
||||
// We can continue setting things up, following all the same patterns used above and in the `states` example.
|
||||
.add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
|
||||
.add_systems(
|
||||
OnExit(IsPaused::Paused),
|
||||
clear_state_bound_entities(IsPaused::Paused),
|
||||
)
|
||||
.add_systems(OnEnter(TurboMode), setup_turbo_text)
|
||||
.add_systems(OnExit(TurboMode), clear_state_bound_entities(TurboMode))
|
||||
.add_systems(
|
||||
OnEnter(Tutorial::MovementInstructions),
|
||||
movement_instructions,
|
||||
)
|
||||
.add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions)
|
||||
.add_systems(
|
||||
OnExit(Tutorial::MovementInstructions),
|
||||
clear_state_bound_entities(Tutorial::MovementInstructions),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(Tutorial::PauseInstructions),
|
||||
clear_state_bound_entities(Tutorial::PauseInstructions),
|
||||
)
|
||||
.add_systems(Update, log_transitions)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct MenuData {
|
||||
root_entity: Entity,
|
||||
}
|
||||
|
||||
#[derive(Component, PartialEq, Eq)]
|
||||
enum MenuButton {
|
||||
Play,
|
||||
Tutorial,
|
||||
}
|
||||
|
||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
|
||||
const ACTIVE_BUTTON: Color = Color::srgb(0.15, 0.85, 0.15);
|
||||
const HOVERED_ACTIVE_BUTTON: Color = Color::srgb(0.25, 0.55, 0.25);
|
||||
const PRESSED_ACTIVE_BUTTON: Color = Color::srgb(0.35, 0.95, 0.35);
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
}
|
||||
|
||||
fn setup_menu(mut commands: Commands, tutorial_state: Res<State<TutorialState>>) {
|
||||
let button_entity = commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
ButtonBundle {
|
||||
style: Style {
|
||||
width: Val::Px(200.),
|
||||
height: Val::Px(65.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Play,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Play",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
parent
|
||||
.spawn((
|
||||
ButtonBundle {
|
||||
style: Style {
|
||||
width: Val::Px(200.),
|
||||
height: Val::Px(65.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(match tutorial_state.get() {
|
||||
TutorialState::Active => ACTIVE_BUTTON,
|
||||
TutorialState::Inactive => NORMAL_BUTTON,
|
||||
}),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Tutorial,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Tutorial",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
})
|
||||
.id();
|
||||
commands.insert_resource(MenuData {
|
||||
root_entity: button_entity,
|
||||
});
|
||||
}
|
||||
|
||||
fn menu(
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
tutorial_state: Res<State<TutorialState>>,
|
||||
mut next_tutorial: ResMut<NextState<TutorialState>>,
|
||||
mut interaction_query: Query<
|
||||
(&Interaction, &mut UiImage, &MenuButton),
|
||||
(Changed<Interaction>, With<Button>),
|
||||
>,
|
||||
) {
|
||||
for (interaction, mut image, menu_button) in &mut interaction_query {
|
||||
let color = &mut image.color;
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
*color = if menu_button == &MenuButton::Tutorial
|
||||
&& tutorial_state.get() == &TutorialState::Active
|
||||
{
|
||||
PRESSED_ACTIVE_BUTTON
|
||||
} else {
|
||||
PRESSED_BUTTON
|
||||
};
|
||||
|
||||
match menu_button {
|
||||
MenuButton::Play => next_state.set(AppState::InGame {
|
||||
paused: false,
|
||||
turbo: false,
|
||||
}),
|
||||
MenuButton::Tutorial => next_tutorial.set(match tutorial_state.get() {
|
||||
TutorialState::Active => TutorialState::Inactive,
|
||||
TutorialState::Inactive => TutorialState::Active,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Interaction::Hovered => {
|
||||
if menu_button == &MenuButton::Tutorial
|
||||
&& tutorial_state.get() == &TutorialState::Active
|
||||
{
|
||||
*color = HOVERED_ACTIVE_BUTTON;
|
||||
} else {
|
||||
*color = HOVERED_BUTTON;
|
||||
}
|
||||
}
|
||||
Interaction::None => {
|
||||
if menu_button == &MenuButton::Tutorial
|
||||
&& tutorial_state.get() == &TutorialState::Active
|
||||
{
|
||||
*color = ACTIVE_BUTTON;
|
||||
} else {
|
||||
*color = NORMAL_BUTTON;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
|
||||
commands.entity(menu_data.root_entity).despawn_recursive();
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct StateBound<S: States>(S);
|
||||
|
||||
fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands.spawn((
|
||||
StateBound(InGame),
|
||||
SpriteBundle {
|
||||
texture: asset_server.load("branding/icon.png"),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn clear_state_bound_entities<S: States>(
|
||||
state: S,
|
||||
) -> impl Fn(Commands, Query<(Entity, &StateBound<S>)>) {
|
||||
info!("Clearing entities for {state:?}");
|
||||
move |mut commands, query| {
|
||||
for (entity, bound) in &query {
|
||||
if bound.0 == state {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SPEED: f32 = 100.0;
|
||||
const TURBO_SPEED: f32 = 300.0;
|
||||
fn movement(
|
||||
time: Res<Time>,
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
turbo: Option<Res<State<TurboMode>>>,
|
||||
mut query: Query<&mut Transform, With<Sprite>>,
|
||||
) {
|
||||
for mut transform in &mut query {
|
||||
let mut direction = Vec3::ZERO;
|
||||
if input.pressed(KeyCode::ArrowLeft) {
|
||||
direction.x -= 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowRight) {
|
||||
direction.x += 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowUp) {
|
||||
direction.y += 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowDown) {
|
||||
direction.y -= 1.0;
|
||||
}
|
||||
|
||||
if direction != Vec3::ZERO {
|
||||
transform.translation += direction.normalize()
|
||||
* if turbo.is_some() { TURBO_SPEED } else { SPEED }
|
||||
* time.delta_seconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_pause(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
current_state: Res<State<AppState>>,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
) {
|
||||
if input.just_pressed(KeyCode::Space) {
|
||||
if let AppState::InGame { paused, turbo } = current_state.get() {
|
||||
next_state.set(AppState::InGame {
|
||||
paused: !*paused,
|
||||
turbo: *turbo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_paused_screen(mut commands: Commands) {
|
||||
info!("Printing Pause");
|
||||
commands
|
||||
.spawn((
|
||||
StateBound(IsPaused::Paused),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Px(400.),
|
||||
height: Val::Px(400.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Play,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Paused",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_turbo(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
current_state: Res<State<AppState>>,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
) {
|
||||
if input.just_pressed(KeyCode::KeyT) {
|
||||
if let AppState::InGame { paused, turbo } = current_state.get() {
|
||||
next_state.set(AppState::InGame {
|
||||
paused: *paused,
|
||||
turbo: !*turbo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_turbo_text(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
StateBound(TurboMode),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::Start,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"TURBO MODE",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.3, 0.1),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn quit_to_menu(input: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextState<AppState>>) {
|
||||
if input.just_pressed(KeyCode::Escape) {
|
||||
next_state.set(AppState::Menu);
|
||||
}
|
||||
}
|
||||
|
||||
fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
|
||||
for mut sprite in &mut query {
|
||||
let new_color = LinearRgba {
|
||||
blue: (time.elapsed_seconds() * 0.5).sin() + 2.0,
|
||||
..LinearRgba::from(sprite.color)
|
||||
};
|
||||
|
||||
sprite.color = new_color.into();
|
||||
}
|
||||
}
|
||||
|
||||
fn movement_instructions(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
StateBound(Tutorial::MovementInstructions),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::End,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Move the bevy logo with the arrow keys",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.3, 0.3, 0.7),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Press T to enter TURBO MODE",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.3, 0.3, 0.7),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Press SPACE to pause",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.3, 0.3, 0.7),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Press ESCAPE to return to the menu",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.3, 0.3, 0.7),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn pause_instructions(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
StateBound(Tutorial::PauseInstructions),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::End,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Press SPACE to resume",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.3, 0.3, 0.7),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Press ESCAPE to return to the menu",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.3, 0.3, 0.7),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// print when either an `AppState` transition or a `TutorialState` transition happens
|
||||
fn log_transitions(
|
||||
mut transitions: EventReader<StateTransitionEvent<AppState>>,
|
||||
mut tutorial_transitions: EventReader<StateTransitionEvent<TutorialState>>,
|
||||
) {
|
||||
for transition in transitions.read() {
|
||||
info!(
|
||||
"transition: {:?} => {:?}",
|
||||
transition.before, transition.after
|
||||
);
|
||||
}
|
||||
for transition in tutorial_transitions.read() {
|
||||
info!(
|
||||
"tutorial transition: {:?} => {:?}",
|
||||
transition.before, transition.after
|
||||
);
|
||||
}
|
||||
}
|
275
examples/ecs/sub_states.rs
Normal file
275
examples/ecs/sub_states.rs
Normal file
|
@ -0,0 +1,275 @@
|
|||
//! This example illustrates the use of [`SubStates`] for more complex state handling patterns.
|
||||
//!
|
||||
//! [`SubStates`] are [`States`] that only exist while the App is in another [`State`]. They can
|
||||
//! be used to create more complex patterns while relying on simple enums, or to de-couple certain
|
||||
//! elements of complex state objects.
|
||||
//!
|
||||
//! In this case, we're transitioning from a `Menu` state to an `InGame` state, at which point we create
|
||||
//! a substate called `IsPaused` to track whether the game is paused or not.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Menu,
|
||||
InGame,
|
||||
}
|
||||
|
||||
// In this case, instead of deriving `States`, we derive `SubStates`
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, SubStates)]
|
||||
// And we need to add an attribute to let us know what the source state is
|
||||
// and what value it needs to have. This will ensure that unless we're
|
||||
// in [`AppState::InGame`], the [`IsPaused`] state resource
|
||||
// will not exist.
|
||||
#[source(AppState = AppState::InGame)]
|
||||
enum IsPaused {
|
||||
#[default]
|
||||
Running,
|
||||
Paused,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_state::<AppState>()
|
||||
.add_sub_state::<IsPaused>() // We set the substate up here.
|
||||
// Most of these remain the same
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(OnEnter(AppState::Menu), setup_menu)
|
||||
.add_systems(Update, menu.run_if(in_state(AppState::Menu)))
|
||||
.add_systems(OnExit(AppState::Menu), cleanup_menu)
|
||||
.add_systems(OnEnter(AppState::InGame), setup_game)
|
||||
.add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
|
||||
.add_systems(
|
||||
OnExit(IsPaused::Paused),
|
||||
clear_state_bound_entities(IsPaused::Paused),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
// Instead of relying on [`AppState::InGame`] here, we're relying on
|
||||
// [`IsPaused::Running`], since we don't want movement or color changes
|
||||
// if we're paused
|
||||
(movement, change_color).run_if(in_state(IsPaused::Running)),
|
||||
// The pause toggle, on the other hand, needs to work whether we're
|
||||
// paused or not, so it uses [`AppState::InGame`] instead.
|
||||
toggle_pause.run_if(in_state(AppState::InGame)),
|
||||
),
|
||||
)
|
||||
.add_systems(Update, log_transitions)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct MenuData {
|
||||
button_entity: Entity,
|
||||
}
|
||||
|
||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
}
|
||||
|
||||
fn setup_menu(mut commands: Commands) {
|
||||
let button_entity = commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn(ButtonBundle {
|
||||
style: Style {
|
||||
width: Val::Px(150.),
|
||||
height: Val::Px(65.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Play",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
})
|
||||
.id();
|
||||
commands.insert_resource(MenuData { button_entity });
|
||||
}
|
||||
|
||||
fn menu(
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
mut interaction_query: Query<
|
||||
(&Interaction, &mut UiImage),
|
||||
(Changed<Interaction>, With<Button>),
|
||||
>,
|
||||
) {
|
||||
for (interaction, mut image) in &mut interaction_query {
|
||||
let color = &mut image.color;
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
*color = PRESSED_BUTTON;
|
||||
next_state.set(AppState::InGame);
|
||||
}
|
||||
Interaction::Hovered => {
|
||||
*color = HOVERED_BUTTON;
|
||||
}
|
||||
Interaction::None => {
|
||||
*color = NORMAL_BUTTON;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
|
||||
commands.entity(menu_data.button_entity).despawn_recursive();
|
||||
}
|
||||
|
||||
fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands.spawn(SpriteBundle {
|
||||
texture: asset_server.load("branding/icon.png"),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
const SPEED: f32 = 100.0;
|
||||
fn movement(
|
||||
time: Res<Time>,
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut query: Query<&mut Transform, With<Sprite>>,
|
||||
) {
|
||||
for mut transform in &mut query {
|
||||
let mut direction = Vec3::ZERO;
|
||||
if input.pressed(KeyCode::ArrowLeft) {
|
||||
direction.x -= 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowRight) {
|
||||
direction.x += 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowUp) {
|
||||
direction.y += 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowDown) {
|
||||
direction.y -= 1.0;
|
||||
}
|
||||
|
||||
if direction != Vec3::ZERO {
|
||||
transform.translation += direction.normalize() * SPEED * time.delta_seconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
|
||||
for mut sprite in &mut query {
|
||||
let new_color = LinearRgba {
|
||||
blue: (time.elapsed_seconds() * 0.5).sin() + 2.0,
|
||||
..LinearRgba::from(sprite.color)
|
||||
};
|
||||
|
||||
sprite.color = new_color.into();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_pause(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
current_state: Res<State<IsPaused>>,
|
||||
mut next_state: ResMut<NextState<IsPaused>>,
|
||||
) {
|
||||
if input.just_pressed(KeyCode::Space) {
|
||||
next_state.set(match current_state.get() {
|
||||
IsPaused::Running => IsPaused::Paused,
|
||||
IsPaused::Paused => IsPaused::Running,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct StateBound<S: States>(S);
|
||||
|
||||
fn clear_state_bound_entities<S: States>(
|
||||
state: S,
|
||||
) -> impl Fn(Commands, Query<(Entity, &StateBound<S>)>) {
|
||||
move |mut commands, query| {
|
||||
for (entity, bound) in &query {
|
||||
if bound.0 == state {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_paused_screen(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
StateBound(IsPaused::Paused),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Px(400.),
|
||||
height: Val::Px(400.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Paused",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// print when an `AppState` transition happens
|
||||
fn log_transitions(mut transitions: EventReader<StateTransitionEvent<AppState>>) {
|
||||
for transition in transitions.read() {
|
||||
info!(
|
||||
"transition: {:?} => {:?}",
|
||||
transition.before, transition.after
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue