Skip to content
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

Computed State & Sub States #11426

Merged
merged 198 commits into from
May 2, 2024
Merged

Conversation

lee-orr
Copy link
Contributor

@lee-orr lee-orr commented Jan 19, 2024

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:

  • Standard [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 a compute method 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 an InAMenu computed 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 State and 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 ComputedState is mainly a state matching feature, while SubStates are more of a state mutation related feature - but one that is easy to add with the help of the machinery introduced by ComputedState, it was added here as well. The relevant discussion is here: https://discord.com/channels/691052431525675048/1200556329803186316

Solution

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 FreelyMutableState trait , which is implemented as part of the derive macro for States. This allows us to limit use of NextState<S> to states that are actually mutable, preventing mis-use of ComputedStates.


Changelog

  • Added ComputedStates trait
  • Added FreelyMutableState trait
  • Converted NextState resource to an Enum, with Unchanged and Pending
  • Added App::add_computed_state::<S: ComputedStates>(), to allow for easily adding derived states to an App.
  • Moved the StateTransition schedule label from bevy_app to bevy_ecs - but maintained the export in bevy_app for continuity.
  • Modified the process for updating states. Instead of just having an apply_state_transition system that can be added anywhere, we now have a multi-stage process that has to run within the StateTransition label. First, all the state changes are calculated - manual transitions rely on apply_state_transition, while computed transitions run their computation process before both call internal_apply_state_transition to 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.
  • Added SubStates trait
  • Adjusted apply_state_transition to be a no-op if the State<S> resource doesn't exist

Migration 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:

  • if they created a NextState(Some(S)) - they should now use NextState::Pending(S)
  • if they created a NextState(None) -they should now use NextState::Unchanged
  • if they matched on the NextState value, they would need to make the adjustments above

If the user manually utilized apply_state_transition, they should instead use systems that trigger the StateTransition schedule.


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 the State related mechanisms all utilize options (Optional state #11417), it's fairly easy to add support for explicit state removal. And while ComputedStates can add and remove themselves, right now FreelyMutableStates 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#5

  • NextState::ReEnter - this would allow you to trigger exit & entry systems for the current state type. We can potentially also add a NextState::ReEnterRecirsive to also re-trigger any states that depend on the current one.

  • More mechanisms for State updates - 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 the Res<Option<State<S>>> resource in your system, clone the state, mutate it, and then use NextState.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:

    • Inverse Compute States - these would essentially be compute states that have an additional (manually defined) function that can be used to nudge the source states so that they result in the computed states having a given value. For example, you could use set the IsPaused state, and it would attempt to pause or unpause the game by modifying the AppState as needed.
    • Closure-based state modification - this would involve adding a 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.
    • Message-based state modification - this would involve either creating states that can respond to specific messages, similar to Elm or Redux. These could either use the NextState mechanism or the Event mechanism.
  • 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. this feature is now part of this PR. See above.

  • Lastly, since states are getting more complex there might be value in moving them out of bevy_ecs and into their own crate, or at least out of the schedule module into a states module. Move states into their own crate #11087

As 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.

@lee-orr
Copy link
Contributor Author

lee-orr commented Jan 19, 2024

@alice-i-cecile & @MiniaczQ - just wanted to ping you for this new PR, as promised a few weeks ago here: #10088 (comment)

Copy link
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@alice-i-cecile alice-i-cecile added C-Enhancement A new feature A-ECS Entities, components, systems, and events labels Jan 19, 2024
);
}
}
/// Trait defining a state whose value is automatically computed from other [`States`].
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy paste from ComputedStates

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks

Copy link
Contributor

@maniwani maniwani left a comment

Choose a reason for hiding this comment

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

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.

@alice-i-cecile alice-i-cecile added X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers and removed X-Controversial There is active debate or serious implications around merging this PR labels May 2, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 2, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 2, 2024
@alice-i-cecile
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Enhancement A new feature C-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers
Projects
Status: Merged PR
Development

Successfully merging this pull request may close these issues.

Sub State Support