5 Commits

Author SHA1 Message Date
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
2 changed files with 87 additions and 82 deletions

View File

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

View File

@@ -15,10 +15,10 @@ pub struct PluginGameMenu;
impl Plugin for PluginGameMenu {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::TitleScreen), spawn_menu)
.add_systems(OnExit(GameState::TitleScreen), despawn::<TitleUI>)
.add_systems(OnExit(GameState::TitleScreen), despawn::<MarkerMainMenu>)
.add_systems(
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;
impl Plugin for PluginGameOver {
@@ -47,11 +47,16 @@ impl Plugin for PluginGameOver {
.add_systems(OnExit(GameState::GameOver), despawn::<MarkerGameOver>)
.add_systems(
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
#[derive(Component)]
struct OnReadySetGo;
@@ -65,8 +70,9 @@ struct MarkerGameOver;
/// Attach this component to a button and [`PluginGameOver`] will use it to
/// decide what to do when that button is pressed.
#[derive(Component)]
enum GameOverMenuAction {
enum ButtonMenuAction {
ToMainMenu,
StartGame,
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) {
commands.spawn((
OnReadySetGo, // marker, so this can be de-spawned properly
@@ -139,48 +201,8 @@ fn spawn_gameover_ui(mut commands: Commands) {
..default()
},
children![
(
Button,
GameOverMenuAction::ToMainMenu,
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,
GameOverMenuAction::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("Quit"),
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)
(button_bundle("Main Menu"), ButtonMenuAction::ToMainMenu,),
(button_bundle("Quit"), ButtonMenuAction::Quit),
],
));
}
@@ -210,19 +232,24 @@ 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
/// place to keep all system logic for this plugin.
fn operate_gameover_ui(
/// Button animation and action handling is done entirely within this system.
///
/// 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<
(
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&GameOverMenuAction,
&ButtonMenuAction,
),
(Changed<Interaction>, With<Button>),
>,
@@ -236,10 +263,13 @@ fn operate_gameover_ui(
*color = UI_BUTTON_PRESSED.into();
border_color.0 = RED.into();
match menu_action {
GameOverMenuAction::ToMainMenu => {
ButtonMenuAction::ToMainMenu => {
game_state.set(GameState::TitleScreen);
}
GameOverMenuAction::Quit => {
ButtonMenuAction::StartGame => {
game_state.set(GameState::Playing);
}
ButtonMenuAction::Quit => {
app_exit_events.write(AppExit::Success);
}
}
@@ -256,32 +286,7 @@ fn operate_gameover_ui(
}
}
// 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 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),
));
});
}
/// Main menu input listener. Starts game when the spacebar is pressed.
fn handle_spacebar(input: Res<ButtonInput<KeyCode>>, mut game_state: ResMut<NextState<GameState>>) {
if input.just_pressed(KeyCode::Space) {
game_state.set(GameState::GetReady);