Skip to content

Rename StateScoped to DespawnOnExitState and add DespawnOnEnterState #18818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
May 6, 2025

Conversation

mgi388
Copy link
Contributor

@mgi388 mgi388 commented Apr 12, 2025

Objective

Solution

  • Rename component StateScoped to DespawnOnExitState.
  • Rename system clear_state_scoped_entities to despawn_entities_on_exit_state.
  • Add DespawnOnEnterState and despawn_entities_on_enter_state which is the OnEnter equivalent.

Note

Compared to #16284, the main change is that I did the rename in such a way as to keep the terms OnExit and OnEnter together. In my own game, I was adding VisibleOnEnterState and HiddenOnExitState and when naming those, I kept the OnExit and OnEnter together. When I checked #16284 it stood out to me that the naming was a bit awkward. Putting the State in the middle and breaking up OnEnter and OnExit also breaks searching for those terms.

Open questions

  1. Should we split enable_state_scoped_entities into two functions, one for the OnEnter and one for the OnExit? I personally have zero need thus far for the OnEnter version, so I'd be interested in not having this enabled unless I ask for it.
  2. If yes to 1., should we follow my lead in my Visibility state components (see below) and name these app.enable_despawn_entities_on_enter_state() and app.enable_despawn_entities_on_exit_state(), which IMO says what it does on the tin?

Testing

Ran all changed examples.

Side note: VisibleOnEnterState and HiddenOnExitState

For reference to anyone else and to help with the open questions, I'm including the code I wrote for controlling entity visibility when a state is entered/exited.

visibility.rs
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_reflect::prelude::*;
use bevy_render::prelude::*;
use bevy_state::{prelude::*, state::StateTransitionSteps};
use tracing::*;

pub trait AppExtStates {
    fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self;

    fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self;
}

impl AppExtStates for App {
    fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self {
        self.main_mut()
            .enable_visible_entities_on_enter_state::<S>();
        self
    }

    fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self {
        self.main_mut().enable_hidden_entities_on_exit_state::<S>();
        self
    }
}

impl AppExtStates for SubApp {
    fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self {
        if !self
            .world()
            .contains_resource::<Events<StateTransitionEvent<S>>>()
        {
            let name = core::any::type_name::<S>();
            warn!("Visible entities on enter state are enabled for state `{}`, but the state isn't installed in the app!", name);
        }
        // We work with [`StateTransition`] in set
        // [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`],
        // because [`OnExit`] only runs for one specific variant of the state.
        self.add_systems(
            StateTransition,
            update_to_visible_on_enter_state::<S>.in_set(StateTransitionSteps::ExitSchedules),
        )
    }

    fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self {
        if !self
            .world()
            .contains_resource::<Events<StateTransitionEvent<S>>>()
        {
            let name = core::any::type_name::<S>();
            warn!("Hidden entities on exit state are enabled for state `{}`, but the state isn't installed in the app!", name);
        }
        // We work with [`StateTransition`] in set
        // [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`],
        // because [`OnExit`] only runs for one specific variant of the state.
        self.add_systems(
            StateTransition,
            update_to_hidden_on_exit_state::<S>.in_set(StateTransitionSteps::ExitSchedules),
        )
    }
}

#[derive(Clone, Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct VisibleOnEnterState<S: States>(pub S);

#[derive(Clone, Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct HiddenOnExitState<S: States>(pub S);

/// Makes entities marked with [`VisibleOnEnterState<S>`] visible when the state
/// `S` is entered.
pub fn update_to_visible_on_enter_state<S: States>(
    mut transitions: EventReader<StateTransitionEvent<S>>,
    mut query: Query<(&VisibleOnEnterState<S>, &mut Visibility)>,
) {
    // We use the latest event, because state machine internals generate at most
    // 1 transition event (per type) each frame. No event means no change
    // happened and we skip iterating all entities.
    let Some(transition) = transitions.read().last() else {
        return;
    };
    if transition.entered == transition.exited {
        return;
    }
    let Some(entered) = &transition.entered else {
        return;
    };
    for (binding, mut visibility) in query.iter_mut() {
        if binding.0 == *entered {
            visibility.set_if_neq(Visibility::Visible);
        }
    }
}

