State management refactor

Have not run this, almost certain things are broken.

The general idea is we mark entities with components and have a general
purpose "OnStateChange" trigger that marks all "active state" entities
as visible and all "inactive state" entities as invisible.

This should simplify state management -- unless of course there are
exceptions which there will be and will require having some sort of
"Sticky<T>" component that says "When this state transition occurs,
don't touch this one entity"
main
Elijah C. Voigt 2 years ago
parent b6cb7bcf13
commit 82b817a614

@ -22,7 +22,7 @@ impl Plugin for AudioPlugin {
}); });
app.init_resource::<AudioVolume>(); app.init_resource::<AudioVolume>();
app.add_systems(OnEnter(GameState::Menu), play_background); app.add_systems(OnEnter(GameState::Intro), play_background);
app.add_systems( app.add_systems(
Update, Update,
( (

@ -5,15 +5,7 @@ pub(crate) struct CreditsPlugin;
impl Plugin for CreditsPlugin { impl Plugin for CreditsPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Startup, init_credits_ui) app.add_systems(Startup, init_credits_ui)
.add_systems( .add_systems(OnEnter(GameState::Credits), update_credits);
Update,
(menu::exit_to_menu.run_if(in_state(GameState::Credits)),),
)
.add_systems(
OnEnter(GameState::Credits),
(update_credits, activate::<Credits>.after(update_credits)),
)
.add_systems(OnExit(GameState::Credits), deactivate::<Credits>);
} }
} }
@ -23,7 +15,7 @@ struct Credits;
fn init_credits_ui(mut commands: Commands) { fn init_credits_ui(mut commands: Commands) {
commands commands
.spawn(( .spawn((
Credits, GameState::Credits,
NodeBundle { NodeBundle {
style: Style { style: Style {
width: Val::Percent(100.0), width: Val::Percent(100.0),

@ -104,11 +104,14 @@ impl Plugin for Display3dPlugin {
.run_if(in_state(GameState::Play)) .run_if(in_state(GameState::Play))
.run_if(in_state(DisplayState::Display3d)), .run_if(in_state(DisplayState::Display3d)),
) )
.add_systems(OnExit(DisplayState::Display3d), deactivate::<Display3d>) // Manage visible/hidden states
.add_systems(
Update,
manage_state_entities::<DisplayState>().run_if(state_changed::<DisplayState>()),
)
.add_systems( .add_systems(
OnEnter(GameState::Play), OnEnter(GameState::Play),
( (
activate::<Display3d>.run_if(in_state(DisplayState::Display3d)),
set_piece_texture.run_if(resource_exists::<tweak::GameTweaks>()), set_piece_texture.run_if(resource_exists::<tweak::GameTweaks>()),
update_tweaks.run_if(resource_exists::<tweak::GameTweaks>()), update_tweaks.run_if(resource_exists::<tweak::GameTweaks>()),
opening_animation opening_animation
@ -119,7 +122,7 @@ impl Plugin for Display3dPlugin {
} }
} }
#[derive(Debug, Component)] #[derive(Debug, Component, PartialEq)]
pub(crate) struct Display3d; pub(crate) struct Display3d;
#[derive(Debug, Component)] #[derive(Debug, Component)]

@ -13,11 +13,13 @@ impl Plugin for GamePlugin {
.insert_resource(Score { ..default() }) .insert_resource(Score { ..default() })
.add_systems(Startup, setup_board) .add_systems(Startup, setup_board)
.add_systems(OnEnter(GameState::Play), hide_valid_moves) .add_systems(OnEnter(GameState::Play), hide_valid_moves)
.add_systems(
Update,
manage_state_entities::<GameState>().run_if(state_changed::<GameState>()),
)
.add_systems( .add_systems(
Update, Update,
( (
menu::exit_to_menu
.run_if(in_state(GameState::Play).or_else(in_state(GameState::Endgame))),
update_board update_board
.run_if(on_event::<Move>()) .run_if(on_event::<Move>())
.after(handle_selection), .after(handle_selection),

@ -14,15 +14,11 @@ impl Plugin for IntroPlugin {
.run_if(run_once()), .run_if(run_once()),
) )
.add_systems(OnEnter(GameState::Intro), manage_intro) .add_systems(OnEnter(GameState::Intro), manage_intro)
.add_systems( .add_systems(OnExit(GameState::Intro), cleanup_intro)
OnExit(GameState::Intro),
(deactivate::<Intro>, cleanup_intro),
)
// All of these run during GameState::Intro // All of these run during GameState::Intro
.add_systems( .add_systems(
Update, Update,
( (
menu::exit_to_menu,
manage_intro.run_if(any_component_removed::<ui::TextScrollAnimation>()), manage_intro.run_if(any_component_removed::<ui::TextScrollAnimation>()),
// Started when the TextScrollAnimation component is added to the parent entity // Started when the TextScrollAnimation component is added to the parent entity
// Updated for as long as there is scrolling text // Updated for as long as there is scrolling text
@ -42,12 +38,12 @@ impl Plugin for IntroPlugin {
} }
} }
#[derive(Debug, Component)]
struct Intro;
#[derive(Debug, Resource)] #[derive(Debug, Resource)]
struct IntroPlayed; struct IntroPlayed;
#[derive(Debug, Component)]
struct IntroUi;
// Draw the intro text (invisible) on startup // Draw the intro text (invisible) on startup
// Requires the Tweakfile to be loaded // Requires the Tweakfile to be loaded
fn init_intro_text( fn init_intro_text(
@ -65,7 +61,7 @@ fn init_intro_text(
commands commands
.spawn(( .spawn((
Intro, IntroUi,
NodeBundle { NodeBundle {
style: Style { style: Style {
width: Val::Percent(100.0), width: Val::Percent(100.0),
@ -85,7 +81,7 @@ fn init_intro_text(
texts.iter().for_each(|text| { texts.iter().for_each(|text| {
parent parent
.spawn(( .spawn((
Intro, IntroUi,
NodeBundle { NodeBundle {
style: Style { style: Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
@ -99,7 +95,7 @@ fn init_intro_text(
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((
Intro, IntroUi,
TextBundle { TextBundle {
text: Text { text: Text {
sections: text sections: text
@ -127,7 +123,7 @@ fn init_intro_text(
fn manage_intro( fn manage_intro(
// Hack, this way of "finding" the root Node containing our paragraphs is precarious // Hack, this way of "finding" the root Node containing our paragraphs is precarious
query: Query<Entity, (With<Node>, With<Intro>, Without<Parent>)>, query: Query<Entity, (With<Node>, With<IntroUi>, Without<Parent>)>,
mut commands: Commands, mut commands: Commands,
) { ) {
info!("Managing intro"); info!("Managing intro");
@ -140,8 +136,8 @@ fn manage_intro(
} }
fn cleanup_intro( fn cleanup_intro(
query: Query<Entity, With<Intro>>, query: Query<Entity, With<IntroUi>>,
mut texts: Query<&mut Text, With<Intro>>, mut texts: Query<&mut Text, With<IntroUi>>,
mut curr: ResMut<CurrentIntroParagraph>, mut curr: ResMut<CurrentIntroParagraph>,
tweaks_file: Res<tweak::GameTweaks>, tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>, tweaks: Res<Assets<tweak::Tweaks>>,
@ -183,7 +179,7 @@ fn manage_scroll_text_animation(
Added<ui::TextScrollAnimation>, Added<ui::TextScrollAnimation>,
)>, )>,
>, >,
texts: Query<Entity, (With<Text>, With<Intro>)>, texts: Query<Entity, (With<Text>, With<IntroUi>)>,
animated_texts: Query<&ui::TextScroll>, animated_texts: Query<&ui::TextScroll>,
parents: Query<&Parent>, parents: Query<&Parent>,
children: Query<&Children>, children: Query<&Children>,

@ -6,18 +6,12 @@ impl Plugin for LoadingPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Startup, initialize) app.add_systems(Startup, initialize)
.add_systems(Update, loading.run_if(in_state(GameState::Loading))) .add_systems(Update, loading.run_if(in_state(GameState::Loading)))
.add_systems( .add_systems(OnEnter(GameState::Loading), enter_loading)
OnEnter(GameState::Loading), .add_systems(OnExit(GameState::Loading), exit_loading);
(activate::<Loading>, enter_loading),
)
.add_systems(
OnExit(GameState::Loading),
(deactivate::<Loading>, exit_loading),
);
} }
} }
#[derive(Debug, Component)] #[derive(Debug, Component, PartialEq)]
struct Loading; struct Loading;
// TODO: Why is this image not showing?? // TODO: Why is this image not showing??
@ -82,6 +76,6 @@ fn loading(
debug!("s {} g {} t {} f {}", s, g, t, f); debug!("s {} g {} t {} f {}", s, g, t, f);
if t { if t {
next_state.set(GameState::Menu) next_state.set(GameState::Intro)
} }
} }

@ -70,7 +70,6 @@ fn main() {
pub enum GameState { pub enum GameState {
#[default] #[default]
Loading, Loading,
Menu,
Credits, Credits,
Intro, Intro,
Play, Play,
@ -90,31 +89,38 @@ fn debug_state<S: States>(state: Res<State<S>>) {
info!("State change {:?}", *state); info!("State change {:?}", *state);
} }
fn activate<Marker: Component>( pub(crate) fn manage_state_entities<Marker>() -> impl FnMut(
mut entities: Query< Query<
&mut Visibility, (&Marker, &mut Visibility),
( (
With<Marker>, With<Marker>,
Without<game::Captured>, Without<game::Captured>,
Without<game::ValidMove>, Without<game::ValidMove>,
), ),
>, >,
) { Res<State<Marker>>,
info!("Activating state"); )
entities.iter_mut().for_each(|mut visibility| { where
*visibility = Visibility::Visible; Marker: Component + States + PartialEq + std::fmt::Debug,
}); {
} move |mut entities: Query<
(&Marker, &mut Visibility),
fn deactivate<Marker: Component>( (
mut entities: Query< With<Marker>,
&mut Visibility, Without<game::Captured>,
(With<Marker>, Without<game::Captured>, Without<GameState>), Without<game::ValidMove>,
),
>, >,
) { curr_state: Res<State<Marker>>| {
entities.iter_mut().for_each(|mut visibility| { info!("Activating state {:?}", *curr_state);
*visibility = Visibility::Hidden; entities.iter_mut().for_each(|(mark, mut visibility)| {
}); *visibility = if mark == curr_state.get() {
Visibility::Inherited
} else {
Visibility::Hidden
};
});
}
} }
pub(crate) fn any_component_changed<C: Component>(q: Query<Entity, Changed<C>>) -> bool { pub(crate) fn any_component_changed<C: Component>(q: Query<Entity, Changed<C>>) -> bool {

@ -1,56 +1,48 @@
use bevy::{
app::AppExit,
input::{keyboard::KeyboardInput, ButtonState},
};
use crate::prelude::*; use crate::prelude::*;
pub(crate) struct MenuPlugin; pub(crate) struct MenuPlugin;
impl Plugin for MenuPlugin { impl Plugin for MenuPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Startup, init_menu_ui) app.add_state::<MenuState>()
// Initialize menus
.add_systems( .add_systems(
Update, OnExit(GameState::Loading),
( (init_play_menu, init_tutorial_menu, init_endgame_menu),
handle_menu_button::<GameState>, // Run in all states
handle_menu_quit.run_if(in_state(GameState::Menu)),
bevy::window::close_on_esc.run_if(in_state(GameState::Menu)),
),
) )
// Manage visible/hidden states
.add_systems( .add_systems(
OnEnter(GameState::Menu), Update,
(activate::<Menu>, activate::<display3d::Display3d>), manage_state_entities::<MenuState>().run_if(state_changed::<MenuState>()),
) );
.add_systems(OnExit(GameState::Menu), deactivate::<Menu>);
} }
} }
#[derive(Debug, Component)] #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)]
struct Menu; enum MenuState {
#[default]
#[derive(Debug, Component)] None,
struct Quit; Play,
Tutorial,
Endgame,
}
fn init_menu_ui(mut commands: Commands) { fn init_menu_ui(mut commands: Commands) {
commands commands
.spawn(( .spawn((NodeBundle {
Menu, style: Style {
NodeBundle { width: Val::Percent(100.0),
style: Style { height: Val::Percent(100.0),
width: Val::Percent(100.0), justify_content: JustifyContent::Center,
height: Val::Percent(100.0), align_items: AlignItems::Center,
justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column,
align_items: AlignItems::Center, position_type: PositionType::Absolute,
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
..default()
},
background_color: Color::NONE.into(),
visibility: Visibility::Hidden,
..default() ..default()
}, },
)) background_color: Color::NONE.into(),
visibility: Visibility::Hidden,
..default()
},))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle::from_section( parent.spawn(TextBundle::from_section(
"M A R T I A N C H E S S", "M A R T I A N C H E S S",
@ -115,71 +107,36 @@ fn init_menu_ui(mut commands: Commands) {
}); });
parent parent
.spawn(( .spawn((ButtonBundle {
Quit, style: Style {
ButtonBundle { padding: UiRect::all(Val::Px(5.0)),
style: Style { margin: UiRect::all(Val::Px(5.0)),
padding: UiRect::all(Val::Px(5.0)),
margin: UiRect::all(Val::Px(5.0)),
..default()
},
background_color: Color::ORANGE.with_a(0.5).into(),
..default() ..default()
}, },
)) background_color: Color::ORANGE.with_a(0.5).into(),
..default()
},))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((TextBundle::from_section(
Quit, "Quit",
TextBundle::from_section( TextStyle {
"Quit", color: Color::BLACK,
TextStyle { font_size: 32.0,
color: Color::BLACK, ..default()
font_size: 32.0, },
..default() ),));
},
),
));
}); });
}); });
} }
pub(crate) fn handle_menu_button<T: Component + States>( fn init_play_menu(mut commands: Commands) {
events: Query<(&Interaction, &T), (With<Button>, Changed<Interaction>)>, todo!("Play Menu");
mut next_gs: ResMut<NextState<T>>,
) {
events.iter().for_each(|(i, gs)| match i {
Interaction::Pressed => {
next_gs.set(gs.clone());
}
_ => (),
});
} }
fn handle_menu_quit( fn init_tutorial_menu(mut commands: Commands) {
events: Query<&Interaction, (With<Quit>, With<Button>, Changed<Interaction>)>, todo!("Tutorial Menu");
mut writer: EventWriter<AppExit>,
) {
events
.iter()
.filter(|&interaction| *interaction == Interaction::Pressed)
.for_each(|_| writer.send(AppExit));
} }
/// Used to return to the menu fn init_endgame_menu(mut commands: Commands) {
pub(crate) fn exit_to_menu( todo!("Endgame Menu");
mut events: EventReader<KeyboardInput>,
mut next_state: ResMut<NextState<GameState>>,
) {
events
.read()
.filter(
|KeyboardInput {
key_code, state, ..
}| {
*key_code == Some(KeyCode::Escape) && *state == ButtonState::Pressed
},
)
.for_each(|_| {
next_state.set(GameState::Menu);
})
} }

@ -14,15 +14,6 @@ impl Plugin for TutorialPlugin {
.run_if(resource_exists::<tweak::GameTweaks>()) .run_if(resource_exists::<tweak::GameTweaks>())
.run_if(run_once()), .run_if(run_once()),
) )
.add_systems(
Update,
// Toggle the tutorial state iff we are in the play state and the tutorial button was pressed
toggle_tutorial_button.run_if(in_state(GameState::Play).and_then(
|buttons: Query<&Interaction, (With<Button>, With<Tutorial>, Changed<Interaction>)>| -> bool {
buttons.iter().any(|i| *i == Interaction::Pressed)
},
)),
)
.add_systems( .add_systems(
Update, Update,
( (
@ -43,8 +34,11 @@ impl Plugin for TutorialPlugin {
), ),
), ),
) )
.add_systems(OnExit(GameState::Play), deactivate::<TutorialState>) // Manage visible/hidden states
.add_systems(OnEnter(GameState::Play), activate::<Tutorial>) .add_systems(
Update,
manage_state_entities::<TutorialState>().run_if(state_changed::<TutorialState>()),
)
.add_systems( .add_systems(
Update, Update,
activate_tutorial_step.run_if( activate_tutorial_step.run_if(
@ -54,9 +48,6 @@ impl Plugin for TutorialPlugin {
} }
} }
#[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)]
pub(crate) struct Tutorial;
#[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)] #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)]
pub(crate) enum TutorialState { pub(crate) enum TutorialState {
#[default] #[default]
@ -96,7 +87,6 @@ fn initialize_tutorial(
commands commands
.spawn(( .spawn((
TutorialState::Intro, // Marks the button to start the tutorial TutorialState::Intro, // Marks the button to start the tutorial
Tutorial, // Marks the button for activate::<Tutorial>
GameState::Play, // Marks the button to be ignored during tutorial step cleanup GameState::Play, // Marks the button to be ignored during tutorial step cleanup
ButtonBundle { ButtonBundle {
style: Style { style: Style {
@ -371,16 +361,3 @@ fn activate_tutorial_step(
} }
}); });
} }
// Toggles the menu on/off by inserting/removing the Tutorial state
fn toggle_tutorial_button(
state: Res<State<TutorialState>>,
mut next_state: ResMut<NextState<TutorialState>>,
) {
match state.get() {
// If inactive, start the intro
TutorialState::None => next_state.set(TutorialState::Intro),
// Else, revert to None state
_ => next_state.set(TutorialState::None),
}
}

Loading…
Cancel
Save