6 Commits

Author SHA1 Message Date
33828d6a85 Rewrite WorldSize as a newtype around Vec2
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m7s
Closes #16

Bevy provides Deref and DerefMut derives, so that's nice. I'm not sure
it makes any sense to keep the field private since those derefs expose
the Vec2 anyway. I added an `impl Default` anyway, though.
2025-08-12 23:39:19 -05:00
5e2018d3e4 Docs for physics module 2025-08-12 23:26:37 -05:00
7c877264a2 autoformat 2025-08-12 23:18:46 -05:00
16842c13f7 Docs for objects.rs 2025-08-12 23:18:33 -05:00
e199db16eb Docs for machinery.rs 2025-08-12 23:01:32 -05:00
f17910daef Docs for lib.rs 2025-08-12 22:41:37 -05:00
5 changed files with 74 additions and 24 deletions

View File

@@ -1,3 +1,7 @@
//! Asteroids, implemented as a Bevy plugin.
//!
//! Compile-time configurables can be found in the [`config`] module.
pub mod config;
mod events;
mod machinery;
@@ -24,6 +28,7 @@ use bevy_rapier2d::{
use machinery::Lifetime;
use resources::{GameAssets, Lives, Score, WorldSize};
/// The main game plugin.
pub struct AsteroidPlugin;
impl Plugin for AsteroidPlugin {
@@ -35,10 +40,7 @@ impl Plugin for AsteroidPlugin {
RapierDebugRenderPlugin::default(),
))
.insert_resource(ClearColor(BACKGROUND_COLOR))
.insert_resource(WorldSize {
width: WINDOW_SIZE.x,
height: WINDOW_SIZE.y,
})
.insert_resource(WorldSize::default())
.insert_resource(Lives(3))
.register_type::<Lives>()
.insert_resource(Score(0))
@@ -90,6 +92,7 @@ fn debug_collision_event_printer(mut collision_events: EventReader<CollisionEven
}
}
/// The game's main state tracking mechanism, registered with Bevy as a [`State`](`bevy::prelude::State`).
#[derive(Clone, Debug, Eq, Hash, PartialEq, States)]
pub enum GameState {
TitleScreen, // Program is started. Present title screen and await user start
@@ -102,9 +105,9 @@ fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
/*
Checks if "W" is pressed and increases velocity accordingly.
*/
/// Player's thruster control system.
///
/// Checks if "W" is pressed and increases velocity accordingly.
fn input_ship_thruster(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut physics::Velocity, &Transform, &mut Children), With<Ship>>,
@@ -135,10 +138,10 @@ fn input_ship_thruster(
}
}
/*
Checks if "A" or "D" is pressed and updates the player's Rotation component accordingly
Does *not* rotate the graphical widget! (that's done by the `apply_rotation_to_mesh` system)
*/
/// Player's rotation control system.
///
/// Checks if "A" or "D" is pressed and updates the player's [`AngularVelocity`]
/// component accordingly.
fn input_ship_rotation(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut AngularVelocity, With<Ship>>,
@@ -157,6 +160,12 @@ fn input_ship_rotation(
}
}
/// Player's gun trigger.
///
/// Checks if the spacebar has just been pressed, spawning a bullet if so.
///
/// TODO: Hook up a timer to control weapon fire-rate. Something will have to
/// tick those timers. Maybe this system?
fn input_ship_shoot(
keyboard_input: Res<ButtonInput<KeyCode>>,
ship: Single<(&Transform, &physics::Velocity), With<Ship>>,

View File

@@ -1,5 +1,8 @@
//! These are Systems that power the main game mechanics (and some misc items to support them)
//! Systems, Components, and any other items for powering the game logic.
//!
//! Where the objects (ship, asteroid, etc) carry their own behavioral systems,
//! the *game* keeps its main logic here. Its for ambient behaviors, like
//! asteroid spawning, or eventually the flying saucer spawns.
use rand::{Rng, SeedableRng};
use std::time::Duration;
@@ -7,6 +10,12 @@ use bevy::prelude::*;
use crate::{WorldSize, events::SpawnAsteroid, objects::AsteroidSize};
/// Asteroid spawning parameters and state.
///
/// This struct keeps track of the rng and timer for spawning asteroids. In the
/// future it may contain additional fields to allow for more control.
///
/// It's values are operated by the [`tick_asteroid_manager`] system.
#[derive(Resource)]
pub struct AsteroidSpawner {
rng: std::sync::Mutex<rand::rngs::StdRng>,
@@ -26,8 +35,8 @@ impl AsteroidSpawner {
}
}
/// Update the asteroid spawn timer and spawn any asteroids
/// that are due this frame.
/// Update the asteroid spawn timer in the [`AsteroidSpawner`] resource, and
/// spawns any asteroids that are due this frame.
pub fn tick_asteroid_manager(
mut events: EventWriter<SpawnAsteroid>,
mut spawner: ResMut<AsteroidSpawner>,
@@ -46,7 +55,7 @@ pub fn tick_asteroid_manager(
let spawn_angle = rng.random_range(0.0..(std::f32::consts::PI * 2.0));
// Rho will be the radius of a circle bordering the viewport, multiplied by 1.2
// TODO: Use view diagonal to get a minimally sized circle around the play area
let spawn_distance = play_area.width.max(play_area.height) / 2.0;
let spawn_distance = play_area.x.max(play_area.y) / 2.0;
// Convert polar to Cartesian, use as position
let pos = Vec2::new(
@@ -77,9 +86,11 @@ pub fn tick_asteroid_manager(
}
}
/// Sets a lifetime on an entity to automatically delete it after expiration.
#[derive(Component)]
pub struct Lifetime(pub Timer);
/// System to tick the [`Lifetime`] timers and despawn expired entities.
pub fn tick_lifetimes(
mut commands: Commands,
time: Res<Time>,

View File

@@ -28,6 +28,7 @@ use crate::{
physics::{Velocity, Wrapping},
};
/// The asteroid, defined entirely by [it's size](`AsteroidSize`).
#[derive(Component, Deref, DerefMut)]
pub struct Asteroid(pub AsteroidSize);
@@ -39,6 +40,8 @@ pub enum AsteroidSize {
}
impl AsteroidSize {
/// Convenience util to get the "next smallest" size. Useful for splitting
/// after collision.
pub fn next(&self) -> Option<Self> {
match self {
AsteroidSize::Small => None,
@@ -48,9 +51,11 @@ impl AsteroidSize {
}
}
/// Marker component for the player's ship.
#[derive(Component)]
pub struct Ship;
/// Marker component for bullets.
#[derive(Component)]
pub struct Bullet;
@@ -126,6 +131,11 @@ pub fn split_asteroids(
}
}
/// Spawns the player at the world origin. Used during the state change to
/// [`GameState::Playing`] to spawn the player.
///
/// This only spawns the player. For player **re**-spawn activity, see the
/// [`ship_impact_listener()`] system.
pub fn spawn_player(mut commands: Commands, game_assets: Res<GameAssets>) {
commands
.spawn((
@@ -160,6 +170,9 @@ pub fn bullet_impact_listener(mut commands: Commands, mut events: EventReader<Bu
/// Watch for [`ShipDestroy`] events and update game state accordingly.
///
/// One life is taken from the counter, asteroids are cleared, and the player
/// is placed back at the origin. If lives reach 0, this system will change
/// states to [`GameState::GameOver`].
/// - Subtract a life
/// - Check life count. If 0, go to game-over state
/// - Clear all asteroids

View File

@@ -1,5 +1,19 @@
//! Custom physics items
//!
//! TODO: Refactor in terms of Rapier2D, *or* implement colliders and remove it.
//!
//! Position, both translation and rotation, are tracked by the built-in
//! Bevy [`Transform`] component. This module adds a (Linear) [`Velocity`] and
//! [`AngularVelocity`] component to the mix.
//! These two components are operated by simple integrator systems to
//! accumulate translation and rotation from the velocities.
//!
//! The [`Wrapping`] component marks things that should wrap around the world
//! boundaries. It's a kind of physical property of this world, so I'm putting
//! it here.
//!
//! Collisions are also considered physics. After all, I got *a physics engine*
//! to detect them for me (so that I don't have to write clipping code).
use crate::{
WorldSize, events,
@@ -42,9 +56,9 @@ pub(crate) fn wrap_entities(
mut query: Query<&mut Transform, With<Wrapping>>,
world_size: Res<WorldSize>,
) {
let right = world_size.width / 2.0;
let right = world_size.x / 2.0;
let left = -right;
let top = world_size.height / 2.0;
let top = world_size.y / 2.0;
let bottom = -top;
for mut pos in query.iter_mut() {

View File

@@ -10,7 +10,7 @@ use bevy::{
Vec2,
primitives::{Circle, Triangle2d},
},
prelude::{Deref, Reflect, ReflectResource},
prelude::{Deref, DerefMut, Reflect, ReflectResource},
render::mesh::Mesh,
sprite::ColorMaterial,
};
@@ -19,7 +19,7 @@ use bevy_inspector_egui::inspector_options::ReflectInspectorOptions;
use crate::{
ASTEROID_SMALL_COLOR, BULLET_COLOR, PLAYER_SHIP_COLOR, SHIP_THRUSTER_COLOR_ACTIVE,
SHIP_THRUSTER_COLOR_INACTIVE,
SHIP_THRUSTER_COLOR_INACTIVE, config::WINDOW_SIZE,
};
#[derive(Resource, Debug, Deref, Clone, Copy)]
@@ -41,10 +41,13 @@ impl From<Lives> for String {
}
}
#[derive(Resource)]
pub struct WorldSize {
pub width: f32,
pub height: f32,
#[derive(Deref, DerefMut, Resource)]
pub struct WorldSize(Vec2);
impl Default for WorldSize {
fn default() -> Self {
WorldSize(Vec2::new(WINDOW_SIZE.x, WINDOW_SIZE.y))
}
}
#[derive(Resource)]