Computed State & Sub States#11426
Conversation
…nto optional-state
…nto optional-state
…nd "Remove". Add matching functions to NextState impl. Remove RemoveState struct. Remove unneeded apply_deferred system calls from state derivation systems.
|
@alice-i-cecile & @MiniaczQ - just wanted to ping you for this new PR, as promised a few weeks ago here: #10088 (comment) |
|
The generated |
MiniaczQ
left a comment
There was a problem hiding this comment.
Looks good, I like the new event driven architecture
| // but only mark it as changed if it wasn't empty. | ||
| let Some(mut next_state_resource) = world.get_resource_mut::<NextState<S>>() else { | ||
| /// | ||
| /// If the [`State<S>`] resource does not exist, it does nothing. Removing or adding states |
There was a problem hiding this comment.
Can we address sub-states here?
It's common for them to not exist at given point in time and setting their NextState<S> should be well defined.
IMO it should be cleared if the sub-state doesn't exist.
A reactive resource like NextState would be weird if it persisted, acted with delay.
It should also be the initial value of a sub-state when it comes to exist.
E.g.
enum AppState {
Menu,
Game,
}
enum GameState {
Tutorial,
Campaign,
Custom,
}
fn main() {
// [...]
app.init_state::<AppState>()
.add_sub_state::<GameState>();
}
fn menu_ui(app: ResMut<NextState<AppState>>, game: ResMut<NextState<GameState>>) {
// [...]
app.set(AppState::Game); // Change root state
match mode { // Change sub-state when it comes to exist
"tutorial" => game.set(GameState::Tutorial),
"campaign" => game.set(GameState::Campaign),
"custom" => game.set(GameState::Custom),
}
}For the scope of this PR and your sanity, I think we should decide on the correct scenario and ensure it's handled in another PR.
There was a problem hiding this comment.
I set it up so that, at this point at least, NextState gets cleared if it is ignored - so the initial value is always going to be the result from the "should_exist" method.
I think I might, in a future PR, look at allowing "should_exist" to accept the "NextState" as a potential input as well - primarily since I like the idea of letting you influence the initial value, but don't want to have two conflicting sources of an initial value ("should_exist" and "NextState") to avoid weird bugs/unexpected behaviour.
| ); | ||
| } | ||
| } | ||
| /// Trait defining a state whose value is automatically computed from other [`States`]. |
There was a problem hiding this comment.
Copy paste from ComputedStates
There was a problem hiding this comment.
I like this solution. The state machine logic isn't integral to the ECS operations, so this solution being so self-contained (not inventing new ECS machinery) aligns with other major ECS work (observers, relationships, etc.).
I haven't followed all the discussions that led to this point, but looking at the code and examples, usage seems easy to understand and I know the author has been iterating on this feature for a long time (and incorporating a lot of feedback), so I trust their judgement.
Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
|
Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1308 if you'd like to help out. |
Summary/Description
This PR extends states to allow support for a wider variety of state types and patterns, by providing 3 distinct types of state:
States] can only be changed by manually setting the [NextState<S>] resource. These states are the baseline on which the other state types are built, and can be used on their own for many simple patterns. See the state example for a simple use case - these are the states that existed so far in Bevy.SubStates] are children of other states - they can be changed manually using [NextState<S>], but are removed from the [World] if the source states aren't in the right state. See the sub_states example for a simple use case based on the derive macro, or read the trait docs for more complex scenarios.ComputedStates] are fully derived from other states - they provide acomputemethod that takes in the source states and returns their derived value. They are particularly useful for situations where a simplified view of the source states is necessary - such as having anInAMenucomputed state derived from a source state that defines multiple distinct menus. See the computed state example to see a sampling of uses for these states.Objective
This PR is another attempt at allowing Bevy to better handle complex state objects in a manner that doesn't rely on strict equality. While my previous attempts (#10088 and #9957) relied on complex matching capacities at the point of adding a system to application, this one instead relies on deterministically deriving simple states from more complex ones.
As a result, it does not require any special macros, nor does it change any other interactions with the state system once you define and add your derived state. It also maintains a degree of distinction between
Stateand just normal application state - your derivations have to end up being discreet pre-determined values, meaning there is less of a risk/temptation to place a significant amount of logic and data within a given state.Addition - Sub States
closes #9942
After some conversation with Maintainers & SMEs, a significant concern was that people might attempt to use this feature as if it were sub-states, and find themselves unable to use it appropriately. Since
ComputedStateis mainly a state matching feature, whileSubStatesare more of a state mutation related feature - but one that is easy to add with the help of the machinery introduced byComputedState, it was added here as well. The relevant discussion is here: https://discord.com/channels/691052431525675048/1200556329803186316Solution
closes #11358
The solution is to create a new type of state - one implementing
ComputedStates- which is deterministically tied to one or more other states. Implementors write a function to transform the source states into the computed state, and it gets triggered whenever one of the source states changes.In addition, we added the
FreelyMutableStatetrait , which is implemented as part of the derive macro forStates. This allows us to limit use ofNextState<S>to states that are actually mutable, preventing mis-use ofComputedStates.Changelog
ComputedStatestraitFreelyMutableStatetraitNextStateresource to an Enum, withUnchangedandPendingApp::add_computed_state::<S: ComputedStates>(), to allow for easily adding derived states to an App.StateTransitionschedule label frombevy_apptobevy_ecs- but maintained the export inbevy_appfor continuity.apply_state_transitionsystem that can be added anywhere, we now have a multi-stage process that has to run within theStateTransitionlabel. First, all the state changes are calculated - manual transitions rely onapply_state_transition, while computed transitions run their computation process before both callinternal_apply_state_transitionto apply the transition, send out the transition event, trigger dependent states, and record which exit/transition/enter schedules need to occur. Once all the states have been updated, the transition schedules are called - first the exit schedules, then transition schedules and finally enter schedules.SubStatestraitapply_state_transitionto be a no-op if theState<S>resource doesn't existMigration Guide
If the user accessed the NextState resource's value directly or created them from scratch they will need to adjust to use the new enum variants:
NextState(Some(S))- they should now useNextState::Pending(S)NextState(None)-they should now useNextState::UnchangedNextStatevalue, they would need to make the adjustments aboveIf the user manually utilized
apply_state_transition, they should instead use systems that trigger theStateTransitionschedule.Future Work
There is still some future potential work in the area, but I wanted to keep these potential features and changes separate to keep the scope here contained, and keep the core of it easy to understand and use. However, I do want to note some of these things, both as inspiration to others and an illustration of what this PR could unlock.
NextState::Remove- Now that theStaterelated mechanisms all utilize options (Optional state #11417), it's fairly easy to add support for explicit state removal. And whileComputedStatescan add and remove themselves, right nowFreelyMutableStates can't be removed from within the state system. While it existed originally in this PR, it is a different question with a separate scope and usability concerns - so having it as it's own future PR seems like the best approach. This feature currently lives in a separate branch in my fork, and the differences between it and this PR can be seen here: Remove state lee-orr/bevy#5NextState::ReEnter- this would allow you to trigger exit & entry systems for the current state type. We can potentially also add aNextState::ReEnterRecirsiveto also re-trigger any states that depend on the current one.More mechanisms for
Stateupdates - This PR would finally make states that aren't a set of exclusive Enums useful, and with that comes the question of setting state more effectively. Right now, to update a state you either need to fully create the new state, or include theRes<Option<State<S>>>resource in your system, clone the state, mutate it, and then useNextState.set(my_mutated_state)to make it the pending next state. There are a few other potential methods that could be implemented in future PRs:IsPausedstate, and it would attempt to pause or unpause the game by modifying theAppStateas needed.NextState.modify(f: impl Fn(Option<S> -> Option<S>)method, and then you can pass in closures or function pointers to adjust the state as needed.NextStatemechanism or the Event mechanism.this feature is now part of this PR. See above.SubStates- which are essentially a hybrid of computed and manual states. In the simplest (and most likely) version, they would work by having a computed element that determines whether the state should exist, and if it should has the capacity to add a new version in, but then any changes to it's content would be freely mutated.Lastly, since states are getting more complex there might be value in moving them out of
bevy_ecsand into their own crate, or at least out of theschedulemodule into astatesmodule. Move states into their own crate #11087As mentioned, all these future work elements are TBD and are explicitly not part of this PR - I just wanted to provide them as potential explorations for the future.