/// Makes entities marked with [`HiddenOnExitState<S>`] invisible when the state
/// `S` is exited.
pub fn update_to_hidden_on_exit_state<S: States>(
    mut transitions: EventReader<StateTransitionEvent<S>>,
    mut query: Query<(&HiddenOnExitState<S>, &mut Visibility)>,
) {
    // We use the latest event, because state machine internals generate at most
    // 1 transition event (per type) each frame. No event means no change
    // happened and we skip iterating all entities.
    let Some(transition) = transitions.read().last() else {
        return;
    };
    if transition.entered == transition.exited {
        return;
    }
    let Some(exited) = &transition.exited else {
        return;
    };
    for (binding, mut visibility) in query.iter_mut() {
        if binding.0 == *exited {
            visibility.set_if_neq(Visibility::Hidden);
        }
    }
}

@mgi388 mgi388 added M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide D-Straightforward Simple bug fixes and API improvements, docs, test and examples A-States App-level states machines X-Contentious There are nontrivial implications that should be thought through labels Apr 12, 2025
Copy link
Contributor

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes.

@mgi388 mgi388 force-pushed the rename-StateScoped2 branch from 986f57d to 467ba58 Compare April 12, 2025 07:24
Copy link
Contributor

@benfrankel benfrankel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a few minor comments on the documentation, but the code looks good and I like the rename.

@mgi388
Copy link
Contributor Author

mgi388 commented Apr 16, 2025

@benfrankel thanks for the review ❤️. I'll apply the suggestions shortly (no rush due to 0.16 shenanigans).

In the meantime, if you have any thoughts on the open questions in the PR description, please feel free to add your opinion

@benfrankel
Copy link
Contributor

benfrankel commented Apr 16, 2025

I think it would be best to keep it to a single enable_ method for simplicity, since the performance cost per system is likely negligible. If it is to be split up though, I think the proposed naming sounds good.

Another naming option is apply_despawn_on_enter_state for the system instead of despawn_entities_on_enter_state, and enable_despawn_on_enter_state for the method instead of enable_despawn_entities_on_enter_state. But I think it's fine either way.

Copy link
Contributor

@Henauxg Henauxg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I'm also not interested in DespawnOnEnterState either, I think the performance cost of enabling both with 1 call is likely negligible as it is only scheduled to run once during transitions, whereas the verbosity of splitting the enabling calls seems cumbersome. It will still be possible to refine it later once more state-scope mechanisms are introduced.
Regarding the naming question for the common enabling call, it seems from the discussions in #16284 that "state scope" could still be used to name the feature (bikeshed: could also introduce something like "Entities Lifetime" which could potentially extend to more usages than just state scopes).

Regarding the internal systems name, I prefer despawn_entities_on_enter_state rather than apply_despawn_on_enter_state.

(As a more general note, I feel like it might have been easier to split the work as a straightforward renaming PR from StateScoped to DespawnOnExitState and then have a PR to propose an extension of state scoping mechanisms)

@mgi388 mgi388 added the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Apr 18, 2025
@mgi388
Copy link
Contributor Author

mgi388 commented Apr 24, 2025

(As a more general note, I feel like it might have been easier to split the work as a straightforward renaming PR from StateScoped to DespawnOnExitState and then have a PR to propose an extension of state scoping mechanisms)

@Henauxg in case you weren't aware, I adopted this PR and all the work and review was already done, so it probably would have been more effort to start from scratch, even if you're right.

@mgi388
Copy link
Contributor Author

mgi388 commented Apr 24, 2025

@benfrankel @Henauxg how about this rename for the enable_ funcs.

