Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DefaultQueryFilters #13120

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions crates/bevy_ecs/src/entity_disabling.rs
Original file line number Diff line number Diff line change
@@ -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`]`<Disabled>` or query data like [`Has`]`<Disabled>`.
//!
//! ### 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<ComponentId>,
}

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<Item = ComponentId> {
[self.disabled].into_iter().flatten()
}

pub(super) fn apply(&self, component_access: &mut FilteredAccess<ComponentId>) {
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::<ComponentId>::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::<Vec<_>>()
);

// 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::<Vec<_>>()
);
assert_eq!(
vec![ComponentId::new(1)],
applied_access.without_filters().collect::<Vec<_>>()
);

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::<Vec<_>>()
);
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::<Vec<_>>()
);
assert_eq!(0, applied_access.without_filters().count());
}
}
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions crates/bevy_ecs/src/query/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,16 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
.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)]
Expand Down
98 changes: 93 additions & 5 deletions crates/bevy_ecs/src/query/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -202,7 +203,12 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {

// 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::<DefaultQueryFilters>() {
default_filters.apply(&mut component_access);
is_dense &= default_filters.is_dense(world.components());
}

Self {
world_id: world.id(),
Expand All @@ -229,15 +235,24 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
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::<DefaultQueryFilters>() {
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")]
Expand Down Expand Up @@ -1720,7 +1735,8 @@ impl<D: QueryData, F: QueryFilter> From<QueryBuilder<'_, D, F>> for QueryState<D
mod tests {
use crate as bevy_ecs;
use crate::{
component::Component, prelude::*, query::QueryEntityError, world::FilteredEntityRef,
component::Component, entity_disabling::DefaultQueryFilters, prelude::*,
query::QueryEntityError, world::FilteredEntityRef,
};

#[test]
Expand Down Expand Up @@ -2153,4 +2169,76 @@ mod tests {
let query_2 = QueryState::<&B, Without<C>>::new(&mut world);
let _: QueryState<Entity, Changed<C>> = 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::<C>());
world.insert_resource(df);

// Without<C> only matches the first entity
let mut query = QueryState::<()>::new(&mut world);
assert_eq!(1, query.iter(&world).count());

// With<C> matches the last two entities
let mut query = QueryState::<(), With<C>>::new(&mut world);
assert_eq!(2, query.iter(&world).count());

// Has should bypass the filter entirely
let mut query = QueryState::<Has<C>>::new(&mut world);
assert_eq!(3, query.iter(&world).count());

// Other filters should still be respected
let mut query = QueryState::<Has<C>, Without<B>>::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::<Sparse>());
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::<Table>());
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());
}
}