From fcd778b00087be57fad9a2e11dae50a8e39adeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Thu, 31 Oct 2024 00:34:53 +0100 Subject: [PATCH 1/4] enable_state_scoped_entities as a derive attribute --- crates/bevy_state/macros/src/lib.rs | 4 +- crates/bevy_state/macros/src/states.rs | 56 ++++++++++++++++++++++++-- crates/bevy_state/src/app.rs | 12 ++++++ crates/bevy_state/src/state/states.rs | 5 +++ crates/bevy_state/src/state_scoped.rs | 5 +-- examples/state/sub_states.rs | 2 +- examples/testbed/2d.rs | 2 +- examples/testbed/3d.rs | 2 +- 8 files changed, 76 insertions(+), 12 deletions(-) diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs index b18895cdb86ad..8e3300a57b043 100644 --- a/crates/bevy_state/macros/src/lib.rs +++ b/crates/bevy_state/macros/src/lib.rs @@ -9,12 +9,12 @@ mod states; use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; -#[proc_macro_derive(States)] +#[proc_macro_derive(States, attributes(states))] pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } -#[proc_macro_derive(SubStates, attributes(source))] +#[proc_macro_derive(SubStates, attributes(states, source))] pub fn derive_substates(input: TokenStream) -> TokenStream { states::derive_substates(input) } diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs index 7a3872fdd135f..b445a98649c61 100644 --- a/crates/bevy_state/macros/src/states.rs +++ b/crates/bevy_state/macros/src/states.rs @@ -4,9 +4,42 @@ use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; use crate::bevy_state_path; +pub const STATES: &str = "states"; +pub const SCOPED_ENTITIES: &str = "scoped_entities"; + +struct StatesAttrs { + scoped_entities_enabled: bool, +} + +fn parse_states_attr(ast: &DeriveInput) -> Result { + let mut attrs = StatesAttrs { + scoped_entities_enabled: false, + }; + + for attr in ast.attrs.iter() { + if attr.path().is_ident(STATES) { + attr.parse_nested_meta(|nested| { + if nested.path.is_ident(SCOPED_ENTITIES) { + attrs.scoped_entities_enabled = true; + Ok(()) + } else { + Err(nested.error("Unsupported attribute")) + } + })?; + } + } + + Ok(attrs) +} + pub fn derive_states(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); + let attrs = match parse_states_attr(&ast) { + Ok(attrs) => attrs, + Err(e) => return e.into_compile_error().into(), + }; + let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); @@ -23,8 +56,14 @@ pub fn derive_states(input: TokenStream) -> TokenStream { let struct_name = &ast.ident; + let scoped_entities_enabled = attrs.scoped_entities_enabled; + 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 { + fn scoped_entities_enabled() -> bool { + #scoped_entities_enabled + } + } impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { } @@ -37,7 +76,7 @@ struct Source { source_value: Pat, } -fn parse_sources_attr(ast: &DeriveInput) -> Result { +fn parse_sources_attr(ast: &DeriveInput) -> Result<(StatesAttrs, Source)> { let mut result = ast .attrs .iter() @@ -73,16 +112,19 @@ fn parse_sources_attr(ast: &DeriveInput) -> Result { )); } + let states_attrs = parse_states_attr(ast)?; + let Some(result) = result.pop() else { return Err(syn::Error::new(ast.span(), "SubStates require a source")); }; - Ok(result) + Ok((states_attrs, 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 (states_attrs, 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(); @@ -113,6 +155,8 @@ pub fn derive_substates(input: TokenStream) -> TokenStream { let source_state_type = sources.source_type; let source_state_value = sources.source_value; + let scoped_entities_enabled = states_attrs.scoped_entities_enabled; + let result = quote! { impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { type SourceStates = #source_state_type; @@ -124,6 +168,10 @@ pub fn derive_substates(input: TokenStream) -> TokenStream { impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + + fn scoped_entities_enabled() -> bool { + #scoped_entities_enabled + } } impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 666b35ce846a6..df5231a6fbae3 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -104,6 +104,9 @@ impl AppExtStates for SubApp { exited: None, entered: Some(state), }); + if S::scoped_entities_enabled() { + self.enable_state_scoped_entities::(); + } } else { let name = core::any::type_name::(); warn!("State {} is already initialized.", name); @@ -126,6 +129,9 @@ impl AppExtStates for SubApp { exited: None, entered: Some(state), }); + if S::scoped_entities_enabled() { + self.enable_state_scoped_entities::(); + } } else { // Overwrite previous state and initial event self.insert_resource::>(State::new(state.clone())); @@ -160,6 +166,9 @@ impl AppExtStates for SubApp { exited: None, entered: state, }); + if S::scoped_entities_enabled() { + self.enable_state_scoped_entities::(); + } } else { let name = core::any::type_name::(); warn!("Computed state {} is already initialized.", name); @@ -188,6 +197,9 @@ impl AppExtStates for SubApp { exited: None, entered: state, }); + if S::scoped_entities_enabled() { + self.enable_state_scoped_entities::(); + } } else { let name = core::any::type_name::(); warn!("Sub state {} is already initialized.", name); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs index 3d315ab202fd8..85162453ecbc9 100644 --- a/crates/bevy_state/src/state/states.rs +++ b/crates/bevy_state/src/state/states.rs @@ -64,4 +64,9 @@ pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug /// Used to help order transitions and de-duplicate [`ComputedStates`](crate::state::ComputedStates), as well as prevent cyclical /// `ComputedState` dependencies. const DEPENDENCY_DEPTH: usize = 1; + + /// Should scoped entities be enabled for this state? + fn scoped_entities_enabled() -> bool { + false + } } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 5f1b2d0be31b1..51b051c8698e6 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -16,8 +16,7 @@ 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. +/// To enable this feature remember to add the attribute `#[states(scoped_entities)]` when deriving [`States`]. /// /// If `bevy_hierarchy` feature is enabled, which it is by default, the despawn will be recursive. /// @@ -26,6 +25,7 @@ use crate::state::{StateTransitionEvent, States}; /// use bevy_ecs::prelude::*; /// /// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// #[states(scoped_entities)] /// enum GameState { /// #[default] /// MainMenu, @@ -53,7 +53,6 @@ use crate::state::{StateTransitionEvent, States}; /// # let mut app = AppMock; /// /// app.init_state::(); -/// app.enable_state_scoped_entities::(); /// app.add_systems(OnEnter(GameState::InGame), spawn_player); /// ``` #[derive(Component, Clone)] diff --git a/examples/state/sub_states.rs b/examples/state/sub_states.rs index 959e58d7f7b82..a09f4f81dc1f3 100644 --- a/examples/state/sub_states.rs +++ b/examples/state/sub_states.rs @@ -25,6 +25,7 @@ enum AppState { // in [`AppState::InGame`], the [`IsPaused`] state resource // will not exist. #[source(AppState = AppState::InGame)] +#[states(scoped_entities)] enum IsPaused { #[default] Running, @@ -43,7 +44,6 @@ fn main() { .add_systems(OnExit(AppState::Menu), cleanup_menu) .add_systems(OnEnter(AppState::InGame), setup_game) .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen) - .enable_state_scoped_entities::() .add_systems( Update, ( diff --git a/examples/testbed/2d.rs b/examples/testbed/2d.rs index 639418168238c..5fe448c842a18 100644 --- a/examples/testbed/2d.rs +++ b/examples/testbed/2d.rs @@ -10,7 +10,6 @@ fn main() { let mut app = App::new(); app.add_plugins((DefaultPlugins,)) .init_state::() - .enable_state_scoped_entities::() .add_systems(OnEnter(Scene::Shapes), shapes::setup) .add_systems(OnEnter(Scene::Bloom), bloom::setup) .add_systems(OnEnter(Scene::Text), text::setup) @@ -20,6 +19,7 @@ fn main() { } #[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)] +#[states(scoped_entities)] enum Scene { #[default] Shapes, diff --git a/examples/testbed/3d.rs b/examples/testbed/3d.rs index 9f44840a8fa31..a017a10be7f30 100644 --- a/examples/testbed/3d.rs +++ b/examples/testbed/3d.rs @@ -10,7 +10,6 @@ fn main() { let mut app = App::new(); app.add_plugins((DefaultPlugins,)) .init_state::() - .enable_state_scoped_entities::() .add_systems(OnEnter(Scene::Light), light::setup) .add_systems(OnEnter(Scene::Animation), animation::setup) .add_systems(Update, switch_scene); @@ -24,6 +23,7 @@ fn main() { } #[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)] +#[states(scoped_entities)] enum Scene { #[default] Light, From f230a9336305217aa6b93a3b104e6321f79e2b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 1 Nov 2024 16:53:47 +0100 Subject: [PATCH 2/4] use a const instead of a fn --- crates/bevy_state/macros/src/states.rs | 8 ++------ crates/bevy_state/src/app.rs | 8 ++++---- crates/bevy_state/src/state/states.rs | 7 +++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs index b445a98649c61..52c133f6ee204 100644 --- a/crates/bevy_state/macros/src/states.rs +++ b/crates/bevy_state/macros/src/states.rs @@ -60,9 +60,7 @@ pub fn derive_states(input: TokenStream) -> TokenStream { quote! { impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { - fn scoped_entities_enabled() -> bool { - #scoped_entities_enabled - } + const SCOPED_ENTITIES_ENABLED: bool = #scoped_entities_enabled; } impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { @@ -169,9 +167,7 @@ pub fn derive_substates(input: TokenStream) -> TokenStream { impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; - fn scoped_entities_enabled() -> bool { - #scoped_entities_enabled - } + const SCOPED_ENTITIES_ENABLED: bool = #scoped_entities_enabled; } impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index df5231a6fbae3..243489e9246b9 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -104,7 +104,7 @@ impl AppExtStates for SubApp { exited: None, entered: Some(state), }); - if S::scoped_entities_enabled() { + if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { @@ -129,7 +129,7 @@ impl AppExtStates for SubApp { exited: None, entered: Some(state), }); - if S::scoped_entities_enabled() { + if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { @@ -166,7 +166,7 @@ impl AppExtStates for SubApp { exited: None, entered: state, }); - if S::scoped_entities_enabled() { + if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { @@ -197,7 +197,7 @@ impl AppExtStates for SubApp { exited: None, entered: state, }); - if S::scoped_entities_enabled() { + if S::SCOPED_ENTITIES_ENABLED { self.enable_state_scoped_entities::(); } } else { diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs index 85162453ecbc9..a0f512a7a8d77 100644 --- a/crates/bevy_state/src/state/states.rs +++ b/crates/bevy_state/src/state/states.rs @@ -65,8 +65,7 @@ pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug /// `ComputedState` dependencies. const DEPENDENCY_DEPTH: usize = 1; - /// Should scoped entities be enabled for this state? - fn scoped_entities_enabled() -> bool { - false - } + /// Should [`StateScoped`] be enabled for this state? If set to `true`, the [`StateScoped`] + /// component will be used to remove entities when changing state. + const SCOPED_ENTITIES_ENABLED: bool = false; } From d5b88a8909fa1b654c337a46a60f8b776e243ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 1 Nov 2024 16:59:21 +0100 Subject: [PATCH 3/4] some docs --- crates/bevy_state/src/app.rs | 3 +++ crates/bevy_state/src/state_scoped.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 243489e9246b9..c1aaf0933337a 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -58,6 +58,9 @@ pub trait AppExtStates { /// Enable state-scoped entity clearing for state `S`. /// + /// If the [`States`] trait was derived with the `#[states(scoped_entities)]` attribute, it + /// will be called automatically. + /// /// For more information refer to [`StateScoped`](crate::state_scoped::StateScoped). fn enable_state_scoped_entities(&mut self) -> &mut Self; diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 51b051c8698e6..3e9f49f51734d 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -17,6 +17,7 @@ use crate::state::{StateTransitionEvent, States}; /// when the world's state of the matching type no longer matches the supplied value. /// /// To enable this feature remember to add the attribute `#[states(scoped_entities)]` when deriving [`States`]. +/// It's also possible to enable it when adding the state to an app with [`enable_state_scoped_entities`](crate::app::AppExtStates::enable_state_scoped_entities). /// /// If `bevy_hierarchy` feature is enabled, which it is by default, the despawn will be recursive. /// From 68b999bf29b69e59d078f1d947615a5796fe8d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 1 Nov 2024 18:23:52 +0100 Subject: [PATCH 4/4] fix doc link --- crates/bevy_state/src/state/states.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs index a0f512a7a8d77..8e2422d46a361 100644 --- a/crates/bevy_state/src/state/states.rs +++ b/crates/bevy_state/src/state/states.rs @@ -65,7 +65,7 @@ pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug /// `ComputedState` dependencies. const DEPENDENCY_DEPTH: usize = 1; - /// Should [`StateScoped`] be enabled for this state? If set to `true`, the [`StateScoped`] - /// component will be used to remove entities when changing state. + /// Should [`StateScoped`](crate::state_scoped::StateScoped) be enabled for this state? If set to `true`, + /// the `StateScoped` component will be used to remove entities when changing state. const SCOPED_ENTITIES_ENABLED: bool = false; }