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:
Lee-Orr 2024-05-02 15:36:23 -04:00 committed by GitHub
parent e357b63448
commit b8832dc862
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2543 additions and 134 deletions

View file

@ -1692,6 +1692,28 @@ description = "Illustrates how to use States to control transitioning from a Men
category = "ECS (Entity Component System)" category = "ECS (Entity Component System)"
wasm = false 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]] [[example]]
name = "system_piping" name = "system_piping"
path = "examples/ecs/system_piping.rs" path = "examples/ecs/system_piping.rs"

View file

@ -7,7 +7,7 @@ use bevy_ecs::{
event::{event_update_system, ManualEventReader}, event::{event_update_system, ManualEventReader},
intern::Interned, intern::Interned,
prelude::*, prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel}, schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel},
system::SystemId, system::SystemId,
}; };
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
@ -266,26 +266,17 @@ impl App {
/// Initializes a [`State`] with standard starting values. /// 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 /// Adds [`State<S>`] and [`NextState<S>`] resources, and enables use of the [`OnEnter`], [`OnTransition`] and [`OnExit`] schedules.
/// each state variant (if they don't already exist), an instance of [`apply_state_transition::<S>`] /// These schedules are triggered before [`Update`](crate::Update) and at startup.
/// 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.
/// ///
/// If you would like to control how other systems run based on the current state, you can /// 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`]. /// emulate this behavior using the [`in_state`] [`Condition`].
/// ///
/// Note that you can also apply state transitions at other points in the schedule by adding /// Note that you can also apply state transitions at other points in the schedule
/// the [`apply_state_transition::<S>`] system manually. /// by triggering the [`StateTransition`](`bevy_ecs::schedule::StateTransition`) schedule manually.
/// pub fn init_state<S: FreelyMutableState + FromWorld>(&mut self) -> &mut Self {
/// [`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 {
self.main_mut().init_state::<S>(); self.main_mut().init_state::<S>();
self self
} }
@ -293,29 +284,42 @@ impl App {
/// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously
/// added of the same type. /// added of the same type.
/// ///
/// Adds [`State<S>`] and [`NextState<S>`] resources, [`OnEnter`] and [`OnExit`] schedules for /// Adds [`State<S>`] and [`NextState<S>`] resources, and enables use of the [`OnEnter`], [`OnTransition`] and [`OnExit`] schedules.
/// each state variant (if they don't already exist), an instance of [`apply_state_transition::<S>`] /// These schedules are triggered before [`Update`](crate::Update) and at startup.
/// 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.
/// ///
/// If you would like to control how other systems run based on the current state, you can /// 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`]. /// emulate this behavior using the [`in_state`] [`Condition`].
/// ///
/// Note that you can also apply state transitions at other points in the schedule by adding /// Note that you can also apply state transitions at other points in the schedule
/// the [`apply_state_transition::<S>`] system manually. /// by triggering the [`StateTransition`](`bevy_ecs::schedule::StateTransition`) schedule manually.
/// pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self {
/// [`StateTransition`]: crate::StateTransition self.main_mut().insert_state::<S>(state);
/// [`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);
self 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 /// # Examples
/// ///

View file

@ -31,8 +31,7 @@ pub mod prelude {
app::{App, AppExit}, app::{App, AppExit},
main_schedule::{ main_schedule::{
First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main, First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main,
PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, StateTransition, PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, Update,
Update,
}, },
sub_app::SubApp, sub_app::SubApp,
DynamicPlugin, Plugin, PluginGroup, DynamicPlugin, Plugin, PluginGroup,

View file

@ -1,6 +1,6 @@
use crate::{App, Plugin}; use crate::{App, Plugin};
use bevy_ecs::{ use bevy_ecs::{
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel}, schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition},
system::{Local, Resource}, system::{Local, Resource},
world::{Mut, World}, world::{Mut, World},
}; };
@ -73,12 +73,6 @@ pub struct First;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PreUpdate; 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". /// 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. /// See the [`Main`] schedule for some details about how schedules are run.

