Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move utilities from examples to bevy_state and add concept of state-scoped entities #13649

Merged
merged 19 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/bevy_state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ categories = ["game-engines", "data-structures"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["bevy_reflect", "bevy_app"]
default = ["bevy_reflect", "bevy_app", "bevy_hierarchy"]
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]
bevy_app = ["dep:bevy_app"]
bevy_hierarchy = ["dep:bevy_hierarchy"]

[dependencies]
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_state_macros = { path = "macros", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
bevy_app = { path = "../bevy_app", version = "0.14.0-dev", optional = true }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev", optional = true }

[lints]
workspace = true
Expand Down
30 changes: 28 additions & 2 deletions crates/bevy_state/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use bevy_app::{App, MainScheduleOrder, Plugin, PreUpdate, Startup, SubApp};
use bevy_ecs::{event::Events, schedule::ScheduleLabel, world::FromWorld};
use bevy_ecs::{
event::Events,
schedule::{IntoSystemConfigs, ScheduleLabel},
world::FromWorld,
};

use crate::state::{
setup_state_transitions_in_world, ComputedStates, FreelyMutableState, NextState, State,
StateTransition, StateTransitionEvent, SubStates,
StateTransition, StateTransitionEvent, StateTransitionSteps, States, SubStates,
};
#[cfg(feature = "bevy_hierarchy")]
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
use crate::state_bound::clear_state_bound_entities;

/// State installation methods for [`App`](bevy_app::App) and [`SubApp`](bevy_app::SubApp).
pub trait AppExtStates {
Expand Down Expand Up @@ -44,6 +50,12 @@ pub trait AppExtStates {
///
/// This method is idempotent: it has no effect when called again using the same generic type.
fn add_sub_state<S: SubStates>(&mut self) -> &mut Self;

#[cfg(feature = "bevy_hierarchy")]
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
/// Enable state bound entity clearing for state `S`.
///
/// For more information refer to [`StateBound`](crate::state_bound::StateBound).
fn add_state_bound<S: States>(&mut self) -> &mut Self;
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
}

impl AppExtStates for SubApp {
Expand Down Expand Up @@ -120,6 +132,14 @@ impl AppExtStates for SubApp {

self
}

#[cfg(feature = "bevy_hierarchy")]
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
fn add_state_bound<S: States>(&mut self) -> &mut Self {
self.add_systems(
StateTransition,
clear_state_bound_entities::<S>.in_set(StateTransitionSteps::ExitSchedules),
)
}
}

impl AppExtStates for App {
Expand All @@ -142,6 +162,12 @@ impl AppExtStates for App {
self.main_mut().add_sub_state::<S>();
self
}

#[cfg(feature = "bevy_hierarchy")]
fn add_state_bound<S: States>(&mut self) -> &mut Self {
self.main_mut().add_state_bound::<S>();
self
}
}

/// Registers the [`StateTransition`] schedule in the [`MainScheduleOrder`] to enable state processing.
Expand Down
10 changes: 8 additions & 2 deletions crates/bevy_state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub mod app;
pub mod condition;
/// Provides definitions for the basic traits required by the state system
pub mod state;
#[cfg(feature = "bevy_hierarchy")]
/// Provides [`StateBound`] and [`clear_state_bound_entities`] for managing lifetime of entities.
pub mod state_bound;

/// Most commonly used re-exported types.
pub mod prelude {
Expand All @@ -44,7 +47,10 @@ pub mod prelude {
pub use crate::condition::*;
#[doc(hidden)]
pub use crate::state::{
ComputedStates, NextState, OnEnter, OnExit, OnTransition, State, StateSet, StateTransition,
StateTransitionEvent, States, SubStates,
log_transitions, ComputedStates, NextState, OnEnter, OnExit, OnTransition, State, StateSet,
StateTransition, StateTransitionEvent, States, SubStates,
};
#[cfg(feature = "bevy_hierarchy")]
#[doc(hidden)]
pub use crate::state_bound::StateBound;
}
8 changes: 4 additions & 4 deletions crates/bevy_state/src/state/states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ use std::hash::Hash;
///
/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
/// enum GameState {
/// #[default]
/// MainMenu,
/// SettingsMenu,
/// InGame,
/// #[default]
/// MainMenu,
/// SettingsMenu,
/// InGame,
/// }
///
/// fn handle_escape_pressed(mut next_state: ResMut<NextState<GameState>>) {
Expand Down
13 changes: 13 additions & 0 deletions crates/bevy_state/src/state/transitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use bevy_ecs::{
system::{Commands, In, ResMut},
world::World,
};
use bevy_utils::tracing::info;

use super::{resources::State, states::States};

Expand Down Expand Up @@ -206,3 +207,15 @@ pub(crate) fn run_transition<S: States>(

let _ = world.try_run_schedule(OnTransition { exited, entered });
}

/// Logs state transitions into console.
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
///
/// This system is provided to make debugging easier for Bevy developers.
pub fn log_transitions<S: States>(mut transitions: EventReader<StateTransitionEvent<S>>) {
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
for transition in transitions.read() {
info!(
"Transition: {:?} => {:?}",
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
transition.exited, transition.entered
);
}
}
73 changes: 73 additions & 0 deletions crates/bevy_state/src/state_bound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
system::{Commands, Query},
};
use bevy_hierarchy::DespawnRecursiveExt;

use crate::state::{StateTransitionEvent, States};

/// Entities marked with this component will be removed
/// when the world's state of the matching type no longer matches the supplied value.
///
/// To enable this feature, register the [`clear_state_bound_entities`]
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
/// system for selected states.
///
/// ```
/// use bevy_state::prelude::*;
/// use bevy_ecs::prelude::*;
///
/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
/// enum GameState {
/// #[default]
/// MainMenu,
/// SettingsMenu,
/// InGame,
/// }
///
/// # #[derive(Component)]
/// # struct Player;
///
/// fn spawn_player(mut commands: Commands) {
/// commands.spawn((
/// StateBound(GameState::InGame),
/// Player
/// ));
/// }
///
/// # struct AppMock;
/// # impl AppMock {
/// # fn init_state<S>(&mut self) {}
/// # fn add_state_bound<S>(&mut self) {}
/// # fn add_systems<S, M>(&mut self, schedule: S, systems: impl IntoSystemConfigs<M>) {}
/// # }
/// # struct Update;
/// # let mut app = AppMock;
///
/// app.init_state::<GameState>();
/// app.add_state_bound::<GameState>();
/// app.add_systems(OnEnter(GameState::InGame), spawn_player);
/// ```
#[derive(Component)]
pub struct StateBound<S: States>(pub S);
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved

/// Removes entities marked with [`StateBound<S>`]
/// when their state no longer matches the world state.
pub fn clear_state_bound_entities<S: States>(
mut commands: Commands,
mut transitions: EventReader<StateTransitionEvent<S>>,
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
query: Query<(Entity, &StateBound<S>)>,
) {
let Some(transition) = transitions.read().last() else {
return;
};
let Some(exited) = &transition.exited else {
return;
};
for (entity, binding) in &query {
if binding.0 == *exited {
commands.entity(entity).despawn_recursive();
}
}
}
57 changes: 9 additions & 48 deletions examples/state/computed_states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ fn main() {
.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))
.add_state_bound::<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(
Expand All @@ -200,26 +200,22 @@ fn main() {
)
// 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_state_bound::<IsPaused>()
.add_systems(OnEnter(TurboMode), setup_turbo_text)
.add_systems(OnExit(TurboMode), clear_state_bound_entities(TurboMode))
.add_state_bound::<TurboMode>()
.add_systems(
OnEnter(Tutorial::MovementInstructions),
movement_instructions,
)
.add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions)
.add_state_bound::<Tutorial>()
.add_systems(
OnExit(Tutorial::MovementInstructions),
clear_state_bound_entities(Tutorial::MovementInstructions),
)
.add_systems(
OnExit(Tutorial::PauseInstructions),
clear_state_bound_entities(Tutorial::PauseInstructions),
Update,
(
log_transitions::<AppState>,
log_transitions::<TutorialState>,
),
)
.add_systems(Update, log_transitions)
.run();
}

