Add DefaultQueryFilters (#13120)
# Objective Some usecases in the ecosystems are blocked by the inability to stop bevy internals and third party plugins from touching their entities. However the specifics of a general purpose entity disabling system are controversial and further complicated by hierarchies. We can partially unblock these usecases with an opt-in approach: default query filters. ## Solution - Introduce DefaultQueryFilters, these filters are automatically applied to queries that don't otherwise mention the filtered component. - End users and third party plugins can register default filters and are responsible for handling entities they have hidden this way. - Extra features can be left for after user feedback - The default value could later include official ways to hide entities --- ## Changelog - Add DefaultQueryFilters
This commit is contained in:
parent
a7051a4815
commit
de5486725d
152
crates/bevy_ecs/src/entity_disabling.rs
Normal file
152
crates/bevy_ecs/src/entity_disabling.rs
Normal file
@ -0,0 +1,152 @@
|
||||
//! 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)
|
||||
.is_some_and(|info| info.storage_type() == StorageType::Table)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
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));
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ pub mod bundle;
|
||||
pub mod change_detection;
|
||||
pub mod component;
|
||||
pub mod entity;
|
||||
pub mod entity_disabling;
|
||||
pub mod event;
|
||||
pub mod hierarchy;
|
||||
pub mod identifier;
|
||||
|
@ -1123,6 +1123,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)]
|
||||
|
@ -3,6 +3,7 @@ use crate::{
|
||||
batching::BatchingStrategy,
|
||||
component::{ComponentId, Tick},
|
||||
entity::{Entity, EntityBorrow, EntitySet},
|
||||
entity_disabling::DefaultQueryFilters,
|
||||
prelude::FromWorld,
|
||||
query::{
|
||||
Access, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter,
|
||||
@ -214,7 +215,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
|
||||
fn new_uninitialized(world: &mut World) -> Self {
|
||||
let fetch_state = D::init_state(world);
|
||||
let filter_state = F::init_state(world);
|
||||
Self::from_states_uninitialized(world.id(), fetch_state, filter_state)
|
||||
Self::from_states_uninitialized(world, fetch_state, filter_state)
|
||||
}
|
||||
|
||||
/// Creates a new [`QueryState`] but does not populate it with the matched results from the World yet
|
||||
@ -225,7 +226,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
|
||||
let fetch_state = D::get_state(world.components())?;
|
||||
let filter_state = F::get_state(world.components())?;
|
||||
Some(Self::from_states_uninitialized(
|
||||
world.id(),
|
||||
world,
|
||||
fetch_state,
|
||||
filter_state,
|
||||
))
|
||||
@ -236,7 +237,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
|
||||
/// `new_archetype` and its variants must be called on all of the World's archetypes before the
|
||||
/// state can return valid query results.
|
||||
fn from_states_uninitialized(
|
||||
world_id: WorldId,
|
||||
world: &World,
|
||||
fetch_state: <D as WorldQuery>::State,
|
||||
filter_state: <F as WorldQuery>::State,
|
||||
) -> Self {
|
||||
@ -255,10 +256,15 @@ 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: world.id(),
|
||||
archetype_generation: ArchetypeGeneration::initial(),
|
||||
matched_storage_ids: Vec::new(),
|
||||
is_dense,
|
||||
@ -282,15 +288,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")]
|
||||
@ -1880,7 +1895,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,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
@ -2314,4 +2330,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());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user