7 Commits

Author SHA1 Message Date
51e6989ef4 Swap button colors to be less upsetting
Making everything gray is still boring as dirt, but at least it doesn't
look like debug info.
2025-08-14 08:55:13 -05:00
2ffc0e8861 Finish main-menu button handling
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m8s
2025-08-13 17:25:26 -05:00
d15b96ef48 Add "start" and "quit" buttons to title screen
I'm stealing the game-over plugin's button handling for use in the main
menu. I guess it turns out that my widgets are more generic than
intended.
2025-08-13 17:00:29 -05:00
0aefc96f7a Center the text, add a drop shadow 2025-08-13 16:03:40 -05:00
4102446e90 Rename main menu marker to "MarkerMainMenu" 2025-08-13 15:38:53 -05:00
f4df2ae33a Rearrange the main menu components & systems
I'm trying to keep things somewhat in order. Plugins, then components,
then systems. Within those, they're roughly ordered by game state.
2025-08-13 15:36:14 -05:00
fc43be0777 Hook up the action selectors to the buttons
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m8s
2025-08-13 14:22:00 -05:00
3 changed files with 104 additions and 83 deletions

View File

@@ -7,7 +7,7 @@ pub const WINDOW_SIZE: bevy::prelude::Vec2 = bevy::prelude::Vec2::new(800.0, 600
pub const UI_BUTTON_NORMAL: Color = Color::srgb(0.15, 0.15, 0.15); // Button color when it's just hanging out pub const UI_BUTTON_NORMAL: Color = Color::srgb(0.15, 0.15, 0.15); // Button color when it's just hanging out
pub const UI_BUTTON_HOVERED: Color = Color::srgb(0.25, 0.25, 0.25); // ... when it's hovered pub const UI_BUTTON_HOVERED: Color = Color::srgb(0.25, 0.25, 0.25); // ... when it's hovered
pub const UI_BUTTON_PRESSED: Color = Color::srgb(0.35, 0.75, 0.35); // ... when it's pressed pub const UI_BUTTON_PRESSED: Color = Color::srgb(0.55, 0.55, 0.55); // ... when it's pressed
pub(crate) const BACKGROUND_COLOR: Color = Color::srgb(0.3, 0.3, 0.3); pub(crate) const BACKGROUND_COLOR: Color = Color::srgb(0.3, 0.3, 0.3);
pub(crate) const PLAYER_SHIP_COLOR: Color = Color::srgb(1.0, 1.0, 1.0); pub(crate) const PLAYER_SHIP_COLOR: Color = Color::srgb(1.0, 1.0, 1.0);

View File

@@ -81,7 +81,7 @@ impl Plugin for AsteroidPlugin {
.add_event::<events::AsteroidDestroy>() .add_event::<events::AsteroidDestroy>()
.add_event::<events::ShipDestroy>() .add_event::<events::ShipDestroy>()
.add_event::<events::BulletDestroy>(); .add_event::<events::BulletDestroy>();
app.insert_state(GameState::GameOver); app.insert_state(GameState::TitleScreen);
} }
} }

View File

