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

Add remaining State Chart functionalities to States #1597

Closed
alice-i-cecile opened this issue Mar 8, 2021 · 6 comments
Closed

Add remaining State Chart functionalities to States #1597

alice-i-cecile opened this issue Mar 8, 2021 · 6 comments
Labels
A-ECS Entities, components, systems, and events C-Usability A simple quality-of-life change that makes Bevy easier to use S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged

Comments

@alice-i-cecile
Copy link
Member

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

State charts are a popular and powerful abstraction for working with complex state machines. The State implementation created in #1424 is quite good, but there's still a few missing gaps that will present issues with more complex logic and hinder learning for users familiar with other state charts.

What solution would you like?

From my initial understanding of #1424 and the link above, we're missing the following features:

  1. Better nested states, including history states
  2. State guards
  3. Delayed transitions

State guards should be fairly trivial, and delayed transitions can likely just be sugar written in terms of state guards.

What alternative(s) have you considered?

Discover and document alternate patterns to work around these patterns.

Additional context

Integration with XML specs of state charts seems like a non-goal.

We could consider adapting some of the formal state chart jargon as well (e.g. Activity instead of on_update) but I don't think it's worth the clarity loss.

Eventually it would be lovely to have a tool to visualize (and eventually manipulate) state charts used in Bevy, but that's out-of-scope for this issue.

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A simple quality-of-life change that makes Bevy easier to use labels Mar 8, 2021
@zaszi
Copy link
Contributor

zaszi commented Mar 12, 2021

I took an in-depth look at #1424 and tried to figure out how much functionality were are actually missing here. In addition to what @alice-i-cecile said in the OP, we need the following features to be considered a somewhat complete implementation of state charts (at least in the context of Bevy). I approached the problem space mostly from userland, so perhaps others can fill in the required implementation considerations where necessary.

In order to cut down on the size of code blocks a bit, assume the following omissions. Additionally, I included no explicit state changes.

use bevy::prelude::*;

fn a() {
    println!("a");
}

fn b() {
    println!("b");
}

Nested states

Nested states are perfectly doable, and much more ergonomic than before:

#[derive(Clone, Eq, PartialEq, Debug)]
enum GameState {
    MyState1(NestedGameState),
}

#[derive(Clone, Eq, PartialEq, Debug)]
enum NestedGameState {
    MyNestedState1,
    MyNestedState2,
}

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_state(GameState::MyState1(NestedGameState::MyNestedState1))
        .add_system_set(
            State::on_update_set(GameState::MyState1(NestedGameState::MyNestedState1))
                .with_system(a.system()),
        )
        .add_system_set(
            State::on_update_set(GameState::MyState1(NestedGameState::MyNestedState2))
                .with_system(b.system()),
        )
        .run();
}

// Output in MyState1 -> MyNestedState1: aaaaaaaaaa...
// Output in MyState1 -> MyNestedState2: bbbbbbbbbb...

This still results in a lot of boilerplate, however, should you want the same systems running over multiple substates. Something like this would be much desirable:

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_state(GameState::MyState1(NestedGameState::MyNestedState1))
        .add_system_set(
            // We want a.system() to run in both substates.
            State::on_update_set(GameState::MyState1(..))
                .with_system(a.system()),
        )
        .run();
}

// Expected output in MyState1 -> MyNestedState1: aaaaaaaaaa...
// Expected output in MyState1 -> MyNestedState2: aaaaaaaaaa...

Parallel states

It can't get any easier than this:

#[derive(Clone, Eq, PartialEq, Debug)]
enum GameState {
    MyState1,
}

#[derive(Clone, Eq, PartialEq, Debug)]
enum ParallelState {
    MyParallelState1,
}

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_state(GameState::MyState1)
        .add_state(ParallelState::MyParallelState1)
        .add_system_set(State::on_update_set(GameState::MyState1).with_system(a.system()))
        .add_system_set(State::on_update_set(ParallelState::MyParallelState1).with_system(b.system()))
        .run();
}

// Output in MyState1 + MyParallelState1: ababababab...

Problems arise when trying to tie a parallel state to a nested state. We should be able to do something like this:

#[derive(Clone, Eq, PartialEq, Debug)]
enum GameState {
    MyState1(NestedState),
    MyState2,
}

