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 all 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
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 {
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
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),
MiniaczQ marked this conversation as resolved.
Show resolved Hide resolved
)
}
}

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