View file

@ -1,10 +1,10 @@
use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, StateTransition}; use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup};
use bevy_ecs::{ use bevy_ecs::{
event::EventRegistry, event::EventRegistry,
prelude::*, prelude::*,
schedule::{ schedule::{
common_conditions::run_once as run_once_condition, run_enter_schedule, setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel,
InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel, ScheduleBuildSettings, ScheduleLabel,
}, },
system::SystemId, system::SystemId,
}; };
@ -317,40 +317,61 @@ impl SubApp {
} }
/// See [`App::init_state`]. /// 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>>() { if !self.world.contains_resource::<State<S>>() {
setup_state_transitions_in_world(&mut self.world, Some(Startup.intern()));
self.init_resource::<State<S>>() self.init_resource::<State<S>>()
.init_resource::<NextState<S>>() .init_resource::<NextState<S>>()
.add_event::<StateTransitionEvent<S>>() .add_event::<StateTransitionEvent<S>>();
.add_systems( let schedule = self.get_schedule_mut(StateTransition).unwrap();
StateTransition, S::register_state(schedule);
(
run_enter_schedule::<S>.run_if(run_once_condition()),
apply_state_transition::<S>,
)
.chain(),
);
} }
// 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 self
} }
/// See [`App::insert_state`]. /// See [`App::insert_state`].
pub fn insert_state<S: States>(&mut self, state: S) -> &mut Self { pub fn insert_state<S: FreelyMutableState>(&mut self, state: S) -> &mut Self {
self.insert_resource(State::new(state)) if !self.world.contains_resource::<State<S>>() {
.init_resource::<NextState<S>>() setup_state_transitions_in_world(&mut self.world, Some(Startup.intern()));
.add_event::<StateTransitionEvent<S>>() self.insert_resource::<State<S>>(State::new(state))
.add_systems( .init_resource::<NextState<S>>()
StateTransition, .add_event::<StateTransitionEvent<S>>();
(
run_enter_schedule::<S>.run_if(run_once_condition()), let schedule = self.get_schedule_mut(StateTransition).unwrap();
apply_state_transition::<S>, S::register_state(schedule);
) }
.chain(),
); 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 self
} }

View file

@ -11,7 +11,7 @@ proc-macro = true
[dependencies] [dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
syn = "2.0" syn = { version = "2.0", features = ["full"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"

View file

@ -521,3 +521,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
pub fn derive_states(input: TokenStream) -> TokenStream { pub fn derive_states(input: TokenStream) -> TokenStream {
states::derive_states(input) states::derive_states(input)
} }
#[proc_macro_derive(SubStates, attributes(source))]
pub fn derive_substates(input: TokenStream) -> TokenStream {
states::derive_substates(input)
}

View file

@ -1,21 +1,144 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{format_ident, quote}; 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; use crate::bevy_ecs_path;
pub fn derive_states(input: TokenStream) -> TokenStream { pub fn derive_states(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput); let ast = parse_macro_input!(input as DeriveInput);
let generics = ast.generics; let generics = ast.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut trait_path = bevy_ecs_path(); let mut base_trait_path = bevy_ecs_path();
trait_path.segments.push(format_ident!("schedule").into()); 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()); 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 struct_name = &ast.ident;
quote! { quote! {
impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} 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() .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()
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -246,6 +246,7 @@ Example | Description
--- | --- --- | ---
[Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components [Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components
[Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events [Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events
[Computed States](../examples/ecs/computed_states.rs) | Advanced state patterns using Computed States
[Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type
[Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules [Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules
[Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components
@ -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 [Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system
[Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up) [Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up)
[State](../examples/ecs/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state [State](../examples/ecs/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state
[Sub States](../examples/ecs/sub_states.rs) | Using Sub States for hierarchical state handling.
[System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state [System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state
[System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` [System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam`
[System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully

View 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
View 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
);
}
}