Skip to content

Commit de54867

Browse files
authored
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
1 parent a7051a4 commit de54867

File tree

4 files changed

+260
-9
lines changed

4 files changed

+260
-9
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//! Types for entity disabling.
2+
//!
3+
//! Disabled entities do not show up in queries unless the query explicitly mentions them.
4+
//!
5+
//! If for example we have `Disabled` as an entity disabling component, when you add `Disabled`
6+
//! to an entity, the entity will only be visible to queries with a filter like
7+
//! [`With`]`<Disabled>` or query data like [`Has`]`<Disabled>`.
8+
//!
9+
//! ### Note
10+
//!
11+
//! Currently only queries for which the cache is built after enabling a filter will have entities
12+
//! with those components filtered. As a result, they should generally only be modified before the
13+
//! app starts.
14+
//!
15+
//! Because filters are applied to all queries they can have performance implication for
16+
//! the enire [`World`], especially when they cause queries to mix sparse and table components.
17+
//! See [`Query` performance] for more info.
18+
//!
19+
//! [`With`]: crate::prelude::With
20+
//! [`Has`]: crate::prelude::Has
21+
//! [`World`]: crate::prelude::World
22+
//! [`Query` performance]: crate::prelude::Query#performance
23+
24+
use crate as bevy_ecs;
25+
use crate::{
26+
component::{ComponentId, Components, StorageType},
27+
query::FilteredAccess,
28+
};
29+
use bevy_ecs_macros::Resource;
30+
31+
/// The default filters for all queries, these are used to globally exclude entities from queries.
32+
/// See the [module docs](crate::entity_disabling) for more info.
33+
#[derive(Resource, Default, Debug)]
34+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
35+
pub struct DefaultQueryFilters {
36+
disabled: Option<ComponentId>,
37+
}
38+
39+
impl DefaultQueryFilters {
40+
#[cfg_attr(
41+
not(test),
42+
expect(dead_code, reason = "No Disabled component exist yet")
43+
)]
44+
/// Set the [`ComponentId`] for the entity disabling marker
45+
pub(crate) fn set_disabled(&mut self, component_id: ComponentId) -> Option<()> {
46+
if self.disabled.is_some() {
47+
return None;
48+
}
49+
self.disabled = Some(component_id);
50+
Some(())
51+
}
52+
53+
/// Get an iterator over all currently enabled filter components
54+
pub fn ids(&self) -> impl Iterator<Item = ComponentId> {
55+
[self.disabled].into_iter().flatten()
56+
}
57+
58+
pub(super) fn apply(&self, component_access: &mut FilteredAccess<ComponentId>) {
59+
for component_id in self.ids() {
60+
if !component_access.contains(component_id) {
61+
component_access.and_without(component_id);
62+
}
63+
}
64+
}
65+
66+
pub(super) fn is_dense(&self, components: &Components) -> bool {
67+
self.ids().all(|component_id| {
68+
components
69+
.get_info(component_id)
70+
.is_some_and(|info| info.storage_type() == StorageType::Table)
71+
})
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
78+
use super::*;
79+
use alloc::{vec, vec::Vec};
80+
81+
#[test]
82+
fn test_set_filters() {
83+
let mut filters = DefaultQueryFilters::default();
84+
assert_eq!(0, filters.ids().count());
85+
86+
assert!(filters.set_disabled(ComponentId::new(1)).is_some());
87+
assert!(filters.set_disabled(ComponentId::new(3)).is_none());
88+
89+
assert_eq!(1, filters.ids().count());
90+
assert_eq!(Some(ComponentId::new(1)), filters.ids().next());
91+
}
92+
93+
#[test]
94+
fn test_apply_filters() {
95+
let mut filters = DefaultQueryFilters::default();
96+
filters.set_disabled(ComponentId::new(1));
97+
98+
// A component access with an unrelated component
99+
let mut component_access = FilteredAccess::<ComponentId>::default();
100+
component_access
101+
.access_mut()
102+
.add_component_read(ComponentId::new(2));
103+
104+
let mut applied_access = component_access.clone();
105+
filters.apply(&mut applied_access);
106+
assert_eq!(0, applied_access.with_filters().count());
107+
assert_eq!(
108+
vec![ComponentId::new(1)],
109+
applied_access.without_filters().collect::<Vec<_>>()
110+
);
111+
112+
// We add a with filter, now we expect to see both filters
113+
component_access.and_with(ComponentId::new(4));
114+
115+
let mut applied_access = component_access.clone();
116+
filters.apply(&mut applied_access);
117+
assert_eq!(
118+
vec![ComponentId::new(4)],
119+
applied_access.with_filters().collect::<Vec<_>>()
120+
);
121+
assert_eq!(
122+
vec![ComponentId::new(1)],
123+
applied_access.without_filters().collect::<Vec<_>>()
124+
);
125+
126+
let copy = component_access.clone();
127+
// We add a rule targeting a default component, that filter should no longer be added
128+
component_access.and_with(ComponentId::new(1));
129+
130+
let mut applied_access = component_access.clone();
131+
filters.apply(&mut applied_access);
132+
assert_eq!(
133+
vec![ComponentId::new(1), ComponentId::new(4)],
134+
applied_access.with_filters().collect::<Vec<_>>()
135+
);
136+
assert_eq!(0, applied_access.without_filters().count());
137+
138+
// Archetypal access should also filter rules
139+
component_access = copy.clone();
140+
component_access
141+
.access_mut()
142+
.add_archetypal(ComponentId::new(1));
143+
144+
let mut applied_access = component_access.clone();
145+
filters.apply(&mut applied_access);
146+
assert_eq!(
147+
vec![ComponentId::new(4)],
148+
applied_access.with_filters().collect::<Vec<_>>()
149+
);
150+
assert_eq!(0, applied_access.without_filters().count());
151+
}
152+
}

