Skip to content

Commit

Permalink
Move utilities from examples to bevy_state and add concept of state…
Browse files Browse the repository at this point in the history
…-scoped entities (#13649)

# Objective

Move `StateScoped` and `log_transitions` to `bevy_state`, since they're
useful for end users.

Addresses #12852, although not in the way the issue had in mind.

## Solution

- Added `bevy_hierarchy` to default features of `bevy_state`.
- Move `log_transitions` to `transitions` module.
- Move `StateScoped` to `state_scoped` module, gated behind
`bevy_hierarchy` feature.
- Refreshed implementation.
- Added `enable_state_coped_entities<S: States>()` to add required
machinery to `App` for clearing state-scoped entities.


## Changelog

- Added `log_transitions` for displaying state transitions.
- Added `StateScoped` for binding entity lifetime to state and app
`enable_state_coped_entities` to register cleaning behavior.

---------

Co-authored-by: Alice Cecile <[email protected]>
Co-authored-by: François Mockers <[email protected]>
  • Loading branch information
3 people authored Jun 4, 2024
1 parent ad68722 commit 58a0c13
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 110 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1774,6 +1774,7 @@ wasm = false
name = "state"
path = "examples/state/state.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]

[package.metadata.example.state]
name = "State"
Expand All @@ -1785,6 +1786,7 @@ wasm = false
name = "sub_states"
path = "examples/state/sub_states.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]

[package.metadata.example.sub_states]
name = "Sub States"
Expand All @@ -1796,6 +1798,7 @@ wasm = false
name = "computed_states"
path = "examples/state/computed_states.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]

[package.metadata.example.computed_states]
name = "Computed States"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_dev_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev", features = [
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.14.0-dev" }
bevy_state = { path = "../bevy_state", version = "0.14.0-dev" }

# other
serde = { version = "1.0", features = ["derive"], optional = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_dev_tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub mod fps_overlay;
#[cfg(feature = "bevy_ui_debug")]
pub mod ui_debug_overlay;

pub mod states;

/// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools`
/// feature.
///
Expand Down
18 changes: 18 additions & 0 deletions crates/bevy_dev_tools/src/states.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Tools for debugging states.
use bevy_ecs::event::EventReader;
use bevy_state::state::{StateTransitionEvent, States};
use bevy_utils::tracing::info;

/// Logs state transitions into console.
///
/// This system is provided to make debugging easier by tracking state changes.
pub fn log_transitions<S: States>(mut transitions: EventReader<StateTransitionEvent<S>>) {
// State internals can generate at most one event (of type) per frame.
let Some(transition) = transitions.read().last() else {
return;
};
let name = std::any::type_name::<S>();
let StateTransitionEvent { exited, entered } = transition;
info!("{} transition: {:?} => {:?}", name, exited, entered);
}
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
52 changes: 46 additions & 6 deletions crates/bevy_state/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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,
};
use crate::state_scoped::clear_state_scoped_entities;

/// State installation methods for [`App`](bevy_app::App) and [`SubApp`](bevy_app::SubApp).
pub trait AppExtStates {
Expand Down Expand Up @@ -44,6 +49,11 @@ 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;

/// Enable state-scoped entity clearing for state `S`.
///
/// For more information refer to [`StateScoped`](crate::state_scoped::StateScoped).
fn enable_state_scoped_entities<S: States>(&mut self) -> &mut Self;
}

impl AppExtStates for SubApp {
Expand Down Expand Up @@ -91,10 +101,13 @@ impl AppExtStates for SubApp {
self.add_event::<StateTransitionEvent<S>>();
let schedule = self.get_schedule_mut(StateTransition).unwrap();
S::register_computed_state_systems(schedule);
let state = self.world().resource::<State<S>>().get().clone();
let state = self
.world()
.get_resource::<State<S>>()
.map(|s| s.get().clone());
self.world_mut().send_event(StateTransitionEvent {
exited: None,
entered: Some(state),
entered: state,
});
}

Expand All @@ -111,15 +124,36 @@ impl AppExtStates for SubApp {
self.add_event::<StateTransitionEvent<S>>();
let schedule = self.get_schedule_mut(StateTransition).unwrap();
S::register_sub_state_systems(schedule);
let state = self.world().resource::<State<S>>().get().clone();
let state = self
.world()
.get_resource::<State<S>>()
.map(|s| s.get().clone());
self.world_mut().send_event(StateTransitionEvent {
exited: None,
entered: Some(state),
entered: state,
});
}

self
}

fn enable_state_scoped_entities<S: States>(&mut self) -> &mut Self {
use bevy_utils::tracing::warn;

if !self
.world()
.contains_resource::<Events<StateTransitionEvent<S>>>()
{
let name = std::any::type_name::<S>();
warn!("State scoped entities are enabled for state `{}`, but the state isn't installed in the app!", name);
}
// We work with [`StateTransition`] in set [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`],
// because [`OnExit`] only runs for one specific variant of the state.
self.add_systems(
StateTransition,
clear_state_scoped_entities::<S>.in_set(StateTransitionSteps::ExitSchedules),
)
}
}

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

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

