-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Comments
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 statesNested 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 statesIt 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 guardsThe 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 transitionsWhile 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 transitionsThis 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 statesThis still leaves history states but if I recall correctly @TheRawMeatball already had some ideas going? |
State Guards
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.
For this, you should be able to use Bevy's native 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... |
A related idea that could possibly be implemented in terms of state charts: app state persistence for node-based UIs. |
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. |
Blocked on States redesign, see #2801. |
This is better tackled in an external library, keeping the built-in states simple. |
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:
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.
The text was updated successfully, but these errors were encountered: