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

State Pattern Matching - Alternative #10088

Closed
wants to merge 157 commits into from

Conversation

lee-orr
Copy link
Contributor

@lee-orr lee-orr commented Oct 11, 2023

This is an alternative solution to the one in #9957 - it builds on the same basis, but provides a different API. I provide a fuller explanation on the rational for the change in the [Reasons for the Change] section.

Objective

The main purpose of this PR is to enable versatile state matching. The current implementation of States relies solely on equality when determining whether a given system or schedule should run:

enum AppState {
  Menu,
  Options,
  Credits,
  InGame(GameState)
}

enum GameState {
  Running,
  Paused
}

app.add_systems(OnExit(AppState::Menu), clear_ui);
app.add_systems(OnExit(AppState::Options), clear_ui);
app.add_systems(OnExit(AppState::Credits), clear_ui);

app.add_systems(Update, animate_idle_character.run_if((in_state(AppState::InGame(GameState::Running)),in_state(AppState::InGame(GameState::Paused)));
``

or, for an example based on structs:

```rust
struct AppState {
  menu: Option<Menu>
  in_game: Option<GameState>
}

enum Menu {
  Main,
  Options,
  Credits,
}

enum GameState {
  Running,
  Paused
}


app.add_systems(OnExit(AppState { menu: Some(Menu::Main), in_game: None}), clear_ui);
app.add_systems(OnExit(AppState { menu: Some(Menu::Options), in_game: None}), clear_ui);
app.add_systems(OnExit(AppState { menu: Some(Menu::Credits), in_game: None}), clear_ui);

app.add_systems(Update, animate_idle_character.run_if((in_state(AppState { menu: None, in_game: GameState::Running }),in_state(AppState { menu: None, in_game: GameState::Paused}));

This can lead to a variety of issues when the structure of the state object changes during development, or if you don't accurately foresee the way the states could change.

In addition, it is much harder to handle state changes that should only occur when entering/leaving a collection of states, but not when moving between them - like so:

// Enum
app.add_systems(OnEnter(AppState::Menu), setup_menu_backdrop);
app.add_systems(OnEnter(AppState::Options), setup_menu_backdrop);
app.add_systems(OnEnter(AppState::Credits), setup_menu_backdrop);

// State
app.add_systems(OnEnter(AppState { menu: Some(Menu::Main), in_game: None}), setup_menu_backdrop);
app.add_systems(OnEnter(AppState { menu: Some(Menu::Options), in_game: None}), setup_menu_backdrop);
app.add_systems(OnEnter(AppState { menu: Some(Menu::Credits), in_game: None}), setup_menu_backdrop);

fn setup_menu_backdrop(is_set_up_query: Query<Entity, With<Backdrop>>, ...) {
  if !is_set_up_query.is_empty() {
    return;
  }
  ...
}

This PR provides a set of tools for matching states in a more robust, reliable, and ergonomic way.

Solution

There are a few layers to the solution, and originally I went from "lowest level" to "highest level" - but I feel that added to some confusion on the reasoning/purpose behind each layer of the solution. So here, I will be going from the "highest level" stuff, which is most likely to be used directly by end users, to the "lowest level" stuff that serves as infrastructure for the higher level elements.

State Matching Macro

This solution adds the state_matches! macro for states and state transitions - allowing you to use pattern matching syntax with some small adjustments.

So - for the example above, we would be able to use:

// Enum Example
.add_systems(Entering, setup_menu_backdrop.run_if(state_matches!((AppState, Menu | Options | Credits)))
.add_systems(Exiting, clear_ui.run_if(state_matches!(AppState, every Menu | Options | Credits)))
.add_sytstems(Update, animate_idle_character.run_if(state_matches!(AppState, InGame(_))));

// State Example
.add_systems(Entering, setup_menu_backdrop.run_if(state_matches!(AppState, AppState { menu: Some(_), .. })))
.add_systems(Exiting, clear_ui.run_if(state_matches!(AppState, every AppState { menu: Some(_), .. })))
.add_sytstems(Update, animate_idle_character.run_if(state_matches!(AppState, AppState { in_game: Some(_), .. }))));

More details about the macro syntax exist below.

Note that the macro generates a StateMatcher, so it can be used anywhere those are used.

State Matchers

Speaking of StateMatchers - these form the core of the solution, providing a separation between the state storage/data (implementers of States) and the mechanism for determining whether a system should run in a given state (implementers of StateMatcher<S: States>).

There are a limited number of auto-implementations of StateMatcher<S>, as well as a couple of utility structs that implement it, but otherwise it is behind a sealed interface and inaccessible to the outside world.

In addition, StateMatchers implement IntoSystem<(), bool>, making them a valid run condition! That's why we don't need to add any in_state(), entering() or state_matches() calls.

Any given StateMatcher can be used for two things:

  • to check if a single state matches it (using the S::match(&self, matcher) -> bool method)
  • to check if a transition between states matches it (using the S::match_Transition(matcher, main: Option<&S>, secondary: Option<&S>) -> MatchesStateTransition trait function)

The MatchesStateTransition Enum represents the different options for handling transitions:
TransitionMatches - when the whole transition matches, MainMatches - when the main matches, but the transition as a whole is invalid, NoMatch - when neither the main nor the transition match. This is used to enable every and normal options in the macros, as well as to allow for custom logic if you so choose.

The main parameter is always the primary state instance we care about:

  • when Entering - it is the incoming state, and the secondary parameter is the outgoing state
  • when Exiting - it is the outgoing state, and the secondary parameter is the incoming state
  • Otherwise, it is the current state, and the secondary parameter is None.

This allows you to re-use the same matcher for exits, entrances, and while in the state for example:

// defining our matcher
// we want to be in game, and not moving into or out of "Super Mode"
fn in_game(main: Option<&AppState>, secondary: Option<&AppState>) -> bool {
  if matches!(main, AppState::InGame(_)) {
      secondary != Some(AppState::SuperMode)
  } else {
    false
  }
}

// And then we can use it:
.add_systems(Entering, setup_game.run_if(in_game))
.add_systems(Exiting, cleanup_game.run_if(in_game))

// We can also use it for Update, which in this case will only run if we're in `AppState::InGame(_)`,
// and ignores the secondary
.add_system(Update, move_character.run_if(in_game))

In normal use, you would probably use either the state_matches! macro, the States itself (which is also a StateMatcher using Eq), Fn(&S) -> bool or Fn(&S, Option<&S>) -> bool.

The full list of auto implementations is:

  • S itself
  • Fn(&Self) -> bool
  • Fn(Option<&Self>) -> bool
  • Fn(&Self, &Self) -> bool
  • Fn(&Self, Option<&Self>) -> bool
  • Fn(Option<&Self>, Option<&Self>) -> bool
  • Fn(&Self, &Self) -> MatchesStateTransition
  • Fn(&Self, Option<&Self>) -> MatchesStateTransition
  • Fn(Option<&Self>, Option<&Self>) -> MatchesStateTransition

StateMatcher's also expose 3 utility functions:

  • .every() - this sets it up to only use check if the main state matches, and fully ignores the secondary state in a transition. This is particularly useful since by default, if both the main and secondary match the transition will be treated as false (returning MatchesStateTransition::MainMatches).
  • .invert_transition() - this swaps the main and secondary states. This is useful for setting up transitions where you care about both the incoming and outgoing state, but want to use pre-defined or simple matchers for each rather than writing a dedicated function.
  • .chain(other: impl StateMatcher<S>) - if the current state matcher returns false (when testing a single state) or NoMatch (when testing a transition), it will execute other and return it's result.

Underlying Changes to State

  • Making all the systems relying on Res<State<S>> and Res<NextState<S>> within bevy_ecs rely on optional versions of the resources if possible
  • NextState<S> is no longer a struct, but rather an enum with 3 options: Keep, Value(S), Setter(Box<dyn Fn(S) -> S>). This is the only breaking change. The reasoning is: the previous use of NextState(Option<S>) created a confusion with None, where if one was not aware that states couldn't be removed they might assume setting the value to None removes the state. In addition, given that with this change states can be removed, that confusion would only get worse. In addition, with more complex States that aren't just a single, flat enum you might want to be able to "patch" a state rather than fully replace it. The Setter option allows you to provide a callback for patching the state, as it is at the time when apply_state_transition is called. By replacing the Option<S>, we opened up to a few additional use cases.
  • Adding schedules for Entering and Exiting - which will run systems whenever a state is changed. Exiting runs before State<S> is set to the new value, while Entering<S> runs after.

Notes on the macro syntax

You'll notice that the macro syntax is similar, but not identical, to the matches! syntax. This originates from the need to be able to determine the state type S for our matchers & schedule labels (see below). As work on the PR continued, this also evolved into the marco supporting a more robust syntax to resolve issues that came up.

I wanted to provide a full accounting of the macro syntax, and the reasoning behind it - but didn't want to place that within the earlier segments since I didn't want the more significant concepts to be lost to the details here.

The macro has the following structure: state_matches!(StateTypeIdent, MatchPattern, ...). The sections are detailed below:

StateTypeIdent

This is the name/path to the state type. This is required since we can't count on type inference when using patterns...

MatchPattern

A MatchPattern starts with an optional every keyword - which ensure the match will be true whenever the main state matches, regardless of the secondary state(see the StateMatcher explanation for more on main and secondary states in a state matcher).

After that, you can either provide a Pattern, a Closure, or an Expression:

  • The Pattern works like a normal pattern match, but you can exclude the root type name if you want (for example, InGame(_) instead of AppState::InGame(_)).
  • The Closure should follow one of the default implementations of StateMatcher - Fn(&S) -> bool, Fn(&S, Option<&S>) -> bool, Fn(Option<&S>, Option<&S>) -> bool, Fn(&S, Option<&S>) -> MatchesStateTransition or Fn(Option<&S>, Option<&S>) -> MatchesStateTransition.
  • The Expression should be a valid, existing, StateMatcher, preceded by an = symbol, like so: =AppState::MainMenu" or =in_game.The=` is used to differentiate expressions from patterns, which can't always be determined by syntax alone.

Note that you can have more than one MatchPattern in a macro, in a comma-separated list. If you do, they will be evaluated in order, and for transitions MatchesStateTransition::MainMatches will return immediately, only MatchesStateTransition::NoMatch will continue to evaluate the next pattern. This is needed because, at times, a simple every may not be enough. This is the same as the .chain() method on a state matcher.

For example, I might have a system for clearing the UI, and I want it to run whenever I move between distinct UI's. However, while I'm in game the UI remains consistent, even if the in game state changes - so what I want is:

add_systems(Exiting, clear_ui.run_if(state_matches!(AppState, InGame(_), every _)));

This will first check if I'm leaving InGame(_), and if so if I'm also moving to InGame(_) it will return false. However, otherwise - it will see that it should match every _ - meaning transitioning to and from any other state should return true and run the clear_ui system.

Reasons for the Change

Issues with the original

The previous PR had a lot of repetition both in the required syntax:

.add_systems(Entering, system.run_if(entering!(AppState, AppState::InGame(_)))

Here we need to set the schedule to Entering and use the entering! macro (or, in some cases, the entering function). The same was true for Exiting and Transitioning.

The same repetition also existed under the hood - with 3 run conditions that were virtually identical (entering, exiting, state_matches), and one that was more complex but only added a small amount of syntactic sugar (transitioning). And again, the same was the case with the macros.

We also had a robust set of tools for using closures & state values to match things (a variety of auto implementations of StateMatcher), but had to always use them through one of a variety of tools you'd have to learn and keep track of (the 4 macros and 4 run conditions).

Another issue is that we had some capabilities that were simple via macro, but had no non-macro equivalent - like every or the sequencing of multiple patterns/closures for handling a transition.

Lastly, it was hard to justify the existance of transitioning, since both entering and exiting had situations where they would look at both sides of a transition...

Changes

  • Remove all the macros except state_matches! - and use it for all pattern matching:
// From:
.add_systems(Entering, system.run_if(entering!(AppState, AppState::InGame(_)))
// To:
.add_systems(Entering, system.run_if(state_matches!(AppState, AppState::InGame(_)))
  • add a StateMatcherSystem (that wraps a function system), and implement IntoSystem<(), bool> for StateMatcher. This means we can use States values and StateMatcher closures directly as run conditions, and chain them:
// From:
.add_systems(Entering, system.run_if(entering!(AppState, |state: &AppState| state.is_paused))
// To:
.add_systems(Entering, system.run_if(|state: &AppState| state.is_paused)
  • Add EveryTransition(StateMatcher) - allowing access to the every feature outside of the macro. In addition, added a method .every() to StateMatcher that wraps it in the struct for you.
// Before the only option would have been:
.add_systems(Exiting, cleanup.run_if(exiting!(AppState, every _))

// Now you can do:
.add_systems(Exiting, cleanup.run_if((|_: &AppState| true).every())
  • Add CombineStateMatchers(StateMatcher1, StateMatcher2) - this allows you to chain state matchers like you do in the macro. Added .chain(other: StateMatcher<S>) as well.
// Before the only option would have been:
.add_systems(Exiting, cleanup.run_if(exiting!(AppState, InGame(_), every _))

// Now you can do:
.add_systems(Exiting, cleanup.run_if(state_matches!(AppState, AppState::InGame(_)).chain((|_: &AppState| true).every())))

// That is more useful if you have some pre-defined match functions you want to use:
.add_systems(Existing, cleanup.run_if(in_game.chain(always.every()))

// These all do the same thing - if it'll run the cleanup if you're moving from being in game to not being in game, or on any other transition, but NOT moving from one in game state to another
  • Add InvertTransition(StateMatcher) - allowing the ability to invert the focus of a matcher when matching a transition. This means that, when we are Entering - we can swap to make the primary focus of the matcher the outgoing state rather than the incoming one (and vice versa for Exiting). Added a .invert_transition() method as well. These allow you to mimic the transition(from, to) functionality without needing it there, given it's limited use.
.add_systems(Entering, resume_game.run_if(in_game.and_then(paused.invert_transition()))

// That would be equivalent to:
.add_systems(Entering, resume_game.run_if(transitioning(paused, in_game))
  • Remove the Transitioning schedule label (OnTransition still exists, just like OnEnter and OnExit - which rely on strict equality to work). Note that this, as well as the transitioning run condition specifically, might still be worth including for ergonomics. They just aren't functionally necessary.

  • Remove all the run conditions (except in_state for compatibility - it is functionally equivalent to just passing in the state value, but has a more limited set of SystemParams)

  • Macro syntax adjustments - removed the macro!(Expression) variety, since it is not redundant since any valid expression would also be a StateMatcher already. On the other hand, added the ability to use expressions withing the MatcherPattern like so macro!(StateType, =existing_state_matcher). The = is used to differentiate expressions from patterns, which can't always be determined by syntax alone. The addition allows you to chain existing state matchers, patterns, and closures together for a single state_matches! statement.


Migration Guide

This PR makes two breaking changes:

  • NextState<S> moves from being defined as a tuple struct: NextState(Option<S>) to an enum. When using the .set(S) api the behavior will remain consistent, but if you manipulate NextState directly - you'll need to go from NextState(Some(S::Variant)) to NextState::Value(S::Variant)
  • Removing the state_exists_and_equals run condition, as it's behavior is now identical to in_state

lee-orr and others added 30 commits September 27, 2023 22:45
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
…ey treat the State resource as optional, and fail gracefully if it does not exist.

Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
…uleLabels

Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
@alice-i-cecile alice-i-cecile added this to the 0.13 milestone Oct 12, 2023
…ate-support-alternative

Signed-off-by: lee-orr <lee-orr@users.noreply.github.com>
@MiniaczQ
Copy link
Contributor

I'm catching up on the states hierarchy progress.
I think following the Entity design (flat storage, relations) would be a simpler solution, without any macros.
I'll draft something up for comparison.

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Dec 26, 2023

Making all the systems relying on Res<State<S>> and Res<NextState<S>> within bevy_ecs rely on optional versions of the resources if possible

I think we should do this regardless: I'd prefer to split this out into a seperate tiny PR to reduce the scope here.

@lee-orr
Copy link
Contributor Author

lee-orr commented Dec 29, 2023

Making all the systems relying on Res<State<S>> and Res<NextState<S>> within bevy_ecs rely on optional versions of the resources if possible

I think we should do this regardless: I'd prefer to split this out into a seperate tiny PR to reduce the scope here.

Will look into doing that when I have a chance

@lee-orr
Copy link
Contributor Author

lee-orr commented Dec 29, 2023

@alice-i-cecile @MiniaczQ - I also had another potential approach to handling similar things in mind, and will mock something up in the next couple of days. This idea is one that is more likely to be usable as a 3rd party crate as well, though I'll ping y'all when I have that ready.

This was referenced Jan 18, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jan 19, 2024
# Objective

Adjust bevy internals to utilize `Option<Res<State<S>>>` instead of
`Res<State<S>>`, to allow for adding/removing states at runtime and
avoid unexpected panics.

As requested here:
#10088 (comment)

---

## Changelog

- Changed the use of `world.resource`/`world.resource_mut` to
`world.get_resource`/`world.get_resource_mut` in the
`run_enter_schedule` and `apply_state_transition` systems and handled
the `None` option.
- `in_state` now returns a ` FnMut(Option<Res<State<S>>>) -> bool +
Clone`, returning `false` if the resource doesn't exist.
- `state_exists_and_equals` was marked as deprecated, and now just runs
and returns `in_state`, since their bevhaviour is now identical
- `state_changed` now takes an `Option<Res<State<S>>>` and returns
`false` if it does not exist.

I would like to remove `state_exists_and_equals` fully, but wanted to
ensure that is acceptable before doing so.

---------

Co-authored-by: Mike <mike.hsu@gmail.com>
@alice-i-cecile alice-i-cecile removed this from the 0.13 milestone Jan 24, 2024
@lee-orr
Copy link
Contributor Author

lee-orr commented Jan 26, 2024

Closing since #11426 has more traction and supports the same use cases

@lee-orr lee-orr closed this Jan 26, 2024
github-merge-queue bot pushed a commit that referenced this pull request May 2, 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](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs)
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](https://github.com/lee-orr/bevy/blob/derived_state/examples/ecs/sub_states.rs)
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`](ComputedStates::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](https://github.com/lee-orr/bevy/blob/derived_state/examples/ecs/computed_states.rscomputed_states.rs)
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 (#11417), it's fairly easy to add support for explicit
state removal. And while `ComputedStates` can add and remove themselves,
right now `FreelyMutableState`s 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: lee-orr#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. #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.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Marcel Champagne <voiceofmarcel@gmail.com>
Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
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-Feature A new feature, making something new possible X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants