5 Commits

Author SHA1 Message Date
a74b99deb4 Visual button interaction is working, needs act
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m22s
2025-08-13 12:59:38 -05:00
2f9401e93f Begin GameOver scene, widget spawner works
I've taken a lot directly from the Bevy UI button example.
(https://bevy.org/examples/ui-user-interface/button/)

I'll make it look better later. For now, it just needs to exist. Onward
to the UI operation system!
2025-08-13 11:58:11 -05:00
54ef257ab4 Make generic UI despawning function 2025-08-13 11:56:35 -05:00
69bef24913 Collect GUI plugins at the top of the module
I want the different "scenes" to be their own plugins for ease of setup
and reading.

The main menu plugin has been renamed to have "Plugin" first. This is so
the lexical sort in the docs places all the plugins next to each other.

The "get-ready" plugin has been given an empty struct and an
`impl Plugin` to match the main menu plugin. I've started the game over
scene, but left it unimplemented.
2025-08-13 11:09:48 -05:00
df4479bf49 Remove collision debug print system 2025-08-13 10:33:59 -05:00
3 changed files with 165 additions and 48 deletions

View File

@@ -5,6 +5,10 @@ use bevy::color::Color;
pub const WINDOW_SIZE: bevy::prelude::Vec2 = bevy::prelude::Vec2::new(800.0, 600.0); pub const WINDOW_SIZE: bevy::prelude::Vec2 = bevy::prelude::Vec2::new(800.0, 600.0);
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_PRESSED: Color = Color::srgb(0.35, 0.75, 0.35); // ... 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);
pub(crate) const SHIP_THRUSTER_COLOR_ACTIVE: Color = Color::srgb(1.0, 0.2, 0.2); pub(crate) const SHIP_THRUSTER_COLOR_ACTIVE: Color = Color::srgb(1.0, 0.2, 0.2);

View File

@@ -13,7 +13,7 @@ mod widgets;
use crate::config::{ use crate::config::{
ASTEROID_SMALL_COLOR, BACKGROUND_COLOR, BULLET_COLOR, BULLET_LIFETIME, BULLET_SPEED, ASTEROID_SMALL_COLOR, BACKGROUND_COLOR, BULLET_COLOR, BULLET_LIFETIME, BULLET_SPEED,
PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST, SHIP_THRUSTER_COLOR_ACTIVE, PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST, SHIP_THRUSTER_COLOR_ACTIVE,
SHIP_THRUSTER_COLOR_INACTIVE, WINDOW_SIZE, SHIP_THRUSTER_COLOR_INACTIVE,
}; };
use crate::machinery::AsteroidSpawner; use crate::machinery::AsteroidSpawner;
use crate::objects::{Bullet, Ship}; use crate::objects::{Bullet, Ship};
@@ -34,8 +34,9 @@ pub struct AsteroidPlugin;
impl Plugin for AsteroidPlugin { impl Plugin for AsteroidPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins(( app.add_plugins((
widgets::GameMenuPlugin, widgets::PluginGameMenu,
widgets::preparation_widget_plugin, widgets::PluginGameOver,
widgets::PluginGetReady,
RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(10.0), RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(10.0),
RapierDebugRenderPlugin::default(), RapierDebugRenderPlugin::default(),
)) ))
@@ -64,8 +65,6 @@ impl Plugin for AsteroidPlugin {
objects::bullet_impact_listener, objects::bullet_impact_listener,
objects::ship_impact_listener, objects::ship_impact_listener,
physics::collision_listener, physics::collision_listener,
// TODO: Remove debug printing
debug_collision_event_printer,
machinery::tick_lifetimes, machinery::tick_lifetimes,
) )
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
@@ -82,13 +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::Playing); app.insert_state(GameState::GameOver);
}
}
fn debug_collision_event_printer(mut collision_events: EventReader<CollisionEvent>) {
for event in collision_events.read() {
dbg!(event);
} }
} }

View File