First, as a reminder, the goal of the rename (more than just pointless rename churn) is that enable_state_scoped_entities:

  1. Is vague. In the context of adding the visibility-based one, it reads like a superset of both, and doesn't really say anything about how it only controls the despawning of entities.
  2. Makes less sense once the original StateScoped component no longer exists. Users will see a component exists named DespawnOnExitState and it won't be obvious that to enable it you need to look for a method named enable_state_scoped_entities.
  3. The entities part being plural is, arguably, awkward and unnecessary.

Proposal:

app.enable_state_scoped_entity_despawning::<GameState>();
app.enable_state_scoped_entity_visibility::<GameState>();

// The visibility one just to show that it _could_ exist, even in userland, 
// and also to show how the naming would sit with the despawning version

I went back and forth a bit coming up with this. Rationale/thinking:

  1. enable_ - shows the user is turning on a feature.
  2. state_scoped_ - shows what we're really enabling is state-based control over entities.
  3. entity_ - clearly shows we're dealing with entities. Without this, it's not obvious that this is for entities.
  4. entity_ not entities_—the plural is not necessary and just makes the whole thing harder to parse.
  5. despawning - tells users that this is specifically about despawning. I thought about lifecycle here, but it's a bit misleading. This feature can only despawn. I also went with despawning not despawn because that feels like it matches better with visibility but I could be convinced that despawn is OK.
  6. visibility - tells users that this is specifically about visibility. Note: Compared to my OP, I'd imagine that the user calls this function and underneath, it enables systems for all of On/Enter/Visibible/Hidden combos.

If we wanted, we could also use the term bound instead of scoped, as follows. I'm probably impartial to bound vs scoped even if "scoped" has always stuck out as an awkward term personally.

app.enable_state_bound_entity_despawning::<GameState>();
app.enable_state_bound_entity_visibility::<GameState>();

WDYT?

@Henauxg
Copy link
Contributor

Henauxg commented Apr 24, 2025

I thought that I would prefer scope to bound as scope is often used in this same context in CS: scope of a variable, function, ... But after giving it a go, with the addition of the OnEnter despawning, this really goes against what we assume when we talk about the "scope" of some object. You usually disappear when going "out of scope", not when coming into it.

Here's my attempt at a more accurate name:

app.enable_entities_despawning_on_state_transitions::<GameState>();

The plural does not seem like a bad thing to me. It manages entities, not an entity. We have plural on App methods such as add_plugins, add_systems, but I'm not bent on it.
I'd like something shorter using Schedule labels directly to convey the information such as an hypothetical app.enable_entities_despawning(OnExit(GameState)); but I'm sure it would be much messier to implement.

EDIT: the use of enable was fine when we didn't have a Disabled component. Now that we have it, I think this is a bit confusing (especially if we introduce a new state scoped enable/disable mechanic: enabled_state_enabled_entities...). Could be replaced with something similar: activate_entities_despawning_on..., schedule_entities_despawning_on..., ...

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Apr 24, 2025
@alice-i-cecile
Copy link
Member

I'm pretty sure I have opinions here on what an ideal naming / setup looks like, but I can tackle that during final review. Other maintainers: please let me handle this one?

@benfrankel
Copy link
Contributor

FWIW in my own crate pyri_state I call this feature react, or "state reaction components". So another option for the bikeshed is app.enable_state_reaction_components.

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration guide looks good.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've come around and quite like the current naming. Let's merge this, and then make an entity-disabling form in follow-up.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 6, 2025
Merged via the queue into bevyengine:main with commit 7a1fcb7 May 6, 2025
38 checks passed
@mgi388 mgi388 deleted the rename-StateScoped2 branch May 6, 2025 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-States App-level states machines D-Straightforward Simple bug fixes and API improvements, docs, test and examples M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rename StateScoped to StateExitDespawn, add the inverse variant StateEntryDespawn
6 participants