/// Registers the [`StateTransition`] schedule in the [`MainScheduleOrder`] to enable state processing.
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pub mod condition;
/// Provides definitions for the basic traits required by the state system
pub mod state;

/// Provides [`StateScoped`] and [`clear_state_scoped_entities`] for managing lifetime of entities.
pub mod state_scoped;

/// Most commonly used re-exported types.
pub mod prelude {
#[cfg(feature = "bevy_app")]
Expand All @@ -47,4 +50,6 @@ pub mod prelude {
ComputedStates, NextState, OnEnter, OnExit, OnTransition, State, StateSet, StateTransition,
StateTransitionEvent, States, SubStates,
};
#[doc(hidden)]
pub use crate::state_scoped::StateScoped;
}
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
84 changes: 84 additions & 0 deletions crates/bevy_state/src/state_scoped.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
system::{Commands, Query},
};
#[cfg(feature = "bevy_hierarchy")]
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 remember to configure your application
/// with [`enable_state_scoped_entities`](crate::app::AppExtStates::enable_state_scoped_entities) on your state(s) of choice.
///
/// If `bevy_hierarchy` feature is enabled, which it is by default, the despawn will be recursive.
///
/// ```
/// 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((
/// StateScoped(GameState::InGame),
/// Player
/// ));
/// }
///
/// # struct AppMock;
/// # impl AppMock {
/// # fn init_state<S>(&mut self) {}
/// # fn enable_state_scoped_entities<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.enable_state_scoped_entities::<GameState>();
/// app.add_systems(OnEnter(GameState::InGame), spawn_player);
/// ```
#[derive(Component)]
pub struct StateScoped<S: States>(pub S);

/// Removes entities marked with [`StateScoped<S>`]
/// when their state no longer matches the world state.
///
/// If `bevy_hierarchy` feature is enabled, which it is by default, the despawn will be recursive.
pub fn clear_state_scoped_entities<S: States>(
mut commands: Commands,
mut transitions: EventReader<StateTransitionEvent<S>>,
query: Query<(Entity, &StateScoped<S>)>,
) {
// We use the latest event, because state machine internals generate at most 1
// transition event (per type) each frame. No event means no change happened
// and we skip iterating all entities.
let Some(transition) = transitions.read().last() else {
return;
};
let Some(exited) = &transition.exited else {
return;
};
for (entity, binding) in &query {
if binding.0 == *exited {
#[cfg(feature = "bevy_hierarchy")]
commands.entity(entity).despawn_recursive();
#[cfg(not(feature = "bevy_hierarchy"))]
commands.entity(entity).despawn();
}
}
}
Loading

0 comments on commit 58a0c13

Please sign in to comment.