Component Hook functions as attributes for Component derive macro (#14005)

# Objective

Fixes https://github.com/bevyengine/bevy/issues/13972

## Solution

Added 3 new attributes to the `Component` macro.

## Testing

Added `component_hook_order_spawn_despawn_with_macro_hooks`, that makes
the same as `component_hook_order_spawn_despawn` but uses a struct, that
defines it's hooks with the `Component` macro.

---

---------

Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
This commit is contained in:
Jenya705 2024-07-08 02:46:00 +02:00 committed by GitHub
parent f009d37fe5
commit 330911f1bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 2 deletions

View file

@ -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<ExprPath>,
on_insert: Option<ExprPath>,
on_remove: Option<ExprPath>,
}
#[derive(Clone, Copy)]
@ -89,6 +106,9 @@ const SPARSE_SET: &str = "SparseSet";
fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
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<Attrs> {
}
};
Ok(())
} else if nested.path.is_ident(ON_ADD) {
attrs.on_add = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())
} else if nested.path.is_ident(ON_INSERT) {
attrs.on_insert = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())
} else if nested.path.is_ident(ON_REMOVE) {
attrs.on_remove = Some(nested.value()?.parse::<ExprPath>()?);
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<ExprPath>,
) -> Option<TokenStream2> {
function.map(|meta| quote! { hooks. #hook (#meta); })
}

View file

@ -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::<R>().assert_order(0);
}
fn a_on_insert<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
world.resource_mut::<R>().assert_order(1);
}
fn a_on_remove<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
world.resource_mut::<R>().assert_order(2);
}
#[derive(Component)]
struct B;
@ -1166,6 +1184,17 @@ mod tests {
assert_eq!(3, world.resource::<R>().0);
}
#[test]
fn component_hook_order_spawn_despawn_with_macro_hooks() {
let mut world = World::new();
world.init_resource::<R>();
let entity = world.spawn(AMacroHooks).id();
world.despawn(entity);
assert_eq!(3, world.resource::<R>().0);
}
#[test]
fn component_hook_order_insert_remove() {
let mut world = World::new();

View file

@ -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<T1, T2>(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::*;

View file

@ -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 {