Expand Down Expand Up @@ -277,22 +273,6 @@ fn menu(
}
}

#[derive(Component)]
struct StateBound<S: States>(S);

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

fn toggle_pause(
input: Res<ButtonInput<KeyCode>>,
current_state: Res<State<AppState>>,
Expand Down Expand Up @@ -329,25 +309,6 @@ fn quit_to_menu(input: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextSta
}
}

/// 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.exited, transition.entered
);
}
for transition in tutorial_transitions.read() {
info!(
"tutorial transition: {:?} => {:?}",
transition.exited, transition.entered
);
}
}

mod ui {
use crate::*;

Expand Down
13 changes: 1 addition & 12 deletions examples/state/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn main() {
Update,
(movement, change_color).run_if(in_state(AppState::InGame)),
)
.add_systems(Update, log_transitions)
.add_systems(Update, log_transitions::<AppState>)
.run();
}

Expand Down Expand Up @@ -163,14 +163,3 @@ fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
sprite.color = new_color.into();
}
}

/// print when an `AppState` transition happens
/// also serves as an example of how to use `StateTransitionEvent`
fn log_transitions(mut transitions: EventReader<StateTransitionEvent<AppState>>) {
for transition in transitions.read() {
info!(
"transition: {:?} => {:?}",
transition.exited, transition.entered
);
}
}
32 changes: 2 additions & 30 deletions examples/state/sub_states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ fn main() {
.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_state_bound::<IsPaused>()
.add_systems(
Update,
(
Expand All @@ -59,7 +56,7 @@ fn main() {
toggle_pause.run_if(in_state(AppState::InGame)),
),
)
.add_systems(Update, log_transitions)
.add_systems(Update, log_transitions::<AppState>)
.run();
}

Expand Down Expand Up @@ -142,31 +139,6 @@ fn toggle_pause(
}
}

#[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();
}
}
}
}

/// print when an `AppState` transition happens
fn log_transitions(mut transitions: EventReader<StateTransitionEvent<AppState>>) {
for transition in transitions.read() {
info!(
"transition: {:?} => {:?}",
transition.exited, transition.entered
);
}
}

mod ui {
use crate::*;

Expand Down
Loading