Skip to content

Conversation

@mockersf
Copy link
Member

@mockersf mockersf commented May 25, 2025

Objective

  • Same state transitions have their uses but are not currently possible

Solution

  • Add a set_forced method on NextState that will trigger OnEnter and OnExit
  • Rerun state transitions when set_forced has been used
  • Rerun them is set is called after set_forced with the same state

@mockersf mockersf added the A-States App-level states machines label May 25, 2025
@benfrankel
Copy link
Contributor

How does this approach interact with computed and sub states?

@janhohenheim
Copy link
Member

janhohenheim commented May 25, 2025

@janhohenheim janhohenheim added the S-Needs-Review Needs reviewer attention (from anyone!) to move forward label May 25, 2025
@benfrankel
Copy link
Contributor

Additional previous work in #11160 and #11158 as well :)

@janhohenheim
Copy link
Member

janhohenheim commented May 25, 2025

Labeling as contentious given that all previous work in this regard was contested
Also related to usability of #19296

@janhohenheim janhohenheim added the X-Contentious There are nontrivial implications that should be thought through label May 25, 2025
@benfrankel
Copy link
Contributor

benfrankel commented May 25, 2025

We might want a working group for bevy_state at some point. There are some old footguns and missing features that users keep hitting again and again (like same-state transitions and the pre-PreStartup schedule), and it would take a deeper redesign to resolve them without introducing technical debt. I know how this can be done, but I don't believe it would be feasible for me to push anything through without a working group behind it.

My take on this particular feature though is that it's so basic and crucial that it's worth introducing technical debt to get it in even without a redesign (obviously since I've tried to do just that in 4 separate PRs). Just need to document how it interacts with computed and sub states.

@mockersf
Copy link
Member Author

How does this approach interact with computed and sub states?

I think it doesn't, it's only enabled when actually calling the method on NextState

This is scoped as small as I think is viable, the only extra thing in this PR is making sure that if next_state.set(State::SomeVariant) is called after next_state.set_forced(State::SomeVariant), it will still rerun the transition schedules

@mgi388
Copy link
Contributor

mgi388 commented May 26, 2025

next_state.set_forced(State::SomeVariant) reads to me like it's forcing the setting of that state now. This is because the context you're in here is "next state" so it makes you think "oh maybe the state will change now by forcing it".

But what set_forced is really doing is something to do with allowing same state transitions to trigger OnEnter and OnExit.

So whether this is just a naming thing, or a flaw in the design, I'm not sure, but set_forced doesn't seem right to me.

I also wondered whether you'd do this at the place you actually change the state (as in this PR), or if you do it in the definition of your states (if it's technically possible).

@mockersf
Copy link
Member Author

mockersf commented May 26, 2025

next_state.set_forced(State::SomeVariant) reads to me like it's forcing the setting of that state now. This is because the context you're in here is "next state" so it makes you think "oh maybe the state will change now by forcing it".

What do you think of set_with_forced_reload as a name? or set_and_enforce_reload?

I also wondered whether you'd do this at the place you actually change the state (as in this PR), or if you do it in the definition of your states (if it's technically possible).

I think it's better to do it like in this pr.

If we want to go for a breaking change, I would probably make this PR the default, and change the current set to set_if_not_already or something

@mgi388
Copy link
Contributor

mgi388 commented May 26, 2025

I think it's better to do it like in this pr.

What's your thinking? Is it because you specifically want per-call site control over this? The reason I mentioned it is that when coming up with a name and design for the per-call site version (this PR), it seems hard to come up with a concise and expressive design whereas when it's defined at the type level, it becomes easier to express. For example, maybe it looks something like this:

#[derive(States)]
enum GameState {
    #[default]
    MainMenu,
    SettingsMenu,
    InGame,
}

// Default behavior: schedules skipped for identical states.
impl StateTransitionBehavior for GameState {
    fn always_run_entry_exit_transitions() -> bool { false }
}

#[derive(States)]
enum ResettingState {
    #[default]
    Idle,
    Active,
}

// Always run transitions, even for identical states.
impl StateTransitionBehavior for ResettingState {
    fn always_run_entry_exit_transitions() -> bool { true }
}

fn set_game_state(mut next_game_state: ResMut<NextState<GameState>>) {
    // Standard behavior: schedules skipped if state is unchanged.
    next_game_state.set(GameState::InGame);
}

fn set_resetting_state(mut next_state: ResMut<NextState<ResettingState>>) {
    // Schedules always run, even if state is unchanged.
    next_state.set(ResettingState::Active);
}

That aside, if we're trying to do this in the NextState API, the most expressive approach could be some sort of builder-style pattern, e.g.,

next_game_state.set(GameState::InGame).with_entry_exit_transitions().apply()

But it raises questions of compatibility which we'd have to decide on.

If I had to pick something on the NextState API without being a breaking change, I think I'm inclined to go with this:

next_game_state.set_with_entry_exit(GameState::InGame); // or:
next_game_state.set_with_entry_exit_transitions(GameState::InGame); 

(noting that I picked entry_exit over enter_exit because the second one feels less idiomatic as a noun phrase)

Or, if you want to change set to do this (breaking change), and introduce a new one that turns it off, then call that one:

next_game_state.set_without_entry_exit(GameState::InGame); // or:
next_game_state.set_without_entry_exit_transitions(GameState::InGame);

If you wanted a breaking change and you want per-call site control, I think a builder pattern could work here and would be the most expressive.

@ThierryBerger
Copy link
Member

Not blocking: as a follow up, updating the examples to include a usage would be neat :)

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.

Needs a migration guide but I am convinced that this is correct at this point. It's too useful for hotpatching, which solves my main objection of "is this actually widely useful behavior".

@alice-i-cecile alice-i-cecile added M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 16, 2025
@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Oct 16, 2025
@github-actions
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.

@mockersf mockersf force-pushed the same-state-transitions branch from 2cfcb58 to 8c70da3 Compare October 18, 2025 23:23
@mockersf
Copy link
Member Author

@alice-i-cecile not sure what you want in a migration guide? Existing code would continue to work exactly the same.

@alice-i-cecile
Copy link
Member

I would like a simple note that same-state transitions now work. Many users have asked for this, and likely have workarounds in place that they can now remove.

Migration guides aren't simply about "things that must be changed": they're a good place to put updated best practices, especially for things that are too small for release notes.

@mockersf mockersf added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Nov 8, 2025
Copy link
Member

@janhohenheim janhohenheim left a comment

Choose a reason for hiding this comment

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

I'm not happy about the naming at all, but I believe it's better to get this in now rather than bikeshed it too much. Can still improve it later.

I think it's worth considering giving this the neat set_next name and calling the other one set_next_different.

@janhohenheim janhohenheim 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-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 8, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Nov 9, 2025
Merged via the queue into bevyengine:main with commit 0b802bd Nov 9, 2025
40 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Dec 9, 2025
# Objective

- #19363 finally allows
self-state transitions
- The naming is a bit meh (`set_forced`)
- Many people expect this to work out of the box
- That PR also missed a few spots where this distinction applies

## Solution

- Expand on #19363 by making it
the default
- Rename `set` -> `set_if_neq`
- Rename `set_forced` -> `set`
- Add the two variants to 
  - commands
  - reflection
  - computed states
- make the transition logs a bit less chatty in the common case
- add a test for the new behavior


## Testing

- CI
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 M-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.

6 participants