Skip to content

Commit fcc77fe

Browse files
Allow users to register their own disabling components / default query filters (#17768)
# Objective Currently, default query filters, as added in #13120 / #17514 are hardcoded to only use a single query filter. This is limiting, as multiple distinct disabling components can serve important distinct roles. I ran into this limitation when experimenting with a workflow for prefabs, which don't represent the same state as "an entity which is temporarily nonfunctional". ## Solution 1. Change `DefaultQueryFilters` to store a SmallVec of ComponentId, rather than an Option. 2. Expose methods on `DefaultQueryFilters`, `World` and `App` to actually configure this. 3. While we're here, improve the docs, write some tests, make use of FromWorld and make some method names more descriptive. ## Follow-up I'm not convinced that supporting sparse set disabling components is useful, given the hit to iteration performance and runtime checks incurred. That's disjoint from this PR though, so I'm not doing it here. The existing warnings are fine for now. ## Testing I've added both a doc test and an mid-level unit test to verify that this works!
1 parent 5e9da92 commit fcc77fe

File tree

6 files changed

+225
-58
lines changed

6 files changed

+225
-58
lines changed

crates/bevy_app/src/app.rs

+11
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,17 @@ impl App {
10341034
.try_register_required_components_with::<T, R>(constructor)
10351035
}
10361036

1037+
/// Registers a component type as "disabling",
1038+
/// using [default query filters](bevy_ecs::entity_disabling::DefaultQueryFilters) to exclude entities with the component from queries.
1039+
///
1040+
/// # Warning
1041+
///
1042+
/// As discussed in the [module docs](bevy_ecs::entity_disabling), this can have performance implications,
1043+
/// as well as create interoperability issues, and should be used with caution.
1044+
pub fn register_disabling_component<C: Component>(&mut self) {
1045+
self.world_mut().register_disabling_component::<C>();
1046+
}
1047+
10371048
/// Returns a reference to the main [`SubApp`]'s [`World`]. This is the same as calling
10381049
/// [`app.main().world()`].
10391050
///

crates/bevy_ecs/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ portable-atomic = [
103103

104104
[dependencies]
105105
bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" }
106-
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true }
106+
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
107+
"smallvec",
108+
], default-features = false, optional = true }
107109
bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, optional = true }
108110
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [
109111
"alloc",
+191-44
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,67 @@
1-
//! Types for entity disabling.
2-
//!
31
//! Disabled entities do not show up in queries unless the query explicitly mentions them.
42
//!
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>`.
3+
//! Entities which are disabled in this way are not removed from the [`World`],
4+
//! and their relationships remain intact.
5+
//! In many cases, you may want to disable entire trees of entities at once,
6+
//! using [`EntityCommands::insert_recursive`](crate::prelude::EntityCommands::insert_recursive).
7+
//!
8+
//! While Bevy ships with a built-in [`Disabled`] component, you can also create your own
9+
//! disabling components, which will operate in the same way but can have distinct semantics.
10+
//!
11+
//! ```
12+
//! use bevy_ecs::prelude::*;
13+
//!
14+
//! // Our custom disabling component!
15+
//! #[derive(Component, Clone)]
16+
//! struct Prefab;
17+
//!
18+
//! #[derive(Component)]
19+
//! struct A;
20+
//!
21+
//! let mut world = World::new();
22+
//! world.register_disabling_component::<Prefab>();
23+
//! world.spawn((A, Prefab));
24+
//! world.spawn((A,));
25+
//! world.spawn((A,));
26+
//!
27+
//! let mut normal_query = world.query::<&A>();
28+
//! assert_eq!(2, normal_query.iter(&world).count());
29+
//!
30+
//! let mut prefab_query = world.query_filtered::<&A, With<Prefab>>();
31+
//! assert_eq!(1, prefab_query.iter(&world).count());
32+
//!
33+
//! let mut maybe_prefab_query = world.query::<(&A, Has<Prefab>)>();
34+
//! assert_eq!(3, maybe_prefab_query.iter(&world).count());
35+
//! ```
36+
//!
37+
//! ## Default query filters
38+
//!
39+
//! In Bevy, entity disabling is implemented through the construction of a global "default query filter".
40+
//! Queries which do not explicitly mention the disabled component will not include entities with that component.
41+
//! If an entity has multiple disabling components, it will only be included in queries that mention all of them.
42+
//!
43+
//! For example, `Query<&Position>` will not include entities with the [`Disabled`] component,
44+
//! even if they have a `Position` component,
45+
//! but `Query<&Position, With<Disabled>>` or `Query<(&Position, Has<Disabled>)>` will see them.
46+
//!
47+
//! Entities with disabling components are still present in the [`World`] and can be accessed directly,
48+
//! using methods on [`World`] or [`Commands`](crate::prelude::Commands).
849
//!
9-
//! ### Note
50+
//! ### Warnings
1051
//!
11-
//! Currently only queries for which the cache is built after enabling a filter will have entities
52+
//! Currently, only queries for which the cache is built after enabling a default query filter will have entities
1253
//! with those components filtered. As a result, they should generally only be modified before the
1354
//! app starts.
1455
//!
1556
//! Because filters are applied to all queries they can have performance implication for
1657
//! the enire [`World`], especially when they cause queries to mix sparse and table components.
1758
//! See [`Query` performance] for more info.
1859
//!
60+
//! Custom disabling components can cause significant interoperability issues within the ecosystem,
61+
//! as users must be aware of each disabling component in use.
62+
//! Libraries should think carefully about whether they need to use a new disabling component,
63+
//! and clearly communicate their presence to their users to avoid the new for library compatibility flags.
64+
//!
1965
//! [`With`]: crate::prelude::With
2066
//! [`Has`]: crate::prelude::Has
2167
//! [`World`]: crate::prelude::World
@@ -24,52 +70,126 @@
2470
use crate::{
2571
component::{ComponentId, Components, StorageType},
2672
query::FilteredAccess,
73+
world::{FromWorld, World},
2774
};
2875
use bevy_ecs_macros::{Component, Resource};
76+
use smallvec::SmallVec;
2977

3078
#[cfg(feature = "bevy_reflect")]
3179
use {crate::reflect::ReflectComponent, bevy_reflect::Reflect};
3280

33-
/// A marker component for disabled entities. See [the module docs] for more info.
81+
/// A marker component for disabled entities.
82+
///
83+
/// Semantically, this component is used to mark entities that are temporarily disabled (typically for gameplay reasons),
84+
/// but will likely be re-enabled at some point.
85+
///
86+
/// Like all disabling components, this only disables the entity itself,
87+
/// not its children or other entities that reference it.
88+
/// To disable an entire tree of entities, use [`EntityCommands::insert_recursive`](crate::prelude::EntityCommands::insert_recursive).
89+
///
90+
/// Every [`World`] has a default query filter that excludes entities with this component,
91+
/// registered in the [`DefaultQueryFilters`] resource.
92+
/// See [the module docs] for more info.
3493
///
3594
/// [the module docs]: crate::entity_disabling
36-
#[derive(Component)]
37-
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
95+
#[derive(Component, Clone, Debug)]
96+
#[cfg_attr(
97+
feature = "bevy_reflect",
98+
derive(Reflect),
99+
reflect(Component),
100+
reflect(Debug)
101+
)]
102+
// This component is registered as a disabling component during World::bootstrap
38103
pub struct Disabled;
39104