@@ -1,34 +1,75 @@
use crate::{ use crate::{
GameState, GameState,
config::{UI_BUTTON_HOVERED, UI_BUTTON_NORMAL, UI_BUTTON_PRESSED},
resources::{Lives, Score}, resources::{Lives, Score},
}; };
use bevy::{ use bevy::{
color::palettes::css::{BLACK, GREEN, LIGHT_BLUE, RED}, color::palettes::css::{BLACK, GREEN, LIGHT_BLUE, RED, WHITE},
prelude::*, prelude::*,
}; };
pub fn spawn_ui(mut commands: Commands, score: Res<Score>, lives: Res<Lives>) { /// Plugin for the main menu
commands.spawn(( pub struct PluginGameMenu;
Text::new(format!("Score: {score:?} | Lives: {lives:?}")),
TextFont::from_font_size(25.0), 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(
Update,
handle_spacebar.run_if(in_state(GameState::TitleScreen)),
);
}
} }
pub fn preparation_widget_plugin(app: &mut App) { /// Plugin for the "get ready" period as gameplay starts.
pub struct PluginGetReady;
impl Plugin for PluginGetReady {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::GetReady), spawn_get_ready) app.add_systems(OnEnter(GameState::GetReady), spawn_get_ready)
.add_systems(OnExit(GameState::GetReady), despawn_get_ready) .add_systems(OnExit(GameState::GetReady), despawn::<OnReadySetGo>)
.add_systems( .add_systems(
Update, Update,
(animate_get_ready_widget).run_if(in_state(GameState::GetReady)), (animate_get_ready_widget).run_if(in_state(GameState::GetReady)),
) )
.insert_resource(ReadySetGoTimer(Timer::from_seconds(3.0, TimerMode::Once))); .insert_resource(ReadySetGoTimer(Timer::from_seconds(3.0, TimerMode::Once)));
}
}
/// Plugin for the game-over screen (TODO)
pub struct PluginGameOver;
impl Plugin for PluginGameOver {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::GameOver), spawn_gameover_ui)
.add_systems(OnExit(GameState::GameOver), despawn::<MarkerGameOver>)
.add_systems(
Update,
operate_gameover_ui.run_if(in_state(GameState::GameOver)),
);
}
} }
/// 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;
/// Marker for things on the game-over screen
#[derive(Component)]
struct MarkerGameOver;
/// Action specifier for the game-over menu's buttons.
///
/// 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 {
ToMainMenu,
Quit,
}
/// Newtype wrapper for `Timer`. Used to count down during the "get ready" phase. /// Newtype wrapper for `Timer`. Used to count down during the "get ready" phase.
#[derive(Deref, DerefMut, Resource)] #[derive(Deref, DerefMut, Resource)]
struct ReadySetGoTimer(Timer); struct ReadySetGoTimer(Timer);
@@ -41,6 +82,14 @@ struct CountdownText;
#[derive(Component)] #[derive(Component)]
struct CountdownBar; struct CountdownBar;
/// Despawns entities matching the generic argument. Intended to remove UI
/// elements.
fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<T>>) {
for entity in to_despawn {
commands.entity(entity).despawn();
}
}
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
@@ -75,12 +124,63 @@ fn spawn_get_ready(mut commands: Commands) {
)); ));
} }
// TODO: Replace this with a generic somewhere else in the crate /// Spawns the game over screen.
// want: `despawn_screen::<OnReadySetGo>>()` ///
fn despawn_get_ready(mut commands: Commands, to_despawn: Query<Entity, With<OnReadySetGo>>) { /// Used by [`PluginGameOver`] when entering the [`GameState::GameOver`] state.
for entity in to_despawn { fn spawn_gameover_ui(mut commands: Commands) {
commands.entity(entity).despawn(); commands.spawn((
} MarkerGameOver, // Marker, so `despawn<T>` can remove this on state exit.
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
..default()
},
children![
(
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("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(),
)]
)
],
));
} }
fn animate_get_ready_widget( fn animate_get_ready_widget(
@@ -108,16 +208,35 @@ fn animate_get_ready_widget(
} }
} }
pub struct GameMenuPlugin; /// Handles interaction and performs updates to the game-over UI.
///
impl Plugin for GameMenuPlugin { /// Used by [`PluginGameOver`] while in the [`GameState::GameOver`] state.
fn build(&self, app: &mut App) { ///
app.add_systems(OnEnter(GameState::TitleScreen), spawn_menu) /// Mostly a button input handler, but it also makes for a convenient single
.add_systems(OnExit(GameState::TitleScreen), despawn_menu) /// place to keep all system logic for this plugin.
.add_systems( fn operate_gameover_ui(
Update, mut interactions: Query<
handle_spacebar.run_if(in_state(GameState::TitleScreen)), (&Interaction, &mut BackgroundColor, &mut BorderColor),
); (Changed<Interaction>, With<Button>),
>,
) {
// 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) in &mut interactions {
match *interaction {
Interaction::Pressed => {
*color = UI_BUTTON_PRESSED.into();
border_color.0 = RED.into();
}
Interaction::Hovered => {
*color = UI_BUTTON_HOVERED.into();
border_color.0 = WHITE.into();
}
Interaction::None => {
*color = UI_BUTTON_NORMAL.into();
border_color.0 = BLACK.into();
}
}
} }
} }
@@ -147,14 +266,15 @@ fn spawn_menu(mut commands: Commands) {
}); });
} }
fn despawn_menu(mut commands: Commands, to_despawn: Query<Entity, With<TitleUI>>) {
for entity in &to_despawn {
commands.entity(entity).despawn();
}
}
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);
} }
} }
pub fn spawn_ui(mut commands: Commands, score: Res<Score>, lives: Res<Lives>) {
commands.spawn((
Text::new(format!("Score: {score:?} | Lives: {lives:?}")),
TextFont::from_font_size(25.0),
));
}