39 Commits

Author SHA1 Message Date
0eac337c00 Update spawn_player to use new GameAssets
Some checks failed
Basic checks / Basic build-and-test supertask (push) Has been cancelled
2025-07-29 11:54:39 -05:00
3d4e0afc58 Swap asteroid-spawning to use new GameAssets 2025-07-29 11:51:12 -05:00
f62ab2c95d New GameAssets resource to hold all my assets
I'm finally getting around to centralizing all the assets instead of
letting spawners load their own.

I'm missing some assets, which may eventually be filled by textures
instead of solid-colors and simple shapes

I also need to hook up all the functions to use this thing instead.
2025-07-29 11:48:02 -05:00
6681b25728 Functional prototype of WIP asteroid spawning
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 6m36s
It needs a whole lot more work, but hey, look: A rock!

... well a circle, anyway.
2025-07-28 15:35:50 -05:00
68a8de1809 Make player ship wrap by marking it with Wrapping
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 3m2s
2025-07-28 09:15:23 -05:00
a6622f24b5 Add new "Wrapping" marker component
Not everything needs to wrap, so I'll use a marker component for the
ones that do. At the moment, I'm thinking only the player's ship will
wrap around. Asteroids can be de-spawned and re-spawned, and bullets can
simply evaporate.
2025-07-27 19:36:05 -05:00
c37887b0e7 Remove unused system inputs for prep widget 2025-07-27 16:20:40 -05:00
31451732c4 Finish the countdown anim with a progress bar
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 6m40s
2025-07-27 16:18:51 -05:00
4ecbfaa370 Implement (most of) the timer countdown
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 26s
The timer-wrapper thing was marked as a Component rather than a
Resource, which I have fixed.

