`Added<T>` should include entities that just became enabled
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
While working on [bevy_enhanced_input#273](https://github.com/simgine/bevy_enhanced_input/pull/273), which adds state-driven context activation using the `Disabled` component, @alice-i-cecile [commented](https://github.com/simgine/bevy_enhanced_input/pull/273#pullrequestreview-2847391274):
> "At some point I'm going to request this be moved upstream as a generic `bevy_state` tool, probably with entity enabling / disabling"
Investigating what this would entail led me to #18981 and #19087. The discussions focus on workarounds rather than addressing what seems to be a missing piece in the existing entity disabling system.
## Background
Bevy already has entity disabling:
- `Disabled` component (built-in)
- `DefaultQueryFilters` automatically excludes disabled entities
- `register_disabling_component` for custom disabling components
- `Allow` filter to opt-in to seeing disabled entities
What's **missing** is tracking **when** entities become enabled/disabled - there's no tick recorded for these transitions.
## The Problem
When an entity is spawned with `Disabled` and later enabled, systems using `Added` never see it:
```rust
// Frame 1
commands.spawn((Mesh3d::default(), Disabled));
// Frame N
commands.entity(e).remove::();
// This system NEVER sees the entity
fn init_meshes(query: Query<&Mesh3d, Added>) {
for mesh in &query {
// Entity never appears here
}
}
```
The entity is "new" to the query, but `Added` doesn't know that.
## Current vs Expected Semantics
| What `Added` means now | What users expect |
|---------------------------|-------------------|
| "Component T was inserted recently" | "Entity with T that I haven't seen yet" |
The current semantics describe the **component's lifecycle**. The useful semantics describe the **query's visibility**.
## Proposed Change
### 1. Track enable/disable transitions
Similar to how `SpawnedOrDespawned` tracks entity spawn/despawn in `EntityMeta`:
```rust
struct EnabledOrDisabled {
tick: Tick,
became_enabled: bool,
}
```
Stored sparsely (only for entities that have used disabling components).
Hooks update this when:
- **First** disabling component added → mark disabled
- **Last** disabling component removed → mark enabled
### 2. Update `Added` to check this
```rust
fn filter_fetch(...) -> bool {
let component_added = /* existing check */;
let just_enabled = entities
.enabled_or_disabled(entity)
.is_some_and(|s| s.became_enabled && s.tick.is_newer_than(last_run, this_run));
component_added || just_enabled
}
```
This follows the existing `Spawned` filter pattern.
## Why Not a Separate `Enabled` Filter?
The alternative is adding an `Enabled` filter, requiring:
```rust
Query<&Mesh3d, Or<(Added, Enabled)>>
```
| Issue | Impact |
|-------|--------|
| Leaky abstraction | Users must understand two "newness" concepts |
| Opt-in everywhere | Every system needs explicit `Or<(Added, Enabled)>` |
| Migration burden | All existing code needs updating |
| Ecosystem churn | Every third-party plugin needs updating |
Updating `Added` makes existing code **just work**.
## What About `Changed`?
Enabling an entity doesn't mutate its components - it only changes visibility. So `Changed` should **not** include just-enabled entities.
`Added` is about "new to this query." `Changed` is about "component was mutated."
## Bonus: `JustDisabled` Filter
For cleanup logic (stopping audio, hiding particles), tracking enable/disable transitions also enables a `JustDisabled` filter:
```rust
fn cleanup_audio(query: Query<&AudioSource, JustDisabled>) {
for source in &query {
source.stop();
}
}
```
## Questions for Discussion
1. Is changing `Added` semantics acceptable, or is a separate `Enabled` filter preferred despite the drawbacks?
2. Does this fully address the render world issue (#18981), or are there additional issues there?
3. Should `JustDisabled` be added as a companion filter?
## Related Issues
- #18981 - Entities spawned with `Disabled` do not render when `Disabled` is removed
- #19087 - Add state-driven entity disabling using `EnabledInState` component
- #18992 - PR: Force all components to be marked as changed (shotgun fix)
Contributor guide
Research direction
Start by reading EntityMeta and the existing SpawnedOrDespawned filter pattern, then trace filter_fetch and the disabling-component hooks described in the issue. Compare the proposed Added behavior with Changed and the render-world concern in #18981. Done requires a maintainer decision on Added versus Enabled, transition tracking, and whether JustDisabled belongs in scope.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- game-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100