40-
/// The default filters for all queries, these are used to globally exclude entities from queries.
105+
/// Default query filters work by excluding entities with certain components from most queries.
106+
///
107+
/// If a query does not explicitly mention a given disabling component, it will not include entities with that component.
108+
/// To be more precise, this checks if the query's [`FilteredAccess`] contains the component,
109+
/// and if it does not, adds a [`Without`](crate::prelude::Without) filter for that component to the query.
110+
///
111+
/// This resource is initialized in the [`World`] whenever a new world is created,
112+
/// with the [`Disabled`] component as a disabling component.
113+
///
114+
/// Note that you can remove default query filters by overwriting the [`DefaultQueryFilters`] resource.
115+
/// This can be useful as a last resort escape hatch, but is liable to break compatibility with other libraries.
116+
///
41117
/// See the [module docs](crate::entity_disabling) for more info.
42-
#[derive(Resource, Default, Debug)]
118+
///
119+
///
120+
/// # Warning
121+
///
122+
/// Default query filters are a global setting that affects all queries in the [`World`],
123+
/// and incur a small performance cost for each query.
124+
///
125+
/// They can cause significant interoperability issues within the ecosystem,
126+
/// as users must be aware of each disabling component in use.
127+
///
128+
/// Think carefully about whether you need to use a new disabling component,
129+
/// and clearly communicate their presence in any libraries you publish.
130+
#[derive(Resource, Debug)]
43131
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
44132
pub struct DefaultQueryFilters {
45-
disabled: Option<ComponentId>,
133+
// We only expect a few components per application to act as disabling components, so we use a SmallVec here
134+
// to avoid heap allocation in most cases.
135+
disabling: SmallVec<[ComponentId; 4]>,
136+
}
137+
138+
impl FromWorld for DefaultQueryFilters {
139+
fn from_world(world: &mut World) -> Self {
140+
let mut filters = DefaultQueryFilters::empty();
141+
let disabled_component_id = world.register_component::<Disabled>();
142+
filters.register_disabling_component(disabled_component_id);
143+
filters
144+
}
46145
}
47146

