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.add_systems(OnEnter(GameState::Menu), play_background);
app.add_systems(OnEnter(GameState::Intro), play_background);
app.add_systems(
Update,
(

@ -5,15 +5,7 @@ pub(crate) struct CreditsPlugin;
impl Plugin for CreditsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, init_credits_ui)
.add_systems(
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>);
.add_systems(OnEnter(GameState::Credits), update_credits);
}
}
@ -23,7 +15,7 @@ struct Credits;
fn init_credits_ui(mut commands: Commands) {
commands
.spawn((
Credits,
GameState::Credits,
NodeBundle {
style: Style {
width: Val::Percent(100.0),

@ -104,11 +104,14 @@ impl Plugin for Display3dPlugin {
.run_if(in_state(GameState::Play))
.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(
OnEnter(GameState::Play),
(
activate::<Display3d>.run_if(in_state(DisplayState::Display3d)),
set_piece_texture.run_if(resource_exists::<tweak::GameTweaks>()),
update_tweaks.run_if(resource_exists::<tweak::GameTweaks>()),
opening_animation
@ -119,7 +122,7 @@ impl Plugin for Display3dPlugin {
}
}
#[derive(Debug, Component)]
#[derive(Debug, Component, PartialEq)]
pub(crate) struct Display3d;
#[derive(Debug, Component)]

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

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

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

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

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

@ -14,15 +14,6 @@ impl Plugin for TutorialPlugin {
.run_if(resource_exists::<tweak::GameTweaks>())
.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(
Update,
(
@ -43,8 +34,11 @@ impl Plugin for TutorialPlugin {
),
),
)
.add_systems(OnExit(GameState::Play), deactivate::<TutorialState>)
.add_systems(OnEnter(GameState::Play), activate::<Tutorial>)
// Manage visible/hidden states
.add_systems(
Update,
manage_state_entities::<TutorialState>().run_if(state_changed::<TutorialState>()),
)
.add_systems(
Update,
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)]
pub(crate) enum TutorialState {
#[default]
@ -96,7 +87,6 @@ fn initialize_tutorial(
commands
.spawn((
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
ButtonBundle {
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