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

Make current state a property of StateStage #1125

Closed
wants to merge 3 commits into from

Conversation

DJMcNab
Copy link
Member

@DJMcNab DJMcNab commented Dec 21, 2020

Extends #1021
Fixes #1117
This also allows avoiding the Clone bound on state

Possible future work:

  • Make state use Eq instead

Extends bevyengine#1021
Fixes bevyengine#1117
This also allows avoiding the Clone bound on state

Possible future work:
- Make state use Eq instead
Maybe this will seem a bit too much like magic to
someone who doesn't know about mem::discriminant? Oh well!

Also fix AppBuilder to not require otherwise unused clones
@memoryruins memoryruins added the A-ECS Entities, components, systems, and events label Dec 22, 2020
@cart
Copy link
Member

cart commented Dec 27, 2020

Hmm this changes things so that State transitions are no longer a global thing with a well defined order of execution. Each StateStage has its own "last state", which can cause confusing outcomes:

enum AppState {
    X,
    Y,
    Z,
}

Res<AppState> = AppState::X

ITERATION 1
------------

StateStageA (current None)

1. on_enter(X)
2. system sets Y
3. on_exit(X)
4. on_enter(Y)
5. on_update(Y)

(StateStageA current is now Y)

StateStageB (current None)

1. on_enter(Y)
2. system sets Z
3. on_exit(Y)
4. on_enter(Z)
5. on_update(Z)

(StateStageB current is now Z)

ITERATION 2
------------

StateStageA (current Y)

1. on_exit(Y)
    * notice that on_exit(Y) is called, despite the fact that we are not currently in AppState::Y
    * (we are currently in AppState::Z)
2. on_enter(Z)

@cart
Copy link
Member

cart commented Dec 27, 2020

Lazy illustrative example:

use bevy::prelude::*;

#[derive(Clone)]
pub enum AppState {
    X,
    Y,
    Z,
}

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_resource(State::new(AppState::X))
        // uncomment the next line and 'enter_system' is not called
        .add_stage_after(
            stage::UPDATE,
            "STAGE_A",
            StateStage::<AppState>::default()
                .with_enter_stage(
                    AppState::X,
                    SystemStage::parallel()
                        .with_system(a.system())
                        .with_system(a_x_enter.system()),
                )
                .with_update_stage(AppState::X, SystemStage::parallel().with_system(a.system()))
                .with_exit_stage(AppState::X, SystemStage::parallel().with_system(a.system()))
                .with_enter_stage(AppState::Y, SystemStage::parallel().with_system(a.system()))
                .with_update_stage(AppState::Y, SystemStage::parallel().with_system(a.system()))
                .with_exit_stage(AppState::Y, SystemStage::parallel().with_system(a.system()))
                .with_enter_stage(AppState::Z, SystemStage::parallel().with_system(a.system()))
                .with_update_stage(AppState::Z, SystemStage::parallel().with_system(a.system()))
                .with_exit_stage(AppState::Z, SystemStage::parallel().with_system(a.system())),
        )
        .add_stage_after(
            "STAGE_A",
            "STAGE_B",
            StateStage::<AppState>::default()
                .with_enter_stage(AppState::X, SystemStage::parallel().with_system(b.system()))
                .with_update_stage(AppState::X, SystemStage::parallel().with_system(b.system()))
                .with_exit_stage(AppState::X, SystemStage::parallel().with_system(b.system()))
                .with_enter_stage(
                    AppState::Y,
                    SystemStage::parallel()
                        .with_system(b.system())
                        .with_system(b_y_enter.system()),
                )
                .with_update_stage(AppState::Y, SystemStage::parallel().with_system(b.system()))
                .with_exit_stage(AppState::Y, SystemStage::parallel().with_system(b.system()))
                .with_enter_stage(AppState::Z, SystemStage::parallel().with_system(b.system()))
                .with_update_stage(AppState::Z, SystemStage::parallel().with_system(b.system()))
                .with_exit_stage(AppState::Z, SystemStage::parallel().with_system(b.system())),
        )
        .add_system_to_stage(stage::LAST, end.system())
        .run();
}

fn a_x_enter(mut state: ResMut<State<AppState>>) {
    state.set_next(AppState::Y).unwrap();
}
fn b_y_enter(mut state: ResMut<State<AppState>>) {
    state.set_next(AppState::Z).unwrap();
}
fn a() {
    println!("  a")
}
fn b() {
    println!("  b")
}

fn end(mut x: Local<usize>) {
    *x+=1;

    if *x == 2 {
        panic!("end");
    }
}
// modify state.rs to print each state
#[allow(clippy::mem_discriminant_non_enum)]
impl<T: Resource> Stage for StateStage<T> {
    fn run(&mut self, world: &mut World, resources: &mut Resources) {
        let current_stage = loop {
            let next = {
                let mut state = resources
                    .get_mut::<State<T>>()
                    .expect("Missing state resource");
                state.previous = state.apply_next().or_else(|| state.previous.take());
                mem::discriminant(&state.current)
            };
            if self.current_stage == Some(next) {
                break next;
            } else {
                let cur = self.current_stage.map(|i| i);
                if let Some(current_state_stages) =
                    self.current_stage.and_then(|it| self.stages.get_mut(&it))
                {
                    println!("exit {:?}", cur.unwrap());
                    current_state_stages.exit.run(world, resources);
                }
                self.current_stage = Some(next);
                if let Some(next_state_stages) = self.stages.get_mut(&next) {
                    println!("enter {:?}", next);
                    next_state_stages.enter.run(world, resources);
                }
            }
        };

        if let Some(current_state_stages) = self.stages.get_mut(&current_stage) {
            println!("update {:?}", current_stage);
            current_state_stages.update.run(world, resources);
        }
    }
}

@DJMcNab
Copy link
Member Author

DJMcNab commented Jan 5, 2021

My mental model is that each StateStage is standalone, and on_state_enter and on_state_exit's roles are to do setup and teardown for that State's on_update stage, within the single StateStage. In your example, my code makes it so that each on_enter has a corresponding on_exit, and no unecessary on_enter/on_exits are called.

That is, I would say that your unexpected behaviour is exactly what I would expect from StateStage.

@cart
Copy link
Member

cart commented Jan 8, 2021

Hmm yeah thats a reasonable take. I think whatever solution we land on needs to be reconciled with the upcoming SystemSet::with_run_criteria (#1144). I think it can be accomplished with a new OnStateEnter::<T> system/criteria, which could internally store the state info, but we should spec that out. Handling run_criteria correctly for states felt "easier" with a global view of state because we could just expose that information directly on the State datatype.

@Ratysz
Copy link
Contributor

Ratysz commented Feb 17, 2021

This is probably superseded by #1424.

@DJMcNab DJMcNab closed this Feb 17, 2021
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

'enter' stage not called in some cases
5 participants