The update function is about as straight forward as can be:
1. tick the timer
2. read the value out, format it for display,
3. check if the timer is expired, change states if so.
2025-07-27 14:42:50 -05:00
eb50655671 autoformat 2025-07-27 12:57:56 -05:00
a7d54c9192 Impl "get-ready"s despawn function 2025-07-27 12:57:07 -05:00
fccd2e6a8b Impl the "get-ready" widget's spawn function 2025-07-27 12:56:42 -05:00
efabcbf636 Start submodule to impl the "Get ready" spinner 2025-07-27 11:58:58 -05:00
f848de6b2e Replace usage of another deprecated function
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 29s
2025-07-27 11:08:59 -05:00
e605bbf80d Spawn just one camera (it's messing with egui)
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 27s
The egui debug inspector disappears when switching scenes, which seems
to be related to the camera change. I'm not going to dig into why
exactly this is happening. I'll just create one camera and keep it.
2025-07-27 11:05:49 -05:00
39bddf1c9e Fix: only run space-to-start when on title screen 2025-07-27 10:43:28 -05:00
477460ad2f Fix: Mark the main-menu camera so it despawns
The camera spawned for the main menu wasn't given the TitleUI marker
component, so it wouldn't despawn when changing to the game scene.
2025-07-27 10:41:00 -05:00
c11322969c Finally impl the "space to start" feature 2025-07-27 10:40:37 -05:00
7123192271 Implement the main menu despawn function 2025-07-27 10:33:53 -05:00
9a2381249f autoformat 2025-07-27 10:31:24 -05:00
f68d841e52 Switch to non-deprecated .single_mut() method 2025-07-27 10:31:12 -05:00
88db8a868a Make the Lives(i32) resource appear on debug UI 2025-07-27 10:30:11 -05:00
584a30f7f8 Add bevy-inspector-egui so I can fiddle with values easier 2025-07-27 09:45:33 -05:00
430b77be2e Fix: add game entities only when in Playing state 2025-07-27 09:32:49 -05:00
6eb69f476f Split title screen into it's own mod & Plugin
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 28s
2025-07-27 09:31:09 -05:00
a4409cb946 Bump to Bevy 0.16 2025-07-26 19:38:49 -05:00
38fbc85505 Autoformat 2025-07-26 18:59:56 -05:00
08c9625e71 Remove all usage of old Mesh & Material bundles 2025-07-26 18:57:47 -05:00
96aff4ae46 Chain method calls, drop intermediate vars 2025-07-26 18:47:18 -05:00
a52311eac6 Switch to new Camera2d struct over cam bundle 2025-07-26 18:45:02 -05:00
cf678f9f16 Update spawn_screen function 2025-07-26 18:44:10 -05:00
2f9afaeac1 Replace spawn_ui implementation 2025-07-26 18:30:38 -05:00
7f5a166f10 Hack to get ship meshes back in place 2025-07-26 18:19:50 -05:00
5e6440340f Rename time elapsed method usage 2025-07-26 18:01:06 -05:00
290aab45f5 Bump to Bevy 0.15, enable dynamic linkage
The dynamic linkage is for improving testing cycle time, not for any direct impl reason.
2025-07-26 17:55:22 -05:00
406e611e31 Autoformat to make the checker happy
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 44s
2025-02-01 17:28:37 -06:00
c86cd0d642 States, I guess. Now to do the others
Game states are named and used to toggle behavior. Now to rewrite those
things to do the *right* behavior.
2024-11-29 16:45:52 -06:00
f114203665 Title menu, but always present. Time for states!
I've made a quick title menu, but it is always present. I'll need to set
up some game state stuff so I can flip between play modes.
2024-11-29 16:01:23 -06:00
37d7c1db42 Spawning score and lives UI elements, no logic
I have basic UI elements! They can't be updated, yet, and there's still
no game logic to allow the player to affect it on their own.
2024-11-28 11:56:42 -06:00
6 changed files with 394 additions and 63 deletions

View File

@@ -1,7 +1,8 @@
[package] [package]
name = "asteroids" name = "asteroids"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
bevy = "0.14.2" bevy = { version = "0.16", features = ["dynamic_linking"] }
bevy-inspector-egui = "0.32.0"

View File

@@ -11,6 +11,8 @@ 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);
pub(crate) const SHIP_THRUSTER_COLOR_INACTIVE: Color = Color::srgb(0.5, 0.5, 0.5); pub(crate) const SHIP_THRUSTER_COLOR_INACTIVE: Color = Color::srgb(0.5, 0.5, 0.5);
pub(crate) const ASTEROID_SMALL_COLOR: Color = Color::srgb(1.0, 0., 0.);
// TODO: asteroid medium & large
pub(crate) const SHIP_THRUST: f32 = 1.0; pub(crate) const SHIP_THRUST: f32 = 1.0;
pub(crate) const SHIP_ROTATION: f32 = 0.1; // +/- rotation speed in... radians per frame pub(crate) const SHIP_ROTATION: f32 = 0.1; // +/- rotation speed in... radians per frame

View File