crates/bevy_ecs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub mod bundle;
3939
pub mod change_detection;
4040
pub mod component;
4141
pub mod entity;
42+
pub mod entity_disabling;
4243
pub mod event;
4344
pub mod hierarchy;
4445
pub mod identifier;

crates/bevy_ecs/src/query/access.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,16 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
11231123
.iter()
11241124
.flat_map(|f| f.without.ones().map(T::get_sparse_set_index))
11251125
}
1126+
1127+
/// Returns true if the index is used by this `FilteredAccess` in any way
1128+
pub fn contains(&self, index: T) -> bool {
1129+
self.access().has_component_read(index.clone())
1130+
|| self.access().has_archetypal(index.clone())
1131+
|| self.filter_sets.iter().any(|f| {
1132+
f.with.contains(index.sparse_set_index())
1133+
|| f.without.contains(index.sparse_set_index())
1134+
})
1135+
}
11261136
}
11271137

11281138
#[derive(Eq, PartialEq)]

crates/bevy_ecs/src/query/state.rs

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{
33
batching::BatchingStrategy,
44
component::{ComponentId, Tick},
55
entity::{Entity, EntityBorrow, EntitySet},
6+
entity_disabling::DefaultQueryFilters,
67
prelude::FromWorld,
78
query::{
89
Access, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter,
@@ -214,7 +215,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
214215
fn new_uninitialized(world: &mut World) -> Self {
215216
let fetch_state = D::init_state(world);
216217
let filter_state = F::init_state(world);
217-
Self::from_states_uninitialized(world.id(), fetch_state, filter_state)
218+
Self::from_states_uninitialized(world, fetch_state, filter_state)
218219
}
219220

220221
/// 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> {
225226
let fetch_state = D::get_state(world.components())?;
226227
let filter_state = F::get_state(world.components())?;
227228
Some(Self::from_states_uninitialized(
228-
world.id(),
229+
world,
229230
fetch_state,
230231
filter_state,
231232
))
@@ -236,7 +237,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
236237
/// `new_archetype` and its variants must be called on all of the World's archetypes before the
237238
/// state can return valid query results.
238239
fn from_states_uninitialized(
239-
world_id: WorldId,
240+
world: &World,
240241
fetch_state: <D as WorldQuery>::State,
241242
filter_state: <F as WorldQuery>::State,
242243
) -> Self {
@@ -255,10 +256,15 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
255256

256257
// For queries without dynamic filters the dense-ness of the query is equal to the dense-ness
257258
// of its static type parameters.
258-
let is_dense = D::IS_DENSE && F::IS_DENSE;
259+
let mut is_dense = D::IS_DENSE && F::IS_DENSE;
260+
261+
if let Some(default_filters) = world.get_resource::<DefaultQueryFilters>() {
262+
default_filters.apply(&mut component_access);
263+
is_dense &= default_filters.is_dense(world.components());
264+
}
259265

260266
Self {
261-
world_id,
267+
world_id: world.id(),
262268
archetype_generation: ArchetypeGeneration::initial(),
263269
matched_storage_ids: Vec::new(),
264270
is_dense,
@@ -282,15 +288,24 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
282288
let filter_state = F::init_state(builder.world_mut());
283289
D::set_access(&mut fetch_state, builder.access());
284290

291+
let mut component_access = builder.access().clone();
292+
293+
// For dynamic queries the dense-ness is given by the query builder.
294+
let mut is_dense = builder.is_dense();
295+
296+
if let Some(default_filters) = builder.world().get_resource::<DefaultQueryFilters>() {
297+
default_filters.apply(&mut component_access);
298+
is_dense &= default_filters.is_dense(builder.world().components());
299+
}
300+
285301
let mut state = Self {
286302
world_id: builder.world().id(),
287303
archetype_generation: ArchetypeGeneration::initial(),
288304
matched_storage_ids: Vec::new(),
289-
// For dynamic queries the dense-ness is given by the query builder.
290-
is_dense: builder.is_dense(),
305+
is_dense,
291306
fetch_state,
292307
filter_state,
293-
component_access: builder.access().clone(),
308+
component_access,
294309
matched_tables: Default::default(),
295310
matched_archetypes: Default::default(),
296311
#[cfg(feature = "trace")]
@@ -1880,7 +1895,8 @@ impl<D: QueryData, F: QueryFilter> From<QueryBuilder<'_, D, F>> for QueryState<D
18801895
mod tests {
18811896
use crate as bevy_ecs;
18821897
use crate::{
1883-
component::Component, prelude::*, query::QueryEntityError, world::FilteredEntityRef,
1898+
component::Component, entity_disabling::DefaultQueryFilters, prelude::*,
1899+
query::QueryEntityError, world::FilteredEntityRef,
18841900
};
18851901
use alloc::vec::Vec;
18861902

@@ -2314,4 +2330,76 @@ mod tests {
23142330
let query_2 = QueryState::<&B, Without<C>>::new(&mut world);
23152331
let _: QueryState<Entity, Changed<C>> = query_1.join_filtered(&world, &query_2);
23162332
}
2333+
2334+
#[test]
2335+
fn query_respects_default_filters() {
2336+
let mut world = World::new();
2337+
world.spawn((A(0), B(0)));
2338+
world.spawn((B(0), C(0)));
2339+
world.spawn(C(0));
2340+
2341+
let mut df = DefaultQueryFilters::default();
2342+
df.set_disabled(world.register_component::<C>());
2343+
world.insert_resource(df);
2344+
2345+
// Without<C> only matches the first entity
2346+
let mut query = QueryState::<()>::new(&mut world);
2347+
assert_eq!(1, query.iter(&world).count());
2348+
2349+
// With<C> matches the last two entities
2350+
let mut query = QueryState::<(), With<C>>::new(&mut world);
2351+
assert_eq!(2, query.iter(&world).count());
2352+
2353+
// Has should bypass the filter entirely
2354+
let mut query = QueryState::<Has<C>>::new(&mut world);
2355+
assert_eq!(3, query.iter(&world).count());
2356+
2357+
// Other filters should still be respected
2358+
let mut query = QueryState::<Has<C>, Without<B>>::new(&mut world);
2359+
assert_eq!(1, query.iter(&world).count());
2360+
}
2361+
2362+
#[derive(Component)]
2363+
struct Table;
2364+
2365+
#[derive(Component)]
2366+
#[component(storage = "SparseSet")]
2367+
struct Sparse;
2368+
2369+
#[test]
2370+
fn query_default_filters_updates_is_dense() {
2371+
let mut world = World::new();
2372+
world.spawn((Table, Sparse));
2373+
world.spawn(Table);
2374+
world.spawn(Sparse);
2375+
2376+
let mut query = QueryState::<()>::new(&mut world);
2377+
// There are no sparse components involved thus the query is dense
2378+
assert!(query.is_dense);
2379+
assert_eq!(3, query.iter(&world).count());
2380+
2381+
let mut df = DefaultQueryFilters::default();
2382+
df.set_disabled(world.register_component::<Sparse>());
2383+
world.insert_resource(df);
2384+
2385+
let mut query = QueryState::<()>::new(&mut world);
2386+
// The query doesn't ask for sparse components, but the default filters adds
2387+
// a sparse components thus it is NOT dense
2388+
assert!(!query.is_dense);
2389+
assert_eq!(1, query.iter(&world).count());
2390+
2391+
let mut df = DefaultQueryFilters::default();
2392+
df.set_disabled(world.register_component::<Table>());
2393+
world.insert_resource(df);
2394+
2395+
let mut query = QueryState::<()>::new(&mut world);
2396+
// If the filter is instead a table components, the query can still be dense
2397+
assert!(query.is_dense);
2398+
assert_eq!(1, query.iter(&world).count());
2399+
2400+
let mut query = QueryState::<&Sparse>::new(&mut world);
2401+
// But only if the original query was dense
2402+
assert!(!query.is_dense);
2403+
assert_eq!(1, query.iter(&world).count());
2404+
}
23172405
}

0 commit comments

Comments
 (0)