diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 081e3e7ac0..b929ca503d 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1034,6 +1034,17 @@ impl App { .try_register_required_components_with::(constructor) } + /// Registers a component type as "disabling", + /// using [default query filters](bevy_ecs::entity_disabling::DefaultQueryFilters) to exclude entities with the component from queries. + /// + /// # Warning + /// + /// As discussed in the [module docs](bevy_ecs::entity_disabling), this can have performance implications, + /// as well as create interoperability issues, and should be used with caution. + pub fn register_disabling_component(&mut self) { + self.world_mut().register_disabling_component::(); + } + /// Returns a reference to the main [`SubApp`]'s [`World`]. This is the same as calling /// [`app.main().world()`]. /// diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 0106286c52..bd3d02d030 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -103,7 +103,9 @@ portable-atomic = [ [dependencies] bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ + "smallvec", +], default-features = false, optional = true } bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, optional = true } bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ "alloc", diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 77326cea3f..d4b1ad266c 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -1,14 +1,55 @@ -//! Types for entity disabling. -//! //! Disabled entities do not show up in queries unless the query explicitly mentions them. //! -//! If for example we have `Disabled` as an entity disabling component, when you add `Disabled` -//! to an entity, the entity will only be visible to queries with a filter like -//! [`With`]`` or query data like [`Has`]``. +//! Entities which are disabled in this way are not removed from the [`World`], +//! and their relationships remain intact. +//! In many cases, you may want to disable entire trees of entities at once, +//! using [`EntityCommands::insert_recursive`](crate::prelude::EntityCommands::insert_recursive). //! -//! ### Note +//! While Bevy ships with a built-in [`Disabled`] component, you can also create your own +//! disabling components, which will operate in the same way but can have distinct semantics. //! -//! Currently only queries for which the cache is built after enabling a filter will have entities +//! ``` +//! use bevy_ecs::prelude::*; +//! +//! // Our custom disabling component! +//! #[derive(Component, Clone)] +//! struct Prefab; +//! +//! #[derive(Component)] +//! struct A; +//! +//! let mut world = World::new(); +//! world.register_disabling_component::(); +//! world.spawn((A, Prefab)); +//! world.spawn((A,)); +//! world.spawn((A,)); +//! +//! let mut normal_query = world.query::<&A>(); +//! assert_eq!(2, normal_query.iter(&world).count()); +//! +//! let mut prefab_query = world.query_filtered::<&A, With>(); +//! assert_eq!(1, prefab_query.iter(&world).count()); +//! +//! let mut maybe_prefab_query = world.query::<(&A, Has)>(); +//! assert_eq!(3, maybe_prefab_query.iter(&world).count()); +//! ``` +//! +//! ## Default query filters +//! +//! In Bevy, entity disabling is implemented through the construction of a global "default query filter". +//! Queries which do not explicitly mention the disabled component will not include entities with that component. +//! If an entity has multiple disabling components, it will only be included in queries that mention all of them. +//! +//! For example, `Query<&Position>` will not include entities with the [`Disabled`] component, +//! even if they have a `Position` component, +//! but `Query<&Position, With>` or `Query<(&Position, Has)>` will see them. +//! +//! Entities with disabling components are still present in the [`World`] and can be accessed directly, +//! using methods on [`World`] or [`Commands`](crate::prelude::Commands). +//! +//! ### Warnings +//! +//! Currently, only queries for which the cache is built after enabling a default query filter will have entities //! with those components filtered. As a result, they should generally only be modified before the //! app starts. //! @@ -16,6 +57,11 @@ //! the enire [`World`], especially when they cause queries to mix sparse and table components. //! See [`Query` performance] for more info. //! +//! Custom disabling components can cause significant interoperability issues within the ecosystem, +//! as users must be aware of each disabling component in use. +//! Libraries should think carefully about whether they need to use a new disabling component, +//! and clearly communicate their presence to their users to avoid the new for library compatibility flags. +//! //! [`With`]: crate::prelude::With //! [`Has`]: crate::prelude::Has //! [`World`]: crate::prelude::World @@ -24,44 +70,118 @@ use crate::{ component::{ComponentId, Components, StorageType}, query::FilteredAccess, + world::{FromWorld, World}, }; use bevy_ecs_macros::{Component, Resource}; +use smallvec::SmallVec; #[cfg(feature = "bevy_reflect")] use {crate::reflect::ReflectComponent, bevy_reflect::Reflect}; -/// A marker component for disabled entities. See [the module docs] for more info. +/// A marker component for disabled entities. +/// +/// Semantically, this component is used to mark entities that are temporarily disabled (typically for gameplay reasons), +/// but will likely be re-enabled at some point. +/// +/// Like all disabling components, this only disables the entity itself, +/// not its children or other entities that reference it. +/// To disable an entire tree of entities, use [`EntityCommands::insert_recursive`](crate::prelude::EntityCommands::insert_recursive). +/// +/// Every [`World`] has a default query filter that excludes entities with this component, +/// registered in the [`DefaultQueryFilters`] resource. +/// See [the module docs] for more info. /// /// [the module docs]: crate::entity_disabling -#[derive(Component)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +#[derive(Component, Clone, Debug)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component), + reflect(Debug) +)] +// This component is registered as a disabling component during World::bootstrap pub struct Disabled; -/// The default filters for all queries, these are used to globally exclude entities from queries. +/// Default query filters work by excluding entities with certain components from most queries. +/// +/// If a query does not explicitly mention a given disabling component, it will not include entities with that component. +/// To be more precise, this checks if the query's [`FilteredAccess`] contains the component, +/// and if it does not, adds a [`Without`](crate::prelude::Without) filter for that component to the query. +/// +/// This resource is initialized in the [`World`] whenever a new world is created, +/// with the [`Disabled`] component as a disabling component. +/// +/// Note that you can remove default query filters by overwriting the [`DefaultQueryFilters`] resource. +/// This can be useful as a last resort escape hatch, but is liable to break compatibility with other libraries. +/// /// See the [module docs](crate::entity_disabling) for more info. -#[derive(Resource, Default, Debug)] +/// +/// +/// # Warning +/// +/// Default query filters are a global setting that affects all queries in the [`World`], +/// and incur a small performance cost for each query. +/// +/// They can cause significant interoperability issues within the ecosystem, +/// as users must be aware of each disabling component in use. +/// +/// Think carefully about whether you need to use a new disabling component, +/// and clearly communicate their presence in any libraries you publish. +#[derive(Resource, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] pub struct DefaultQueryFilters { - disabled: Option, + // We only expect a few components per application to act as disabling components, so we use a SmallVec here + // to avoid heap allocation in most cases. + disabling: SmallVec<[ComponentId; 4]>, +} + +impl FromWorld for DefaultQueryFilters { + fn from_world(world: &mut World) -> Self { + let mut filters = DefaultQueryFilters::empty(); + let disabled_component_id = world.register_component::(); + filters.register_disabling_component(disabled_component_id); + filters + } } impl DefaultQueryFilters { - /// Set the [`ComponentId`] for the entity disabling marker - pub(crate) fn set_disabled(&mut self, component_id: ComponentId) -> Option<()> { - if self.disabled.is_some() { - return None; + /// Creates a new, completely empty [`DefaultQueryFilters`]. + /// + /// This is provided as an escape hatch; in most cases you should initialize this using [`FromWorld`], + /// which is automatically called when creating a new [`World`]. + #[must_use] + pub fn empty() -> Self { + DefaultQueryFilters { + disabling: SmallVec::new(), } - self.disabled = Some(component_id); - Some(()) } - /// Get an iterator over all currently enabled filter components - pub fn ids(&self) -> impl Iterator { - [self.disabled].into_iter().flatten() + /// Adds this [`ComponentId`] to the set of [`DefaultQueryFilters`], + /// causing entities with this component to be excluded from queries. + /// + /// This method is idempotent, and will not add the same component multiple times. + /// + /// # Warning + /// + /// This method should only be called before the app starts, as it will not affect queries + /// initialized before it is called. + /// + /// As discussed in the [module docs](crate::entity_disabling), this can have performance implications, + /// as well as create interoperability issues, and should be used with caution. + pub fn register_disabling_component(&mut self, component_id: ComponentId) { + if !self.disabling.contains(&component_id) { + self.disabling.push(component_id); + } } - pub(super) fn apply(&self, component_access: &mut FilteredAccess) { - for component_id in self.ids() { + /// Get an iterator over all of the components which disable entities when present. + pub fn disabling_ids(&self) -> impl Iterator + use<'_> { + self.disabling.iter().copied() + } + + /// Modifies the provided [`FilteredAccess`] to include the filters from this [`DefaultQueryFilters`]. + pub(super) fn modify_access(&self, component_access: &mut FilteredAccess) { + for component_id in self.disabling_ids() { if !component_access.contains(component_id) { component_access.and_without(component_id); } @@ -69,7 +189,7 @@ impl DefaultQueryFilters { } pub(super) fn is_dense(&self, components: &Components) -> bool { - self.ids().all(|component_id| { + self.disabling_ids().all(|component_id| { components .get_info(component_id) .is_some_and(|info| info.storage_type() == StorageType::Table) @@ -81,24 +201,16 @@ impl DefaultQueryFilters { mod tests { use super::*; + use crate::{ + prelude::World, + query::{Has, With}, + }; use alloc::{vec, vec::Vec}; #[test] - fn test_set_filters() { - let mut filters = DefaultQueryFilters::default(); - assert_eq!(0, filters.ids().count()); - - assert!(filters.set_disabled(ComponentId::new(1)).is_some()); - assert!(filters.set_disabled(ComponentId::new(3)).is_none()); - - assert_eq!(1, filters.ids().count()); - assert_eq!(Some(ComponentId::new(1)), filters.ids().next()); - } - - #[test] - fn test_apply_filters() { - let mut filters = DefaultQueryFilters::default(); - filters.set_disabled(ComponentId::new(1)); + fn filters_modify_access() { + let mut filters = DefaultQueryFilters::empty(); + filters.register_disabling_component(ComponentId::new(1)); // A component access with an unrelated component let mut component_access = FilteredAccess::::default(); @@ -107,7 +219,7 @@ mod tests { .add_component_read(ComponentId::new(2)); let mut applied_access = component_access.clone(); - filters.apply(&mut applied_access); + filters.modify_access(&mut applied_access); assert_eq!(0, applied_access.with_filters().count()); assert_eq!( vec![ComponentId::new(1)], @@ -118,7 +230,7 @@ mod tests { component_access.and_with(ComponentId::new(4)); let mut applied_access = component_access.clone(); - filters.apply(&mut applied_access); + filters.modify_access(&mut applied_access); assert_eq!( vec![ComponentId::new(4)], applied_access.with_filters().collect::>() @@ -133,7 +245,7 @@ mod tests { component_access.and_with(ComponentId::new(1)); let mut applied_access = component_access.clone(); - filters.apply(&mut applied_access); + filters.modify_access(&mut applied_access); assert_eq!( vec![ComponentId::new(1), ComponentId::new(4)], applied_access.with_filters().collect::>() @@ -147,11 +259,46 @@ mod tests { .add_archetypal(ComponentId::new(1)); let mut applied_access = component_access.clone(); - filters.apply(&mut applied_access); + filters.modify_access(&mut applied_access); assert_eq!( vec![ComponentId::new(4)], applied_access.with_filters().collect::>() ); assert_eq!(0, applied_access.without_filters().count()); } + + #[derive(Component)] + struct CustomDisabled; + + #[test] + fn multiple_disabling_components() { + let mut world = World::new(); + world.register_disabling_component::(); + + world.spawn_empty(); + world.spawn(Disabled); + world.spawn(CustomDisabled); + world.spawn((Disabled, CustomDisabled)); + + let mut query = world.query::<()>(); + assert_eq!(1, query.iter(&world).count()); + + let mut query = world.query_filtered::<(), With>(); + assert_eq!(1, query.iter(&world).count()); + + let mut query = world.query::>(); + assert_eq!(2, query.iter(&world).count()); + + let mut query = world.query_filtered::<(), With>(); + assert_eq!(1, query.iter(&world).count()); + + let mut query = world.query::>(); + assert_eq!(2, query.iter(&world).count()); + + let mut query = world.query_filtered::<(), (With, With)>(); + assert_eq!(1, query.iter(&world).count()); + + let mut query = world.query::<(Has, Has)>(); + assert_eq!(4, query.iter(&world).count()); + } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 0019fe3783..cd495251e6 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -258,7 +258,7 @@ impl QueryState { let mut is_dense = D::IS_DENSE && F::IS_DENSE; if let Some(default_filters) = world.get_resource::() { - default_filters.apply(&mut component_access); + default_filters.modify_access(&mut component_access); is_dense &= default_filters.is_dense(world.components()); } @@ -293,7 +293,7 @@ impl QueryState { let mut is_dense = builder.is_dense(); if let Some(default_filters) = builder.world().get_resource::() { - default_filters.apply(&mut component_access); + default_filters.modify_access(&mut component_access); is_dense &= default_filters.is_dense(builder.world().components()); } @@ -2454,8 +2454,8 @@ mod tests { world.spawn((B(0), C(0))); world.spawn(C(0)); - let mut df = DefaultQueryFilters::default(); - df.set_disabled(world.register_component::()); + let mut df = DefaultQueryFilters::empty(); + df.register_disabling_component(world.register_component::()); world.insert_resource(df); // Without only matches the first entity @@ -2494,8 +2494,8 @@ mod tests { assert!(query.is_dense); assert_eq!(3, query.iter(&world).count()); - let mut df = DefaultQueryFilters::default(); - df.set_disabled(world.register_component::()); + let mut df = DefaultQueryFilters::empty(); + df.register_disabling_component(world.register_component::()); world.insert_resource(df); let mut query = QueryState::<()>::new(&mut world); @@ -2504,8 +2504,8 @@ mod tests { assert!(!query.is_dense); assert_eq!(1, query.iter(&world).count()); - let mut df = DefaultQueryFilters::default(); - df.set_disabled(world.register_component::()); + let mut df = DefaultQueryFilters::empty(); + df.register_disabling_component(world.register_component::
()); world.insert_resource(df); let mut query = QueryState::<()>::new(&mut world); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 2c5ea2ef17..dafc183e81 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -42,7 +42,7 @@ use crate::{ Components, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, - entity_disabling::{DefaultQueryFilters, Disabled}, + entity_disabling::DefaultQueryFilters, event::{Event, EventId, Events, SendBatchIds}, observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, @@ -156,10 +156,8 @@ impl World { let on_despawn = OnDespawn::register_component_id(self); assert_eq!(ON_DESPAWN, on_despawn); - let disabled = self.register_component::(); - let mut filters = DefaultQueryFilters::default(); - filters.set_disabled(disabled); - self.insert_resource(filters); + // This sets up `Disabled` as a disabling component, via the FromWorld impl + self.init_resource::(); } /// Creates a new empty [`World`]. /// @@ -250,6 +248,14 @@ impl World { self.components.register_component::() } + /// Registers a component type as "disabling", + /// using [default query filters](DefaultQueryFilters) to exclude entities with the component from queries. + pub fn register_disabling_component(&mut self) { + let component_id = self.register_component::(); + let mut dqf = self.resource_mut::(); + dqf.register_disabling_component(component_id); + } + /// Returns a mutable reference to the [`ComponentHooks`] for a [`Component`] type. /// /// Will panic if `T` exists in any archetypes. diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index e2981ed973..c33346373b 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -351,6 +351,7 @@ impl<'w> DynamicSceneBuilder<'w> { /// [`deny_resource`]: Self::deny_resource #[must_use] pub fn extract_resources(mut self) -> Self { + // Don't extract the DefaultQueryFilters resource let original_world_dqf_id = self .original_world .components()