@@ -5,7 +5,7 @@ use crate::{
}; };
use bevy::{ use bevy::{
color::palettes::css::{BLACK, GREEN, LIGHT_BLUE, RED, WHITE}, color::palettes::css::{BLACK, DARK_GRAY, GREEN, LIGHT_BLUE, RED, WHITE},
prelude::*, prelude::*,
}; };
@@ -15,10 +15,10 @@ pub struct PluginGameMenu;
impl Plugin for PluginGameMenu { impl Plugin for PluginGameMenu {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::TitleScreen), spawn_menu) app.add_systems(OnEnter(GameState::TitleScreen), spawn_menu)
.add_systems(OnExit(GameState::TitleScreen), despawn::<TitleUI>) .add_systems(OnExit(GameState::TitleScreen), despawn::<MarkerMainMenu>)
.add_systems( .add_systems(
Update, Update,
handle_spacebar.run_if(in_state(GameState::TitleScreen)), (handle_spacebar, operate_buttons).run_if(in_state(GameState::TitleScreen)),
); );
} }
} }
@@ -38,7 +38,7 @@ impl Plugin for PluginGetReady {
} }
} }
/// Plugin for the game-over screen (TODO) /// Plugin for the game-over screen
pub struct PluginGameOver; pub struct PluginGameOver;
impl Plugin for PluginGameOver { impl Plugin for PluginGameOver {
@@ -47,11 +47,16 @@ impl Plugin for PluginGameOver {
.add_systems(OnExit(GameState::GameOver), despawn::<MarkerGameOver>) .add_systems(OnExit(GameState::GameOver), despawn::<MarkerGameOver>)
.add_systems( .add_systems(
Update, Update,
operate_gameover_ui.run_if(in_state(GameState::GameOver)), operate_buttons.run_if(in_state(GameState::GameOver)),
); );
} }
} }
// Marker component for the title screen UI entity.
// This way, a query for the TitleUI can be used to despawn the title screen
#[derive(Component)]
struct MarkerMainMenu;
/// Marker component for things on the get-ready indicator /// Marker component for things on the get-ready indicator
#[derive(Component)] #[derive(Component)]
struct OnReadySetGo; struct OnReadySetGo;
@@ -65,8 +70,9 @@ struct MarkerGameOver;
/// Attach this component to a button and [`PluginGameOver`] will use it to /// Attach this component to a button and [`PluginGameOver`] will use it to
/// decide what to do when that button is pressed. /// decide what to do when that button is pressed.
#[derive(Component)] #[derive(Component)]
enum GameOverMenuAction { enum ButtonMenuAction {
ToMainMenu, ToMainMenu,
StartGame,
Quit, Quit,
} }
@@ -90,6 +96,62 @@ fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<
} }
} }
/// Utility function for creating a standard button.
fn button_bundle(text: &str) -> impl Bundle {
(
Button,
// TODO: Generic action
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(2.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
margin: UiRect::all(Val::Px(5.0)),
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(UI_BUTTON_NORMAL),
children![(
Text::new(text),
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)],
)
}
fn spawn_menu(mut commands: Commands) {
commands
.spawn((
MarkerMainMenu,
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
..Default::default()
},
))
.with_children(|cmds| {
cmds.spawn((
Text::new("Robert's Bad Asteroids Game"),
TextFont::from_font_size(50.0),
TextLayout::new_with_justify(JustifyText::Center),
TextShadow::default(),
));
cmds.spawn((
Text::new("Press space to begin"),
TextFont::from_font_size(20.0),
TextColor(Color::srgb(0.7, 0.7, 0.7)),
TextShadow::default(),
));
cmds.spawn((button_bundle("Start Game"), ButtonMenuAction::StartGame));
cmds.spawn((button_bundle("Quit"), ButtonMenuAction::Quit));
});
}
fn spawn_get_ready(mut commands: Commands) { fn spawn_get_ready(mut commands: Commands) {
commands.spawn(( commands.spawn((
OnReadySetGo, // marker, so this can be de-spawned properly OnReadySetGo, // marker, so this can be de-spawned properly
@@ -139,46 +201,8 @@ fn spawn_gameover_ui(mut commands: Commands) {
..default() ..default()
}, },
children![ children![
( (button_bundle("Main Menu"), ButtonMenuAction::ToMainMenu,),
Button, (button_bundle("Quit"), ButtonMenuAction::Quit),
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(2.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
margin: UiRect::all(Val::Px(5.0)),
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(UI_BUTTON_NORMAL),
children![(
Text::new("Main Menu"),
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
),
(
Button,
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
border: UiRect::all(Val::Px(2.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
margin: UiRect::all(Val::Px(5.0)),
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(UI_BUTTON_NORMAL),
children![(
Text::new("Quit"),
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)
], ],
)); ));
} }
@@ -208,25 +232,47 @@ fn animate_get_ready_widget(
} }
} }
/// Handles interaction and performs updates to the game-over UI. /// Handles interactions with the menu buttons.
/// ///
/// Used by [`PluginGameOver`] while in the [`GameState::GameOver`] state. /// The buttons are used by the main menu and the game-over menu to change
/// between game states.
/// ///
/// Mostly a button input handler, but it also makes for a convenient single /// Button animation and action handling is done entirely within this system.
/// place to keep all system logic for this plugin. ///
fn operate_gameover_ui( /// There are no checks for current state. If a "quit" button was put somewhere
/// on the HUD, this system would quit the game. The same will happen for
/// returning to the title screen. This should be useful for making a pause
/// menu, too.
fn operate_buttons(
mut interactions: Query< mut interactions: Query<
(&Interaction, &mut BackgroundColor, &mut BorderColor), (
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&ButtonMenuAction,
),
(Changed<Interaction>, With<Button>), (Changed<Interaction>, With<Button>),
>, >,
mut game_state: ResMut<NextState<GameState>>,
mut app_exit_events: EventWriter<AppExit>,
) { ) {
// TODO: Better colors. These are taken from the example and they're ugly. // TODO: Better colors. These are taken from the example and they're ugly.
// TODO: Read the menu action enum component and take that action. for (interaction, mut color, mut border_color, menu_action) in &mut interactions {
for (interaction, mut color, mut border_color) in &mut interactions {
match *interaction { match *interaction {
Interaction::Pressed => { Interaction::Pressed => {
*color = UI_BUTTON_PRESSED.into(); *color = UI_BUTTON_PRESSED.into();
border_color.0 = RED.into(); border_color.0 = DARK_GRAY.into();
match menu_action {
ButtonMenuAction::ToMainMenu => {
game_state.set(GameState::TitleScreen);
}
ButtonMenuAction::StartGame => {
game_state.set(GameState::Playing);
}
ButtonMenuAction::Quit => {
app_exit_events.write(AppExit::Success);
}
}
} }
Interaction::Hovered => { Interaction::Hovered => {
*color = UI_BUTTON_HOVERED.into(); *color = UI_BUTTON_HOVERED.into();
@@ -240,32 +286,7 @@ fn operate_gameover_ui(
} }
} }
// Marker component for the title screen UI entity. /// Main menu input listener. Starts game when the spacebar is pressed.
// This way, a query for the TitleUI can be used to despawn the title screen
#[derive(Component)]
struct TitleUI;
fn spawn_menu(mut commands: Commands) {
commands
.spawn((
TitleUI,
Node {
flex_direction: FlexDirection::Column,
..Default::default()
},
))
.with_children(|cmds| {
cmds.spawn((
Text::new("Robert's Bad Asteroids Game"),
TextFont::from_font_size(50.0),
));
cmds.spawn((
Text::new("Press space to begin"),
TextFont::from_font_size(40.0),
));
});
}
fn handle_spacebar(input: Res<ButtonInput<KeyCode>>, mut game_state: ResMut<NextState<GameState>>) { fn handle_spacebar(input: Res<ButtonInput<KeyCode>>, mut game_state: ResMut<NextState<GameState>>) {
if input.just_pressed(KeyCode::Space) { if input.just_pressed(KeyCode::Space) {
game_state.set(GameState::GetReady); game_state.set(GameState::GetReady);