@@ -1,31 +1,66 @@
pub mod config; pub mod config;
mod preparation_widget;
mod title_screen;
use std::time::Duration;
use crate::config::{BACKGROUND_COLOR, PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST, WINDOW_SIZE}; use crate::config::{BACKGROUND_COLOR, PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST, WINDOW_SIZE};
use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; use bevy::{color::palettes::css::GRAY, prelude::*};
use config::{SHIP_THRUSTER_COLOR_ACTIVE, SHIP_THRUSTER_COLOR_INACTIVE}; use bevy_inspector_egui::prelude::ReflectInspectorOptions;
use bevy_inspector_egui::InspectorOptions;
use config::{ASTEROID_SMALL_COLOR, SHIP_THRUSTER_COLOR_ACTIVE, SHIP_THRUSTER_COLOR_INACTIVE};
pub struct AsteroidPlugin; 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_systems(Startup, (spawn_camera, spawn_player)) app.add_plugins((
title_screen::GameMenuPlugin,
preparation_widget::preparation_widget_plugin,
))
.insert_resource(ClearColor(BACKGROUND_COLOR)) .insert_resource(ClearColor(BACKGROUND_COLOR))
.insert_resource(WorldSize { .insert_resource(WorldSize {
width: WINDOW_SIZE.x, width: WINDOW_SIZE.x,
height: WINDOW_SIZE.y, height: WINDOW_SIZE.y,
}) })
.insert_resource(Lives(3))
.register_type::<Lives>()
.insert_resource(Score(0))
.insert_resource(AsteroidSpawner {
timer: Timer::new(Duration::from_secs(3), TimerMode::Repeating),
})
.init_resource::<GameAssets>()
.add_systems(Startup, spawn_camera)
.add_systems(OnEnter(GameState::Playing), (spawn_player, spawn_ui))
.add_systems( .add_systems(
FixedUpdate, FixedUpdate,
(input_ship_thruster, input_ship_rotation, wrap_entities), (
input_ship_thruster,
input_ship_rotation,
wrap_entities,
tick_asteroid_manager,
)
.run_if(in_state(GameState::Playing)),
) )
.add_systems( .add_systems(
FixedPostUpdate, FixedPostUpdate,
(integrate_velocity, update_positions, apply_rotation_to_mesh), (integrate_velocity, update_positions, apply_rotation_to_mesh)
.run_if(in_state(GameState::Playing)),
); );
app.insert_state(GameState::TitleScreen);
} }
} }
#[derive(Clone, Debug, Eq, Hash, PartialEq, States)]
pub enum GameState {
TitleScreen, // Program is started. Present title screen and await user start
GetReady, // Short timer to let the player get ready after pressing start
Playing, // Player has started the game. Run the main loop
GameOver, // Game has ended. Present game over dialogue and await user restart
}
#[derive(Component)] #[derive(Component)]
struct Position(bevy::math::Vec2); struct Position(bevy::math::Vec2);
@@ -38,63 +73,182 @@ struct Rotation(f32);
#[derive(Component)] #[derive(Component)]
struct Ship; struct Ship;
#[derive(Component, Deref, DerefMut)]
struct Asteroid(AsteroidSize);
enum AsteroidSize {
SMALL,
MEDIUM,
LARGE,
}
#[derive(Resource)]
struct AsteroidSpawner {
timer: Timer,
// TODO: Configurables?
// - interval
// - density
// - size distribution
}
/// Marker for any entity that should wrap on screen edges
#[derive(Component)]
struct Wrapping;
// Data component to store color properties attached to an entity // Data component to store color properties attached to an entity
// This was easier (and imo better) than holding global consts with // This was easier (and imo better) than holding global consts with
// UUID assets. // UUID assets.
// TODO: Convert to Resource. I don't need per-entity thruster colors for this.
#[derive(Component)] #[derive(Component)]
struct ThrusterColors(Handle<ColorMaterial>, Handle<ColorMaterial>); struct ThrusterColors(Handle<ColorMaterial>, Handle<ColorMaterial>);
#[derive(Resource, Debug, Deref, Clone, Copy)]
struct Score(i32);
impl From<Score> for String {
fn from(value: Score) -> Self {
value.to_string()
}
}
#[derive(InspectorOptions, Reflect, Resource, Debug, Deref, Clone, Copy)]
#[reflect(Resource, InspectorOptions)]
struct Lives(i32);
impl From<Lives> for String {
fn from(value: Lives) -> Self {
value.to_string()
}
}
#[derive(Resource)] #[derive(Resource)]
struct WorldSize { struct WorldSize {
width: f32, width: f32,
height: f32, height: f32,
} }
fn spawn_camera(mut commands: Commands) { #[derive(Resource)]
commands.spawn(Camera2dBundle::default()); struct GameAssets {
meshes: [Handle<Mesh>; 4],
materials: [Handle<ColorMaterial>; 6],
} }
fn spawn_player( impl GameAssets {
mut commands: Commands, fn ship(&self) -> (Handle<Mesh>, Handle<ColorMaterial>) {
mut meshes: ResMut<Assets<Mesh>>, (self.meshes[0].clone(), self.materials[0].clone())
mut materials: ResMut<Assets<ColorMaterial>>, }
) {
let triangle = Triangle2d::new( // The thruster mesh is actually just the ship mesh
fn thruster_mesh(&self) -> Handle<Mesh> {
self.meshes[0].clone()
}
// TODO: Look into parameterizing the material
// A shader uniform should be able to do this, but I don't know how to
// load those in Bevy.
fn thruster_mat_inactive(&self) -> Handle<ColorMaterial> {
self.materials[1].clone()
}
fn thruster_mat_active(&self) -> Handle<ColorMaterial> {
self.materials[2].clone()
}
fn asteroid_small(&self) -> (Handle<Mesh>, Handle<ColorMaterial>) {
(self.meshes[1].clone(), self.materials[1].clone())
}
fn asteroid_medium(&self) -> (Handle<Mesh>, Handle<ColorMaterial>) {
(self.meshes[2].clone(), self.materials[2].clone())
}
fn asteroid_large(&self) -> (Handle<Mesh>, Handle<ColorMaterial>) {
(self.meshes[3].clone(), self.materials[3].clone())
}
}
impl FromWorld for GameAssets {
fn from_world(world: &mut World) -> Self {
let mut world_meshes = world.resource_mut::<Assets<Mesh>>();
let meshes = [
world_meshes.add(Triangle2d::new(
Vec2::new(0.5, 0.0), Vec2::new(0.5, 0.0),
Vec2::new(-0.5, 0.45), Vec2::new(-0.5, 0.45),
Vec2::new(-0.5, -0.45), Vec2::new(-0.5, -0.45),
); )),
let thruster_firing_id = materials.add(SHIP_THRUSTER_COLOR_ACTIVE); world_meshes.add(Circle::new(10.0)),
let thruster_stopped_id = materials.add(SHIP_THRUSTER_COLOR_INACTIVE); world_meshes.add(Circle::new(20.0)),
world_meshes.add(Circle::new(40.0)),
];
let mut world_materials = world.resource_mut::<Assets<ColorMaterial>>();
let materials = [
world_materials.add(PLAYER_SHIP_COLOR),
world_materials.add(SHIP_THRUSTER_COLOR_INACTIVE),
world_materials.add(SHIP_THRUSTER_COLOR_ACTIVE),
world_materials.add(ASTEROID_SMALL_COLOR),
// TODO: asteroid medium and large colors
world_materials.add(ASTEROID_SMALL_COLOR),
world_materials.add(ASTEROID_SMALL_COLOR),
];
GameAssets { meshes, materials }
}
}
let ship_mesh = MaterialMesh2dBundle { fn spawn_camera(mut commands: Commands) {
mesh: meshes.add(triangle).into(), commands.spawn(Camera2d);
material: materials.add(PLAYER_SHIP_COLOR), }
transform: Transform::default().with_scale(Vec3::new(20.0, 20.0, 20.0)),
..default()
};
let thruster_mesh = MaterialMesh2dBundle { fn spawn_player(mut commands: Commands, game_assets: Res<GameAssets>) {
mesh: meshes.add(triangle).into(), let thruster_firing_id = game_assets.thruster_mat_active();
material: materials.add(PLAYER_SHIP_COLOR), let thruster_stopped_id = game_assets.thruster_mat_inactive();
transform: Transform::default()
.with_scale(Vec3::splat(0.5))
.with_translation(Vec3::new(-0.5, 0.0, -0.1)),
..default()
};
let thruster = commands.spawn(thruster_mesh).id(); commands
.spawn((
let mut ship_id = commands.spawn((
Ship, Ship,
Wrapping,
Position(Vec2::default()), Position(Vec2::default()),
Velocity(Vec2::ZERO), Velocity(Vec2::ZERO),
Rotation(0.0), Rotation(0.0),
ship_mesh, Mesh2d(game_assets.ship().0),
MeshMaterial2d(game_assets.ship().1),
ThrusterColors(thruster_firing_id, thruster_stopped_id), ThrusterColors(thruster_firing_id, thruster_stopped_id),
Transform::default().with_scale(Vec3::new(20.0, 20.0, 20.0)),
))
.with_child((
Mesh2d(game_assets.thruster_mesh()),
MeshMaterial2d(game_assets.thruster_mat_inactive()),
Transform::default()
.with_scale(Vec3::splat(0.5))
.with_translation(Vec3::new(-0.5, 0.0, -0.1)),
)); ));
}
ship_id.add_child(thruster); /// Update the asteroid spawn timer and spawn any asteroids
/// that are due this frame.
fn tick_asteroid_manager(
mut commands: Commands,
mut spawner: ResMut<AsteroidSpawner>,
game_assets: Res<GameAssets>,
time: Res<Time>,
) {
spawner.timer.tick(time.delta());
if spawner.timer.just_finished() {
commands.spawn((
Asteroid(AsteroidSize::SMALL),
Position(Vec2::new(40.0, 40.0)),
Velocity(Vec2::new(10.0, 0.0)),
Rotation(0.0),
Mesh2d(game_assets.asteroid_small().0),
MeshMaterial2d(game_assets.asteroid_small().1),
));
}
}
/// Utility function to spawn a single asteroid of a given type
/// TODO: convert to an event listener monitoring for "spawn asteroid" events
/// from the `fn tick_asteroid_manager(...)` system.
fn spawn_asteroid(mut commands: Commands) {
todo!();
} }
/* /*
@@ -105,7 +259,10 @@ fn input_ship_thruster(
mut query: Query<(&mut Velocity, &Rotation, &mut Children, &ThrusterColors), With<Ship>>, mut query: Query<(&mut Velocity, &Rotation, &mut Children, &ThrusterColors), With<Ship>>,
mut commands: Commands, mut commands: Commands,
) { ) {
let Ok((mut velocity, rotation, children, colors)) = query.get_single_mut() else { // TODO: Maybe change for a Single<Ship>> so this only runs for the one ship
// buuut... that would silently do nothing if there are 0 or >1 ships, and
// I might want to crash on purpose in that case.
let Ok((mut velocity, rotation, children, colors)) = query.single_mut() else {
let count = query.iter().count(); let count = query.iter().count();
panic!("There should be exactly one player ship! Instead, there seems to be {count}."); panic!("There should be exactly one player ship! Instead, there seems to be {count}.");
}; };
@@ -116,9 +273,13 @@ fn input_ship_thruster(
if keyboard_input.pressed(KeyCode::KeyW) { if keyboard_input.pressed(KeyCode::KeyW) {
velocity.0 += Vec2::from_angle(rotation.0) * SHIP_THRUST; velocity.0 += Vec2::from_angle(rotation.0) * SHIP_THRUST;
commands.entity(*thrusters).insert(colors.0.clone()); commands
.entity(*thrusters)
.insert(MeshMaterial2d(colors.0.clone()));
} else { } else {
commands.entity(*thrusters).insert(colors.1.clone()); commands
.entity(*thrusters)
.insert(MeshMaterial2d(colors.1.clone()));
} }
} }
@@ -130,7 +291,7 @@ fn input_ship_rotation(
keyboard_input: Res<ButtonInput<KeyCode>>, keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut Rotation, With<Ship>>, mut query: Query<&mut Rotation, With<Ship>>,
) { ) {
let Ok(mut rotation) = query.get_single_mut() else { let Ok(mut rotation) = query.single_mut() else {
let count = query.iter().count(); let count = query.iter().count();
panic!("There should be exactly one player ship! Instead, there seems to be {count}."); panic!("There should be exactly one player ship! Instead, there seems to be {count}.");
}; };
@@ -142,12 +303,16 @@ fn input_ship_rotation(
} }
} }
// TODO: Combine movement integration steps into one function
// They need to be ordered so the physics is deterministic. Bevy can enforce
// order, but it makes more sense to cut out the extra machinery and have one
// single function. Probably better for cache locality or whatever, too.
/* /*
Add velocity to position Add velocity to position
*/ */
fn integrate_velocity(mut query: Query<(&mut Position, &Velocity)>, time: Res<Time>) { fn integrate_velocity(mut query: Query<(&mut Position, &Velocity)>, time: Res<Time>) {
for (mut position, velocity) in &mut query { for (mut position, velocity) in &mut query {
position.0 += velocity.0 * time.delta_seconds(); position.0 += velocity.0 * time.delta_secs();
} }
} }
@@ -167,7 +332,7 @@ fn apply_rotation_to_mesh(mut query: Query<(&mut Transform, &Rotation)>) {
} }
} }
fn wrap_entities(mut query: Query<&mut Position>, world_size: Res<WorldSize>) { fn wrap_entities(mut query: Query<&mut Position, With<Wrapping>>, world_size: Res<WorldSize>) {
let right = world_size.width / 2.0; let right = world_size.width / 2.0;
let left = -right; let left = -right;
let top = world_size.height / 2.0; let top = world_size.height / 2.0;
@@ -187,3 +352,10 @@ fn wrap_entities(mut query: Query<&mut Position>, world_size: Res<WorldSize>) {
} }
} }
} }
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),
));
}