48147
impl DefaultQueryFilters {
49-
/// Set the [`ComponentId`] for the entity disabling marker
50-
pub(crate) fn set_disabled(&mut self, component_id: ComponentId) -> Option<()> {
51-
if self.disabled.is_some() {
52-
return None;
148+
/// Creates a new, completely empty [`DefaultQueryFilters`].
149+
///
150+
/// This is provided as an escape hatch; in most cases you should initialize this using [`FromWorld`],
151+
/// which is automatically called when creating a new [`World`].
152+
#[must_use]
153+
pub fn empty() -> Self {
154+
DefaultQueryFilters {
155+
disabling: SmallVec::new(),
156+
}
157+
}
158+
159+
/// Adds this [`ComponentId`] to the set of [`DefaultQueryFilters`],
160+
/// causing entities with this component to be excluded from queries.
161+
///
162+
/// This method is idempotent, and will not add the same component multiple times.
163+
///
164+
/// # Warning
165+
///
166+
/// This method should only be called before the app starts, as it will not affect queries
167+
/// initialized before it is called.
168+
///
169+
/// As discussed in the [module docs](crate::entity_disabling), this can have performance implications,
170+
/// as well as create interoperability issues, and should be used with caution.
171+
pub fn register_disabling_component(&mut self, component_id: ComponentId) {
172+
if !self.disabling.contains(&component_id) {
173+
self.disabling.push(component_id);
53174
}
54-
self.disabled = Some(component_id);
55-
Some(())
56175
}
57176

58-
/// Get an iterator over all currently enabled filter components
59-
pub fn ids(&self) -> impl Iterator<Item = ComponentId> {
60-
[self.disabled].into_iter().flatten()
177+
/// Get an iterator over all of the components which disable entities when present.
178+
pub fn disabling_ids(&self) -> impl Iterator<Item = ComponentId> + use<'_> {
179+
self.disabling.iter().copied()
61180
}
62181

63-
pub(super) fn apply(&self, component_access: &mut FilteredAccess<ComponentId>) {
64-
for component_id in self.ids() {
182+
/// Modifies the provided [`FilteredAccess`] to include the filters from this [`DefaultQueryFilters`].
183+
pub(super) fn modify_access(&self, component_access: &mut FilteredAccess<ComponentId>) {
184+
for component_id in self.disabling_ids() {
65185
if !component_access.contains(component_id) {
66186
component_access.and_without(component_id);
67187
}
68188
}
69189
}
70190

71191
pub(super) fn is_dense(&self, components: &Components) -> bool {
72-
self.ids().all(|component_id| {
192+
self.disabling_ids().all(|component_id| {
73193
components
74194
.get_info(component_id)
75195
.is_some_and(|info| info.storage_type() == StorageType::Table)
@@ -81,24 +201,16 @@ impl DefaultQueryFilters {
81201
mod tests {
82202

83203
use super::*;
204+
use crate::{
205+
prelude::World,
206+
query::{Has, With},
207+
};
84208
use alloc::{vec, vec::Vec};
85209

86210
#[test]
87-
fn test_set_filters() {
88-
let mut filters = DefaultQueryFilters::default();
89-
assert_eq!(0, filters.ids().count());
90-
91-
assert!(filters.set_disabled(ComponentId::new(1)).is_some());
92-
assert!(filters.set_disabled(ComponentId::new(3)).is_none());
93-
94-
assert_eq!(1, filters.ids().count());
95-
assert_eq!(Some(ComponentId::new(1)), filters.ids().next());
96-
}
97-
98-
#[test]
99-
fn test_apply_filters() {
100-
let mut filters = DefaultQueryFilters::default();
101-
filters.set_disabled(ComponentId::new(1));
211+
fn filters_modify_access() {
212+
let mut filters = DefaultQueryFilters::empty();
213+
filters.register_disabling_component(ComponentId::new(1));
102214

103215
// A component access with an unrelated component
104216
let mut component_access = FilteredAccess::<ComponentId>::default();
@@ -107,7 +219,7 @@ mod tests {
107219
.add_component_read(ComponentId::new(2));
108220

109221
let mut applied_access = component_access.clone();
110-
filters.apply(&mut applied_access);
222+
filters.modify_access(&mut applied_access);
111223
assert_eq!(0, applied_access.with_filters().count());
112224
assert_eq!(
113225
vec![ComponentId::new(1)],
@@ -118,7 +230,7 @@ mod tests {
118230
component_access.and_with(ComponentId::new(4));
119231

120232
let mut applied_access = component_access.clone();
121-
filters.apply(&mut applied_access);
233+
filters.modify_access(&mut applied_access);
122234
assert_eq!(
123235
vec![ComponentId::new(4)],
124236
applied_access.with_filters().collect::<Vec<_>>()
@@ -133,7 +245,7 @@ mod tests {
133245
component_access.and_with(ComponentId::new(1));
134246

135247
let mut applied_access = component_access.clone();
136-
filters.apply(&mut applied_access);
248+
filters.modify_access(&mut applied_access);
137249
assert_eq!(
138250
vec![ComponentId::new(1), ComponentId::new(4)],
139251
applied_access.with_filters().collect::<Vec<_>>()
@@ -147,11 +259,46 @@ mod tests {
147259
.add_archetypal(ComponentId::new(1));
148260

149261
let mut applied_access = component_access.clone();
150-
filters.apply(&mut applied_access);
262+
filters.modify_access(&mut applied_access);
151263
assert_eq!(
152264
vec![ComponentId::new(4)],
153265
applied_access.with_filters().collect::<Vec<_>>()
154266
);
155267
assert_eq!(0, applied_access.without_filters().count());
156268
}
269+
270+
#[derive(Component)]
271+
struct CustomDisabled;
272+
273+
#[test]
274+
fn multiple_disabling_components() {
275+
let mut world = World::new();
276+
world.register_disabling_component::<CustomDisabled>();
277+
278+
world.spawn_empty();
279+
world.spawn(Disabled);
280+
world.spawn(CustomDisabled);
281+
world.spawn((Disabled, CustomDisabled));
282+
283+
let mut query = world.query::<()>();
284+
assert_eq!(1, query.iter(&world).count());
285+
286+
let mut query = world.query_filtered::<(), With<Disabled>>();
287+
assert_eq!(1, query.iter(&world).count());
288+
289+
let mut query = world.query::<Has<Disabled>>();
290+
assert_eq!(2, query.iter(&world).count());
291+
292+
let mut query = world.query_filtered::<(), With<CustomDisabled>>();
293+
assert_eq!(1, query.iter(&world).count());
294+
295+
let mut query = world.query::<Has<CustomDisabled>>();
296+
assert_eq!(2, query.iter(&world).count());
297+
298+
let mut query = world.query_filtered::<(), (With<Disabled>, With<CustomDisabled>)>();
299+
assert_eq!(1, query.iter(&world).count());
300+
301+
let mut query = world.query::<(Has<Disabled>, Has<CustomDisabled>)>();
302+
assert_eq!(4, query.iter(&world).count());
303+
}
157304
}

0 commit comments

Comments
 (0)