#[derive(Clone, Eq, PartialEq, Debug)]
enum NestedState {
    MyNestedState1,
}

#[derive(Clone, Eq, PartialEq, Debug)]
enum ParallelState {
    MyParallelState1,
}

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_state(GameState::MyState1(NestedState::MyNestedState1))
        .add_state_to_state(ParallelState::MyParallelState1, GameState::MyState1(..)) // Add this only to MyState1 somehow.
        .add_system_set(
            State::on_update_set(GameState::MyState1(NestedState::MyNestedState1))
                .with_system(a.system()),
        )
        .add_system_set(State::on_update_set(ParallelState::MyParallelState1).with_system(b.system()))
        .run();
}

// Expected output in MyState1 -> MyNestedState1 + MyParallelState1: ababababab...
// Expected Output in MyState2: aaaaaaaaaa...

State guards

The go-to way to switch states right now is to queue a state change event, just like before:

app_state.set_next(GameState::MyState1).unwrap();

Guards would expand on this and allow a pre-determined way to allow or prohibit state switches based on a condition at runtime. Just like the above, I don't think this needs much more than something like:

app_state.set_next_with_guard(GameState::MyState1, some_condition_type).unwrap();

Then again, this is arguably not much better than simply surrounding it with an if-statement, so I'm curious as to what people think are actually viable options here. Technically we can already do this with the tools we have in a fairly ergonomic way.

Delayed transitions

While we can definitely make use of a guard implemenation as per above, this is not as simple as merely guarding a state change. We need some kind of stateful construct here to at least keep track of elapsed time. This can perhaps be implemented with a built-in parallel state with a configurable duration. In the API we should aim for something simple, though:

use std::time::Duration;

app_state.set_next_with_delay(GameState::MyState1, Duration::from_seconds(1)).unwrap();

Automatic transitions

This is trickier since this requires us to implement some additional internal system that continuously checks for these conditions at runtime. My Bevy internals knowledge is limited, so perhaps other people can chime in here. API-wise this probably belongs in the application builder instead, like so:

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_state(GameState::MyState1)
        // parameters: current state, desired state, condition
        .add_state_transition(GameState::MyState1, GameState::MyState2, some_condition_type)
        .run();
}

History states

This still leaves history states but if I recall correctly @TheRawMeatball already had some ideas going?

@alice-i-cecile
Copy link
Member Author

State Guards

Then again, this is arguably not much better than simply surrounding it with an if-statement, so I'm curious as to what people think are actually viable options here. Technically we can already do this with the tools we have in a fairly ergonomic way.

IMO, if we can get away with it, we should stick to native Rust constructs like if-statements. I would only add a dedicated state guard construct if we need to somehow work with it at a higher level than we can manage otherwise. I might see this being needed for visualization or drag-and-drop state chart code generation for example.

Delayed Transitions

You'll almost certainly want both real-time and tick-time support here. I'll make a quick issue for bringing bevy_tick_timers by @maplant in tree.

We need some kind of stateful construct here to at least keep track of elapsed time.

For this, you should be able to use Bevy's native Time construct and perhaps system-local state, just like ordinary game systems.

Automatic Transitions

I don't expect us to need special functionality for this. Between scheduling an ordinary system to check this and chaining it into other state's on-enter logic this should Just Work.

History States

We'll want to leverage the stack created in some way I expect. You may need to store a phantom copy of each substate? I need to think more about this...

@alice-i-cecile
Copy link
Member Author

A related idea that could possibly be implemented in terms of state charts: app state persistence for node-based UIs.

@alice-i-cecile
Copy link
Member Author

Related to this work: state charts are very useful at an entity level as well (such as for animation sequences). When designing this we should be thoughtful about how we can reuse logic and the API across both levels of state charts.

@alice-i-cecile alice-i-cecile added the S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged label Dec 12, 2021
@alice-i-cecile
Copy link
Member Author

Blocked on States redesign, see #2801.

@alice-i-cecile
Copy link
Member Author

This is better tackled in an external library, keeping the built-in states simple.

@alice-i-cecile alice-i-cecile closed this as not planned Won't fix, can't repro, duplicate, stale Feb 16, 2023
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-Usability A simple quality-of-life change that makes Bevy easier to use S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged
Projects
Archived in project
Development

No branches or pull requests

2 participants