View File

@@ -1,6 +1,7 @@
use bevy::{prelude::*, window::WindowResolution}; use bevy::{prelude::*, window::WindowResolution};
use asteroids::{config::WINDOW_SIZE, AsteroidPlugin}; use asteroids::{config::WINDOW_SIZE, AsteroidPlugin};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
fn main() { fn main() {
App::new() App::new()
@@ -12,5 +13,7 @@ fn main() {
..default() ..default()
})) }))
.add_plugins(AsteroidPlugin) .add_plugins(AsteroidPlugin)
.add_plugins(EguiPlugin::default())
.add_plugins(WorldInspectorPlugin::new())
.run(); .run();
} }

99
src/preparation_widget.rs Normal file
View File

@@ -0,0 +1,99 @@
use bevy::{
color::palettes::css::{BLACK, GREEN, LIGHT_BLUE, RED},
prelude::*,
};
use crate::GameState;
pub fn preparation_widget_plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::GetReady), spawn_get_ready)
.add_systems(OnExit(GameState::GetReady), despawn_get_ready)
.add_systems(
Update,
(animate_get_ready_widget).run_if(in_state(GameState::GetReady)),
)
.insert_resource(ReadySetGoTimer(Timer::from_seconds(3.0, TimerMode::Once)));
}
/// Marker component for things on the get-ready indicator
#[derive(Component)]
struct OnReadySetGo;
/// Newtype wrapper for `Timer`. Used to count down during the "get ready" phase.
#[derive(Deref, DerefMut, Resource)]
struct ReadySetGoTimer(Timer);
/// Marker for the counter text segment
#[derive(Component)]
struct CountdownText;
/// Marker for the counter bar segment
#[derive(Component)]
struct CountdownBar;
fn spawn_get_ready(mut commands: Commands) {
commands.spawn((
OnReadySetGo, // marker, so this can be de-spawned properly
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
width: Val::Percent(30.),
height: Val::Percent(30.),
..default()
},
BackgroundColor(LIGHT_BLUE.into()),
children![
(Text::new("Get Ready!"), TextColor(BLACK.into())),
(
CountdownBar,
Node {
width: Val::Percent(90.0),
height: Val::Percent(10.),
..default()
},
BackgroundColor(GREEN.into()),
),
(
CountdownText,
Text::new("<uninit timer>"),
TextColor(RED.into()),
)
],
));
}
// TODO: Replace this with a generic somewhere else in the crate
// want: `despawn_screen::<OnReadySetGo>>()`
fn despawn_get_ready(mut commands: Commands, to_despawn: Query<Entity, With<OnReadySetGo>>) {
for entity in to_despawn {
commands.entity(entity).despawn();
}
}
fn animate_get_ready_widget(
mut text_segment: Single<&mut Text, With<CountdownText>>,
mut bar_segment: Single<&mut Node, With<CountdownBar>>,
time: Res<Time>,
mut timer: ResMut<ReadySetGoTimer>,
mut game_state: ResMut<NextState<GameState>>,
) {
// Advance the timer, read the remaining time and write it onto the label.
timer.tick(time.delta());
// Add one to the visual value so the countdown starts at 3 and stops at 1.
// Otherwise it starts at 2 and disappears after showing 0.
// That feels wrong even though it's functionally identical.
let tval = timer.0.remaining().as_secs() + 1;
**text_segment = format!("{tval}").into();
// Shrink the progress bar Node
bar_segment.width = Val::Percent(100.0 * (1.0 - timer.fraction()));
// If the timer has expired, change state to playing.
if timer.finished() {
game_state.set(GameState::Playing);
}
}

54
src/title_screen.rs Normal file
View File

@@ -0,0 +1,54 @@
use crate::GameState;
use bevy::prelude::*;
pub struct GameMenuPlugin;
impl Plugin for GameMenuPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::TitleScreen), spawn_menu)
.add_systems(OnExit(GameState::TitleScreen), despawn_menu)
.add_systems(
Update,
handle_spacebar.run_if(in_state(GameState::TitleScreen)),
);
}
}
// 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),
));
});
}
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>>) {
if input.just_pressed(KeyCode::Space) {
game_state.set(GameState::GetReady);
}
}