diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index ed4391d7e0041b..2a52620ed099ef 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -12,8 +12,10 @@ use syn::{ parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, + spanned::Spanned, token::Comma, - DeriveInput, Field, GenericParam, Ident, Index, LitInt, Result, Token, TypeParam, + DeriveInput, Field, GenericParam, Ident, Index, LitInt, Meta, MetaList, NestedMeta, Result, + Token, TypeParam, }; struct AllTuples { @@ -80,7 +82,15 @@ pub fn all_tuples(input: TokenStream) -> TokenStream { }) } -#[proc_macro_derive(Bundle)] +enum BundleFieldKind { + Component, + Ignore, +} + +const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; +const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; + +#[proc_macro_derive(Bundle, attributes(bundle))] pub fn derive_bundle(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); @@ -90,6 +100,36 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { Err(e) => return e.into_compile_error().into(), }; + let mut field_kind = Vec::with_capacity(named_fields.len()); + + 'field_loop: for field in named_fields.iter() { + for attr in &field.attrs { + if attr.path.is_ident(BUNDLE_ATTRIBUTE_NAME) { + if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() { + if let Some(&NestedMeta::Meta(Meta::Path(ref path))) = nested.first() { + if path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) { + field_kind.push(BundleFieldKind::Ignore); + continue 'field_loop; + } + + return syn::Error::new( + path.span(), + format!( + "Invalid bundle attribute. Use `{BUNDLE_ATTRIBUTE_IGNORE_NAME}`" + ), + ) + .into_compile_error() + .into(); + } + + return syn::Error::new(attr.span(), format!("Invalid bundle attribute. Use `#[{BUNDLE_ATTRIBUTE_NAME}({BUNDLE_ATTRIBUTE_IGNORE_NAME})]`")).into_compile_error().into(); + } + } + } + + field_kind.push(BundleFieldKind::Component); + } + let field = named_fields .iter() .map(|field| field.ident.as_ref().unwrap()) @@ -102,16 +142,28 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let mut field_component_ids = Vec::new(); let mut field_get_components = Vec::new(); let mut field_from_components = Vec::new(); - for (field_type, field) in field_type.iter().zip(field.iter()) { - field_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids); - }); - field_get_components.push(quote! { - self.#field.get_components(&mut *func); - }); - field_from_components.push(quote! { - #field: <#field_type as #ecs_path::bundle::Bundle>::from_components(ctx, &mut *func), - }); + for ((field_type, field_kind), field) in + field_type.iter().zip(field_kind.iter()).zip(field.iter()) + { + match field_kind { + BundleFieldKind::Component => { + field_component_ids.push(quote! { + <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids); + }); + field_get_components.push(quote! { + self.#field.get_components(&mut *func); + }); + field_from_components.push(quote! { + #field: <#field_type as #ecs_path::bundle::Bundle>::from_components(ctx, &mut *func), + }); + } + + BundleFieldKind::Ignore => { + field_from_components.push(quote! { + #field: ::std::default::Default::default(), + }); + } + } } let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index d9124f2493b848..52bd4af31030e4 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -82,10 +82,12 @@ use std::{any::TypeId, collections::HashMap}; /// used instead. /// The derived `Bundle` implementation contains the items of its fields, which all must /// implement `Bundle`. -/// As explained above, this includes any [`Component`] type, and other derived bundles: +/// As explained above, this includes any [`Component`] type, and other derived bundles. /// +/// If you want to add `PhantomData` to your `Bundle` you have to mark it with `#[bundle(ignore)]`. /// ``` -/// # use bevy_ecs::{component::Component, bundle::Bundle}; +/// # use std::marker::PhantomData; +/// use bevy_ecs::{component::Component, bundle::Bundle}; /// /// #[derive(Component)] /// struct XPosition(i32); @@ -99,12 +101,20 @@ use std::{any::TypeId, collections::HashMap}; /// y: YPosition, /// } /// +/// // You have to implement `Default` for ignored field types in bundle structs. +/// #[derive(Default)] +/// struct Other(f32); +/// /// #[derive(Bundle)] -/// struct NamedPointBundle { +/// struct NamedPointBundle { /// // Or other bundles /// a: PositionBundle, /// // In addition to more components /// z: PointName, +/// +/// // when you need to use `PhantomData` you have to mark it as ignored +/// #[bundle(ignore)] +/// _phantom_data: PhantomData /// } /// /// #[derive(Component)] diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8dd9a7bf5e1c7c..3c34c1e836132d 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -238,6 +238,45 @@ mod tests { b: B(2), } ); + + #[derive(Default, Component, PartialEq, Debug)] + struct Ignored; + + #[derive(Bundle, PartialEq, Debug)] + struct BundleWithIgnored { + c: C, + #[bundle(ignore)] + ignored: Ignored, + } + + let mut ids = Vec::new(); + ::component_ids( + &mut world.components, + &mut world.storages, + &mut |id| { + ids.push(id); + }, + ); + + assert_eq!(ids, &[world.init_component::(),]); + + let e4 = world + .spawn(BundleWithIgnored { + c: C, + ignored: Ignored, + }) + .id(); + + assert_eq!(world.get::(e4).unwrap(), &C); + assert_eq!(world.get::(e4), None); + + assert_eq!( + world.entity_mut(e4).remove::().unwrap(), + BundleWithIgnored { + c: C, + ignored: Ignored, + } + ); } #[test]