diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index dbd28e4b28..fc4a7e84a8 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; -use syn::{parse_macro_input, parse_quote, DeriveInput, Ident, LitStr, Path, Result}; +use syn::{parse_macro_input, parse_quote, DeriveInput, ExprPath, Ident, LitStr, Path, Result}; pub fn derive_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); @@ -54,6 +54,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let storage = storage_path(&bevy_ecs_path, attrs.storage); + let on_add = hook_register_function_call(quote! {on_add}, attrs.on_add); + let on_insert = hook_register_function_call(quote! {on_insert}, attrs.on_insert); + let on_remove = hook_register_function_call(quote! {on_remove}, attrs.on_remove); + ast.generics .make_where_clause() .predicates @@ -65,15 +69,28 @@ pub fn derive_component(input: TokenStream) -> TokenStream { TokenStream::from(quote! { impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause { const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage; + + #[allow(unused_variables)] + fn register_component_hooks(hooks: &mut #bevy_ecs_path::component::ComponentHooks) { + #on_add + #on_insert + #on_remove + } } }) } pub const COMPONENT: &str = "component"; pub const STORAGE: &str = "storage"; +pub const ON_ADD: &str = "on_add"; +pub const ON_INSERT: &str = "on_insert"; +pub const ON_REMOVE: &str = "on_remove"; struct Attrs { storage: StorageTy, + on_add: Option, + on_insert: Option, + on_remove: Option, } #[derive(Clone, Copy)] @@ -89,6 +106,9 @@ const SPARSE_SET: &str = "SparseSet"; fn parse_component_attr(ast: &DeriveInput) -> Result { let mut attrs = Attrs { storage: StorageTy::Table, + on_add: None, + on_insert: None, + on_remove: None, }; for meta in ast.attrs.iter().filter(|a| a.path().is_ident(COMPONENT)) { @@ -104,6 +124,15 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { } }; Ok(()) + } else if nested.path.is_ident(ON_ADD) { + attrs.on_add = Some(nested.value()?.parse::()?); + Ok(()) + } else if nested.path.is_ident(ON_INSERT) { + attrs.on_insert = Some(nested.value()?.parse::()?); + Ok(()) + } else if nested.path.is_ident(ON_REMOVE) { + attrs.on_remove = Some(nested.value()?.parse::()?); + Ok(()) } else { Err(nested.error("Unsupported attribute")) } @@ -121,3 +150,10 @@ fn storage_path(bevy_ecs_path: &Path, ty: StorageTy) -> TokenStream2 { quote! { #bevy_ecs_path::component::StorageType::#storage_type } } + +fn hook_register_function_call( + hook: TokenStream2, + function: Option, +) -> Option { + function.map(|meta| quote! { hooks. #hook (#meta); }) +} diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 1909aab854..7b4b6cb85d 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1124,11 +1124,29 @@ fn initialize_dynamic_bundle( #[cfg(test)] mod tests { use crate as bevy_ecs; + use crate::component::ComponentId; use crate::prelude::*; + use crate::world::DeferredWorld; #[derive(Component)] struct A; + #[derive(Component)] + #[component(on_add = a_on_add, on_insert = a_on_insert, on_remove = a_on_remove)] + struct AMacroHooks; + + fn a_on_add(mut world: DeferredWorld, _: Entity, _: ComponentId) { + world.resource_mut::().assert_order(0); + } + + fn a_on_insert(mut world: DeferredWorld, _: T1, _: T2) { + world.resource_mut::().assert_order(1); + } + + fn a_on_remove(mut world: DeferredWorld, _: T1, _: T2) { + world.resource_mut::().assert_order(2); + } + #[derive(Component)] struct B; @@ -1166,6 +1184,17 @@ mod tests { assert_eq!(3, world.resource::().0); } + #[test] + fn component_hook_order_spawn_despawn_with_macro_hooks() { + let mut world = World::new(); + world.init_resource::(); + + let entity = world.spawn(AMacroHooks).id(); + world.despawn(entity); + + assert_eq!(3, world.resource::().0); + } + #[test] fn component_hook_order_insert_remove() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 1557b660a4..28a20315c9 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -93,6 +93,42 @@ use std::{ /// [`Table`]: crate::storage::Table /// [`SparseSet`]: crate::storage::SparseSet /// +/// # Adding component's hooks +/// +/// See [`ComponentHooks`] for a detailed explanation of component's hooks. +/// +/// Alternatively to the example shown in [`ComponentHooks`]' documentation, hooks can be configured using following attributes: +/// - `#[component(on_add = on_add_function)]` +/// - `#[component(on_insert = on_insert_function)]` +/// - `#[component(on_remove = on_remove_function)]` +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::world::DeferredWorld; +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::component::ComponentId; +/// # +/// #[derive(Component)] +/// #[component(on_add = my_on_add_hook)] +/// #[component(on_insert = my_on_insert_hook)] +/// // Another possible way of configuring hooks: +/// // #[component(on_add = my_on_add_hook, on_insert = my_on_insert_hook)] +/// // +/// // We don't have a remove hook, so we can leave it out: +/// // #[component(on_remove = my_on_remove_hook)] +/// struct ComponentA; +/// +/// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId) { +/// // ... +/// } +/// +/// // You can also omit writing some types using generics. +/// fn my_on_insert_hook(world: DeferredWorld, _: T1, _: T2) { +/// // ... +/// } +/// +/// ``` +/// /// # Implementing the trait for foreign types /// /// As a consequence of the [orphan rule], it is not possible to separate into two different crates the implementation of `Component` from the definition of a type. @@ -199,7 +235,11 @@ pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, Entity, ComponentId); /// /// This information is stored in the [`ComponentInfo`] of the associated component. /// -/// # Example +/// There is two ways of configuring hooks for a component: +/// 1. Defining the [`Component::register_component_hooks`] method (see [`Component`]) +/// 2. Using the [`World::register_component_hooks`] method +/// +/// # Example 2 /// /// ``` /// use bevy_ecs::prelude::*; diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs index 30876601a0..f28c72f16f 100644 --- a/examples/ecs/component_hooks.rs +++ b/examples/ecs/component_hooks.rs @@ -18,6 +18,12 @@ use bevy::prelude::*; use std::collections::HashMap; #[derive(Debug)] +/// Hooks can also be registered during component initialisation by +/// using [`Component`] derive macro: +/// ```no_run +/// #[derive(Component)] +/// #[component(on_add = ..., on_insert = ..., on_remove = ...)] +/// ``` struct MyComponent(KeyCode); impl Component for MyComponent {