diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs new file mode 100644 index 0000000000000..3ccafc7b7d3cc --- /dev/null +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -0,0 +1,150 @@ +//! 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`]``. +//! +//! ### Note +//! +//! Currently only queries for which the cache is built after enabling a filter will have entities +//! with those components filtered. As a result, they should generally only be modified before the +//! app starts. +//! +//! Because filters are applied to all queries they can have performance implication for +//! the enire [`World`], especially when they cause queries to mix sparse and table components. +//! See [`Query` performance] for more info. +//! +//! [`With`]: crate::prelude::With +//! [`Has`]: crate::prelude::Has +//! [`World`]: crate::prelude::World +//! [`Query` performance]: crate::prelude::Query#performance + +use crate as bevy_ecs; +use crate::{ + component::{ComponentId, Components, StorageType}, + query::FilteredAccess, +}; +use bevy_ecs_macros::Resource; + +/// The default filters for all queries, these are used to globally exclude entities from queries. +/// See the [module docs](crate::entity_disabling) for more info. +#[derive(Resource, Default, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct DefaultQueryFilters { + disabled: Option, +} + +impl DefaultQueryFilters { + #[cfg_attr( + not(test), + expect(dead_code, reason = "No Disabled component exist yet") + )] + /// 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; + } + 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() + } + + pub(super) fn apply(&self, component_access: &mut FilteredAccess) { + for component_id in self.ids() { + if !component_access.contains(component_id) { + component_access.and_without(component_id); + } + } + } + + pub(super) fn is_dense(&self, components: &Components) -> bool { + self.ids().all(|component_id| { + components + .get_info(component_id) + .map_or(false, |info| info.storage_type() == StorageType::Table) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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)); + + // A component access with an unrelated component + let mut component_access = FilteredAccess::::default(); + component_access + .access_mut() + .add_component_read(ComponentId::new(2)); + + let mut applied_access = component_access.clone(); + filters.apply(&mut applied_access); + assert_eq!(0, applied_access.with_filters().count()); + assert_eq!( + vec![ComponentId::new(1)], + applied_access.without_filters().collect::>() + ); + + // We add a with filter, now we expect to see both filters + component_access.and_with(ComponentId::new(4)); + + let mut applied_access = component_access.clone(); + filters.apply(&mut applied_access); + assert_eq!( + vec![ComponentId::new(4)], + applied_access.with_filters().collect::>() + ); + assert_eq!( + vec![ComponentId::new(1)], + applied_access.without_filters().collect::>() + ); + + let copy = component_access.clone(); + // We add a rule targeting a default component, that filter should no longer be added + component_access.and_with(ComponentId::new(1)); + + let mut applied_access = component_access.clone(); + filters.apply(&mut applied_access); + assert_eq!( + vec![ComponentId::new(1), ComponentId::new(4)], + applied_access.with_filters().collect::>() + ); + assert_eq!(0, applied_access.without_filters().count()); + + // Archetypal access should also filter rules + component_access = copy.clone(); + component_access + .access_mut() + .add_archetypal(ComponentId::new(1)); + + let mut applied_access = component_access.clone(); + filters.apply(&mut applied_access); + assert_eq!( + vec![ComponentId::new(4)], + applied_access.with_filters().collect::>() + ); + assert_eq!(0, applied_access.without_filters().count()); + } +} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 4dda0d4ea9e38..8d2fa75c3e95b 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -21,6 +21,7 @@ pub mod bundle; pub mod change_detection; pub mod component; pub mod entity; +pub mod entity_disabling; pub mod event; pub mod identifier; pub mod intern; diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index cf504c2606635..11c56eb201e8b 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1077,6 +1077,16 @@ impl FilteredAccess { .iter() .flat_map(|f| f.without.ones().map(T::get_sparse_set_index)) } + + /// Returns true if the index is used by this `FilteredAccess` in any way + pub fn contains(&self, index: T) -> bool { + self.access().has_component_read(index.clone()) + || self.access().has_archetypal(index.clone()) + || self.filter_sets.iter().any(|f| { + f.with.contains(index.sparse_set_index()) + || f.without.contains(index.sparse_set_index()) + }) + } } #[derive(Eq, PartialEq)] diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 69b67368fe4f3..9a8f3f0c11caa 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -3,6 +3,7 @@ use crate::{ batching::BatchingStrategy, component::{ComponentId, Tick}, entity::Entity, + entity_disabling::DefaultQueryFilters, prelude::FromWorld, query::{ Access, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, @@ -202,7 +203,12 @@ impl QueryState { // For queries without dynamic filters the dense-ness of the query is equal to the dense-ness // of its static type parameters. - let is_dense = D::IS_DENSE && F::IS_DENSE; + let mut is_dense = D::IS_DENSE && F::IS_DENSE; + + if let Some(default_filters) = world.get_resource::() { + default_filters.apply(&mut component_access); + is_dense &= default_filters.is_dense(world.components()); + } Self { world_id: world.id(), @@ -229,15 +235,24 @@ impl QueryState { let filter_state = F::init_state(builder.world_mut()); D::set_access(&mut fetch_state, builder.access()); + let mut component_access = builder.access().clone(); + + // For dynamic queries the dense-ness is given by the query builder. + let mut is_dense = builder.is_dense(); + + if let Some(default_filters) = builder.world().get_resource::() { + default_filters.apply(&mut component_access); + is_dense &= default_filters.is_dense(builder.world().components()); + } + let mut state = Self { world_id: builder.world().id(), archetype_generation: ArchetypeGeneration::initial(), matched_storage_ids: Vec::new(), - // For dynamic queries the dense-ness is given by the query builder. - is_dense: builder.is_dense(), + is_dense, fetch_state, filter_state, - component_access: builder.access().clone(), + component_access, matched_tables: Default::default(), matched_archetypes: Default::default(), #[cfg(feature = "trace")] @@ -1720,7 +1735,8 @@ impl From> for QueryState>::new(&mut world); let _: QueryState> = query_1.join_filtered(&world, &query_2); } + + #[test] + fn query_respects_default_filters() { + let mut world = World::new(); + world.spawn((A(0), B(0))); + world.spawn((B(0), C(0))); + world.spawn(C(0)); + + let mut df = DefaultQueryFilters::default(); + df.set_disabled(world.register_component::()); + world.insert_resource(df); + + // Without only matches the first entity + let mut query = QueryState::<()>::new(&mut world); + assert_eq!(1, query.iter(&world).count()); + + // With matches the last two entities + let mut query = QueryState::<(), With>::new(&mut world); + assert_eq!(2, query.iter(&world).count()); + + // Has should bypass the filter entirely + let mut query = QueryState::>::new(&mut world); + assert_eq!(3, query.iter(&world).count()); + + // Other filters should still be respected + let mut query = QueryState::, Without>::new(&mut world); + assert_eq!(1, query.iter(&world).count()); + } + + #[derive(Component)] + struct Table; + + #[derive(Component)] + #[component(storage = "SparseSet")] + struct Sparse; + + #[test] + fn query_default_filters_updates_is_dense() { + let mut world = World::new(); + world.spawn((Table, Sparse)); + world.spawn(Table); + world.spawn(Sparse); + + let mut query = QueryState::<()>::new(&mut world); + // There are no sparse components involved thus the query is dense + assert!(query.is_dense); + assert_eq!(3, query.iter(&world).count()); + + let mut df = DefaultQueryFilters::default(); + df.set_disabled(world.register_component::()); + world.insert_resource(df); + + let mut query = QueryState::<()>::new(&mut world); + // The query doesn't ask for sparse components, but the default filters adds + // a sparse components thus it is NOT dense + assert!(!query.is_dense); + assert_eq!(1, query.iter(&world).count()); + + let mut df = DefaultQueryFilters::default(); + df.set_disabled(world.register_component::()); + world.insert_resource(df); + + let mut query = QueryState::<()>::new(&mut world); + // If the filter is instead a table components, the query can still be dense + assert!(query.is_dense); + assert_eq!(1, query.iter(&world).count()); + + let mut query = QueryState::<&Sparse>::new(&mut world); + // But only if the original query was dense + assert!(!query.is_dense); + assert_eq!(1, query.iter(&world).count()); + } }