Skip to content

Sub State Support #9942

@lee-orr

Description

@lee-orr

What problem does this solve or what need does it fill?

At this point in time, all states are entirely distinct from one another, and exclusivity between them is handled on a type-by-type basis. This is great in situations where I might want two independent state types - such as having a Walking | Running | Jumping state and a Casual | Tense | Danger state. However, sometimes I might have a set of states that can only occur when I'm within another state - for example Playing | Paused only makes sense if I'm InGame not if I'm in MainMenu.

Right now, the general approach (as outlined here: https://discord.com/channels/691052431525675048/1156638003029082254) is:

  1. create a new State type with a "None" variant.
  2. upon entering the parent state, you would set the state to the actual "Default" you want.
  3. upon exiting the parent state, you would set the state to "None".

However, this does create some issues... For example, I could accidentally set the internal state type to something other than None outside of the valid parent state, which could lead to systems relying on that state running when they shouldn't.

Another potential issue is that I can forget to "reset" the state properly - which might lead to situations where I start on the wrong sub-state.

The other potential solution is to manually flatten the hierarchy into a single state with many variants. (So MainMenu | InGamePlaying | InGamePaused), but that will mean that systems that should run every time we're in game will have to be defined to work in both InGamePlaying and InGamePaused, but enter/exit triggers should not adjust things between those two states... all of which is harder to define the logic for when adding systems to the app.

What solution would you like?

Adding a capacity to optionally add Parent States to a state when you define it, Potentially something like:

#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum RootState {
  #[default]
  Loading,
  Menu,
  InGame,
  InMiniGame
}

#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
#[parent_states(RootState::Menu)]
pub enum Menus {
  #[default]
Main,
Options
}

#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
#[parent_states(RootState::InGame, RootState::InMiniGame)]
pub enum Pause {
#[default] 
  Playing,
  Paused
}

Internally, the State resource would either have an optional state set to none or would fully not exist when a valid parent state isn't active.

What alternative(s) have you considered?

  • Adding an .add_sub_state<State, Parent>(Parent::Variant) function to app for defining sub states, and using that to define a different set of systems related to changing state, triggering schedules, and/or "in_state". The main undesirable point here is that it forces a fully separate but compatible API for sub state's compared to normal states.

  • Adjusting the "State" macro to define sub-states as values in a variant, and then set up all the change state/schedules/etc to rely on matching rather than value comparison. So for example:

enum MyState {
  Loading,
  Menu(MenuType),
  InGame(Movement, Pause),
  InMiniGame(Pause)
}

enum MenuType {
  MainMenu,
  Options,
}

enum Movement {
  Walking,
  Running
}

enum Pause {
  Playing,
  Paused
}

This causes a few issues. First - you can have an issue of combinatorial explosion on variants if you allow multiple sub states per state. Second - you need to bring all sub states into the definition of their parent state. And third, each sub-state can only have a single parent state, since a match against InGame(_,Pause::Paused) won't match `InMiniGame(Pause::Paused). So in actuality, it is treated as though it were two separate states.

Additional context

The previous model of a stack based state system would have supported some of these use cases, but had the issue of many invalid states still being possible. This approach would actively prevent invalid state combinations from occurring in game, and would be able to flag if there was a situation where an attempt was made to set an invalid state, without sacrificing the flexibility of the current state system.

Also of note - I would like to work on this, I just wanted to raise this proposal for discussion before I actually start to make sure there's a chance for people to comment on it and avoid me wasting time attempting to implement this if there's reason not to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-FeatureA new feature, making something new possible

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions