bevyengine / bevyengine/bevy

`Added<T>` should include entities that just became enabled

Open
#22,627 1 comment 1 reaction 0 assignees View on GitHub
A-ECS D-Complex S-Needs-Design S-Needs-Goal X-Needs-SME
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.