Decoupling State and Stages #1297
Replies: 5 comments 18 replies
-
Handling States with Run CriteriaBy @TheRawMeatball, this code shows a quick draft of how we could implement the From a user's perspective, the API would modify system sets or stages with run criteria, which would be set up to detect whether a State has the correct variant, and when it changes. The critical code is these three functions, designed to be inserted within a run criteria like pub fn on_update(val: T) -> impl System<In = (), Out = ShouldRun> {
let d = discriminant(&val);
Wrapper::new(move |s| discriminant(&s.current) == d)
}
pub fn on_entry(val: T) -> impl System<In = (), Out = ShouldRun> {
let d = discriminant(&val);
Wrapper::new(move |s: &State<T>| s.next().map_or(false, |s| discriminant(s) == d))
}
pub fn on_exit(val: T) -> impl System<In = (), Out = ShouldRun> {
let d = discriminant(&val);
Wrapper::new(move |s: &State<T>| s.next().is_some() && discriminant(&s.current) == d)
} This should allow us to remove the concept of Note: this approach increases the importance of run criteria significantly, making resolving #1295 a much higher priority. |
Beta Was this translation helpful? Give feedback.
-
This comment captures discussion from Discord. @TheRawMeatball found that in order to implement State via To allow for multiple state changes per-frame, all systems need to be in one large SystemSet which can have looping RunCriteria. In response, @Ratysz has proposed a change, relevant to #1144, that allows for nesting systems sets. Summarized, it works as follows:
This results in the following behavior:
These sets will only appear nested in the API, avoiding performance costs. This decouples run criteria from dependencies, improving conceptual clarity. (As a reminder, we need this complex looping behavior in the first place to ensure that State changes are propagated completely in a single tick. A similar problem emerges with UI, see #254.) In order to better support these changes, we need to be able to differentiate between hard dependencies that check whether their prerequisite system ran a) at all this tick, and b) the last time it was checked. Because hard dependencies should be rare relative to soft dependencies (due to intermingling concerns about when and if a system should run), we have preliminary consensus on the following names:
|
Beta Was this translation helpful? Give feedback.
-
A new issue with More details can be found here. |
Beta Was this translation helpful? Give feedback.
-
Some of this discussion is a bit beyond me, particularly in terms of the implementation details (so if there are technical hurdles to these ideas, I wouldn't be familiar with them), but I thought I'd contribute some ideas anyway. As Separately to transitions/events, you want some systems to only run when the global state matches a variant of that state, and this can run in any arbitrary stage, and these systems could have other dependencies. If the smallest unit of conditional execution is a Ideally you would listen to separate events to actually initiate the transitions themselves, but this could be handled in userland. For what this could look like, a contrived (and definitely incorrect) example: /// Our `State`
#[derive(Clone)]
enum TrafficLight {
Red,
Yellow,
Green,
}
/// The `Event` that drives state changes
struct TimerComplete;
/// This system updates the timer, and once the timer completes, sends a `TimerComplete` event.
/// I have assumed `EventWriter<T>` has landed.
fn update_timer_and_send_events(
time: Res<Time>,
mut timer: ResMut<Timer>,
mut timer_events: EventWriter<TimerComplete>,
) {
timer.tick(time.delta_seconds());
if timer.just_finished() {
timer_events.send(TimerComplete);
}
}
/// This system listens to `TimerComplete` events, and queues up a state change *1
/// It also emits a `StateTransitionEvent<TrafficLight>`.
fn handle_timer_events(timer_events: EventReader<TimerComplete>, mut state: ResMut<State<TrafficLight>>) {
for timer_event in timer_events.iter() {
match *state {
TrafficLight::Red => { state.set_next(TrafficLight::Green).unwrap(); }
TrafficLight::Green => { state.set_next(TrafficLight::Yellow).unwrap(); }
TrafficLight::Yellow => { state.set_next(TrafficLight::Red).unwrap(); }
}
}
}
/// *1 Alternatively, an event driven system that does the same thing might look like
fn handle_timer_event(In(_): In<TimerComplete>, mut state: ResMut<State<TrafficLight>>) {
match *state {
TrafficLight::Red => { state.set_next(TrafficLight::Green).unwrap(); }
TrafficLight::Green => { state.set_next(TrafficLight::Yellow).unwrap(); }
TrafficLight::Yellow => { state.set_next(TrafficLight::Red).unwrap(); }
}
}
/// This system runs only once, when we enter a certain state, `TrafficLight::Green`*2, and is specially registered.
fn turn_lights_green(
mut query: Query<&Handle<ColorMaterial>, With<Light>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
for handle in query.iter_mut() {
materials.get_mut(&handle).unwrap().color = Color::GREEN;
}
}
/// *2 Alternatively, an event driven system that does the same thing might look like the below.
/// It's assumed that this only runs when the state has entered `TrafficLight::Green`.
fn turn_lights_green(
In(_): In<StateTransitionEvent<TrafficLight>>,
mut query: Query<&Handle<ColorMaterial>, With<Light>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
for handle in query.iter_mut() {
materials.get_mut(&handle).unwrap().color = Color::GREEN;
}
}
/// *2 Alternatively, the user has to handle it themselves
fn change_traffic_light_color(
In(StateTransitionEvent { _from, to }): In<StateTransitionEvent<TrafficLight>>,
mut query: Query<&Handle<ColorMaterial>, With<Light>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
for handle in query.iter_mut() {
let material = materials.get_mut(&handle).unwrap();
material.color = match to {
TrafficLight::Green => Color::GREEN,
TrafficLight::Yellow=> Color::YELLOW,
TrafficLight::Red => Color::RED,
};
}
}
/// This system runs only within a certain state, `TrafficLight::Green`, and is specially registered.
fn cars_move_forward(time: Res<Time>, mut query: Query<(&mut Transform, &Velocity), With<Car>>) {
let dt = time.delta_seconds();
for (mut transform, velocity) in query.iter_mut() {
transform.translation += velocity.0 * dt;
}
}
/// This system runs only within a certain state, `TrafficLight::Yellow`, and is specially registered.
/// The same system runs when it is in the `TrafficLight::Red` state, and was separately registered.
fn cars_slow_down(time: Res<Time>, mut query: Query<(&mut Transform, &mut Velocity), With<Car>>) {
let dt = time.delta_seconds();
for (mut transform, mut velocity) in query.iter_mut() {
// if it's already past the traffic light at (0,0,0), continue moving
if transform.translation.y >= 0.0 {
transform.translation += velocity.0 * dt;
} else {
// slow down / stop
velocity -= 2.0 * velocity.0.normalize() * dt;
transform.translation += velocity * dt;
}
}
} I don't necessarily believe it's a good API, and I've deliberately left out how you might actually register (order and organise) these systems, but it's an overall approach to state management I'd like us to consider. tl;dr
|
Beta Was this translation helpful? Give feedback.
-
Since #1144 is eventually coming, I implemented a nested set abstraction on top of it which I used to make a It handles multiple state changes per tick without issue, acts mostly like the current API without the burden of an extra stage, and could further be improved by using custom label types if we add such an API for system labels. As one final note, this implementation has a stack model that also helps with #1353, and has an incomplete usage example at https://github.com/TheRawMeatball/drone_attack. |
Beta Was this translation helpful? Give feedback.
-
States in Bevy have two main purposes:
Unlike in other engines, you can have as many types of
State
as you want, each of which gets its own enum and is handled (largely) independently. This comment is a good summary of how States currently work as of Bevy 0.4.Their current implementation has two serious issues:
StateStage
with other work that could safely be handled, prevents work from being done in more appropriate stages, and is generally confusing. This also prevents us from properly handling the intersection of states.Previous Discussion
State was introduced in #1021, and significantly cleaned up in #1059.
This topic was inspired by discussion on Discord (1, 2), with the aim of coming to a conclusion about how to best improve this feature.
Beta Was this translation helpful? Give feedback.
All reactions