
# Objective Fixes #15367. Currently, required components can only be defined through the `require` macro attribute. While this should be used in most cases, there are also several instances where you may want to define requirements at runtime, commonly in plugins. Example use cases: - Require components only if the relevant optional plugins are enabled. For example, a `SleepTimer` component (for physics) is only relevant if the `SleepPlugin` is enabled. - Third party crates can define their own requirements for first party types. For example, "each `Handle<Mesh>` should require my custom rendering data components". This also gets around the orphan rule. - Generic plugins that add marker components based on the existence of other components, like a generic `ColliderPlugin<C: AnyCollider>` that wants to add a `ColliderMarker` component for all types of colliders. - This is currently relevant for the retained render world in #15320. The `ExtractComponentPlugin<C>` should add `SyncToRenderWorld` to all components that should be extracted. This is currently done with observers, which is more expensive than required components, and causes archetype moves. - Replace some built-in components with custom versions. For example, if `GlobalTransform` required `Transform` through `TransformPlugin`, but we wanted to use a `CustomTransform` type, we could replace `TransformPlugin` with our own plugin. (This specific example isn't good, but there are likely better use cases where this may be useful) See #15367 for more in-depth reasoning. ## Solution Add `register_required_components::<T, R>` and `register_required_components_with::<T, R>` methods for `Default` and custom constructors respectively. These methods exist on `App` and `World`. ```rust struct BirdPlugin; impl Plugin for BirdPlugin { fn plugin(app: &mut App) { // Make `Bird` require `Wings` with a `Default` constructor. app.register_required_components::<Bird, Wings>(); // Make `Wings` require `FlapSpeed` with a custom constructor. // Fun fact: Some hummingbirds can flutter their wings 80 times per second! app.register_required_components_with::<Wings, FlapSpeed>(|| FlapSpeed::from_duration(1.0 / 80.0)); } } ``` The custom constructor is a function pointer to match the `require` API, though it could take a raw value too. Requirement inheritance works similarly as with the `require` attribute. If `Bird` required `FlapSpeed` directly, it would take precedence over indirectly requiring it through `Wings`. The same logic applies to all levels of the inheritance tree. Note that registering the same component requirement more than once will panic, similarly to trying to add multiple component hooks of the same type to the same component. This avoids constructor conflicts and confusing ordering issues. ### Implementation Runtime requirements have two additional challenges in comparison to the `require` attribute. 1. The `require` attribute uses recursion and macros with clever ordering to populate hash maps of required components for each component type. The expected semantics are that "more specific" requirements override ones deeper in the inheritance tree. However, at runtime, there is no representation of how "specific" each requirement is. 2. If you first register the requirement `X -> Y`, and later register `Y -> Z`, then `X` should also indirectly require `Z`. However, `Y` itself doesn't know that it is required by `X`, so it's not aware that it should update the list of required components for `X`. My solutions to these problems are: 1. Store the depth in the inheritance tree for each entry of a given component's `RequiredComponents`. This is used to determine how "specific" each requirement is. For `require`-based registration, these depths are computed as part of the recursion. 2. Store and maintain a `required_by` list in each component's `ComponentInfo`, next to `required_components`. For `require`-based registration, these are also added after each registration, as part of the recursion. When calling `register_required_components`, it works as follows: 1. Get the required components of `Foo`, and check that `Bar` isn't already a *direct* requirement. 3. Register `Bar` as a required component for `Foo`, and add `Foo` to the `required_by` list for `Bar`. 4. Find and register all indirect requirements inherited from `Bar`, adding `Foo` to the `required_by` list for each component. 5. Iterate through components that require `Foo`, registering the new inherited requires for them as indirect requirements. The runtime registration is likely slightly more expensive than the `require` version, but it is a one-time cost, and quite negligible in practice, unless projects have hundreds or thousands of runtime requirements. I have not benchmarked this however. This does also add a small amount of extra cost to the `require` attribute for updating `required_by` lists, but I expect it to be very minor. ## Testing I added some tests that are copies of the `require` versions, as well as some tests that are more specific to the runtime implementation. I might add a few more tests though. ## Discussion - Is `register_required_components` a good name? Originally I went for `register_component_requirement` to be consistent with `register_component_hooks`, but the general feature is often referred to as "required components", which is why I changed it to `register_required_components`. - Should we *not* panic for duplicate requirements? If so, should they just be ignored, or should the latest registration overwrite earlier ones? - If we do want to panic for duplicate, conflicting registrations, should we at least not panic if the registrations are *exactly* the same, i.e. same component and same constructor? The current implementation panics for all duplicate direct registrations regardless of the constructor. ## Next Steps - Allow `register_required_components` to take a `Bundle` instead of a single required component. - I could also try to do it in this PR if that would be preferable. - Not directly related, but archetype invariants?
283 lines
10 KiB
Rust
283 lines
10 KiB
Rust
use proc_macro::TokenStream;
|
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
|
use quote::{quote, ToTokens};
|
|
use std::collections::HashSet;
|
|
use syn::{
|
|
parenthesized,
|
|
parse::Parse,
|
|
parse_macro_input, parse_quote,
|
|
punctuated::Punctuated,
|
|
spanned::Spanned,
|
|
token::{Comma, Paren},
|
|
DeriveInput, ExprPath, Ident, LitStr, Path, Result,
|
|
};
|
|
|
|
pub fn derive_event(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
TokenStream::from(quote! {
|
|
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
|
|
type Traversal = ();
|
|
const AUTO_PROPAGATE: bool = false;
|
|
}
|
|
|
|
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
|
|
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn derive_resource(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
TokenStream::from(quote! {
|
|
impl #impl_generics #bevy_ecs_path::system::Resource for #struct_name #type_generics #where_clause {
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn derive_component(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
let attrs = match parse_component_attr(&ast) {
|
|
Ok(attrs) => attrs,
|
|
Err(e) => return e.into_compile_error().into(),
|
|
};
|
|
|
|
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_replace = hook_register_function_call(quote! {on_replace}, attrs.on_replace);
|
|
let on_remove = hook_register_function_call(quote! {on_remove}, attrs.on_remove);
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let requires = &attrs.requires;
|
|
let mut register_required = Vec::with_capacity(attrs.requires.iter().len());
|
|
let mut register_recursive_requires = Vec::with_capacity(attrs.requires.iter().len());
|
|
if let Some(requires) = requires {
|
|
for require in requires {
|
|
let ident = &require.path;
|
|
register_recursive_requires.push(quote! {
|
|
<#ident as Component>::register_required_components(
|
|
requiree,
|
|
components,
|
|
storages,
|
|
required_components,
|
|
inheritance_depth + 1
|
|
);
|
|
});
|
|
if let Some(func) = &require.func {
|
|
register_required.push(quote! {
|
|
components.register_required_components_manual::<Self, #ident>(
|
|
storages,
|
|
required_components,
|
|
|| { let x: #ident = #func().into(); x },
|
|
inheritance_depth
|
|
);
|
|
});
|
|
} else {
|
|
register_required.push(quote! {
|
|
components.register_required_components_manual::<Self, #ident>(
|
|
storages,
|
|
required_components,
|
|
<#ident as Default>::default,
|
|
inheritance_depth
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
let required_component_docs = attrs.requires.map(|r| {
|
|
let paths = r
|
|
.iter()
|
|
.map(|r| format!("[`{}`]", r.path.to_token_stream()))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let doc = format!("Required Components: {paths}. \n\n A component's Required Components are inserted whenever it is inserted. Note that this will also insert the required components _of_ the required components, recursively, in depth-first order.");
|
|
quote! {
|
|
#[doc = #doc]
|
|
}
|
|
});
|
|
|
|
// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
|
|
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
|
|
TokenStream::from(quote! {
|
|
#required_component_docs
|
|
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
|
|
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
|
|
fn register_required_components(
|
|
requiree: #bevy_ecs_path::component::ComponentId,
|
|
components: &mut #bevy_ecs_path::component::Components,
|
|
storages: &mut #bevy_ecs_path::storage::Storages,
|
|
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
|
|
inheritance_depth: u16,
|
|
) {
|
|
#(#register_required)*
|
|
#(#register_recursive_requires)*
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn register_component_hooks(hooks: &mut #bevy_ecs_path::component::ComponentHooks) {
|
|
#on_add
|
|
#on_insert
|
|
#on_replace
|
|
#on_remove
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub const COMPONENT: &str = "component";
|
|
pub const STORAGE: &str = "storage";
|
|
pub const REQUIRE: &str = "require";
|
|
|
|
pub const ON_ADD: &str = "on_add";
|
|
pub const ON_INSERT: &str = "on_insert";
|
|
pub const ON_REPLACE: &str = "on_replace";
|
|
pub const ON_REMOVE: &str = "on_remove";
|
|
|
|
struct Attrs {
|
|
storage: StorageTy,
|
|
requires: Option<Punctuated<Require, Comma>>,
|
|
on_add: Option<ExprPath>,
|
|
on_insert: Option<ExprPath>,
|
|
on_replace: Option<ExprPath>,
|
|
on_remove: Option<ExprPath>,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum StorageTy {
|
|
Table,
|
|
SparseSet,
|
|
}
|
|
|
|
struct Require {
|
|
path: Path,
|
|
func: Option<Path>,
|
|
}
|
|
|
|
// values for `storage` attribute
|
|
const TABLE: &str = "Table";
|
|
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_replace: None,
|
|
on_remove: None,
|
|
requires: None,
|
|
};
|
|
|
|
let mut require_paths = HashSet::new();
|
|
for attr in ast.attrs.iter() {
|
|
if attr.path().is_ident(COMPONENT) {
|
|
attr.parse_nested_meta(|nested| {
|
|
if nested.path.is_ident(STORAGE) {
|
|
attrs.storage = match nested.value()?.parse::<LitStr>()?.value() {
|
|
s if s == TABLE => StorageTy::Table,
|
|
s if s == SPARSE_SET => StorageTy::SparseSet,
|
|
s => {
|
|
return Err(nested.error(format!(
|
|
"Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'.",
|
|
)));
|
|
}
|
|
};
|
|
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_REPLACE) {
|
|
attrs.on_replace = 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"))
|
|
}
|
|
})?;
|
|
} else if attr.path().is_ident(REQUIRE) {
|
|
let punctuated =
|
|
attr.parse_args_with(Punctuated::<Require, Comma>::parse_terminated)?;
|
|
for require in punctuated.iter() {
|
|
if !require_paths.insert(require.path.to_token_stream().to_string()) {
|
|
return Err(syn::Error::new(
|
|
require.path.span(),
|
|
"Duplicate required components are not allowed.",
|
|
));
|
|
}
|
|
}
|
|
if let Some(current) = &mut attrs.requires {
|
|
current.extend(punctuated);
|
|
} else {
|
|
attrs.requires = Some(punctuated);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(attrs)
|
|
}
|
|
|
|
impl Parse for Require {
|
|
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
|
let path = input.parse::<Path>()?;
|
|
let func = if input.peek(Paren) {
|
|
let content;
|
|
parenthesized!(content in input);
|
|
let func = content.parse::<Path>()?;
|
|
Some(func)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(Require { path, func })
|
|
}
|
|
}
|
|
|
|
fn storage_path(bevy_ecs_path: &Path, ty: StorageTy) -> TokenStream2 {
|
|
let storage_type = match ty {
|
|
StorageTy::Table => Ident::new("Table", Span::call_site()),
|
|
StorageTy::SparseSet => Ident::new("SparseSet", Span::call_site()),
|
|
};
|
|
|
|
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); })
|
|
}
|