Files
another-asteroids/src/objects.rs
Robert Garrett a48dfc1d65
Some checks failed
Basic checks / Basic build-and-test supertask (push) Failing after 2m1s
Finish the engine upgrade
Events have been replaced with Messages, import paths have been updated
for new engine module layout, and minor API changes have been matched.
2025-12-17 10:15:33 -06:00

237 lines
7.9 KiB
Rust

//! This module contains all the "things" in the game.
//!
//! Asteroids, the player's ship, and such.
use bevy::{
camera::visibility::Visibility,
ecs::{
component::Component,
entity::Entity,
message::{MessageReader, MessageWriter},
query::With,
system::{Commands, Query, Res, ResMut, Single},
},
math::{Vec2, Vec3, Vec3Swizzles},
mesh::Mesh2d,
prelude::{Deref, DerefMut},
sprite_render::MeshMaterial2d,
state::state::NextState,
time::{Timer, TimerMode},
transform::components::Transform,
};
use bevy_rapier2d::prelude::{ActiveCollisionTypes, ActiveEvents, Collider, Sensor};
use crate::{
AngularVelocity, GameAssets, GameState, Lives,
config::{ASTEROID_LIFETIME, DEBRIS_LIFETIME, SHIP_FIRE_RATE},
machinery::{Lifetime, Sparkler},
messages::{AsteroidDestroy, BulletDestroy, ShipDestroy, SpawnAsteroid},
physics::{Velocity, Wrapping},
};
/// The asteroid, defined entirely by [it's size](`AsteroidSize`).
#[derive(Component, Deref, DerefMut)]
pub struct Asteroid(pub AsteroidSize);
#[derive(Clone, Copy, Debug)]
pub enum AsteroidSize {
Small,
Medium,
Large,
}
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,
AsteroidSize::Medium => Some(AsteroidSize::Small),
AsteroidSize::Large => Some(AsteroidSize::Medium),
}
}
}
/// Marker component for the player's ship.
#[derive(Component)]
pub struct Ship;
/// The ship's gun (is just a timer)
#[derive(Component, Deref, DerefMut)]
pub struct Weapon(Timer);
/// Marker component for bullets.
#[derive(Component)]
pub struct Bullet;
/// Debris left behind after the ship is destroyed.
#[derive(Component)]
pub struct Debris;
/// Responds to [`SpawnAsteroid`] events, spawning as specified
pub fn spawn_asteroid(
mut events: MessageReader<SpawnAsteroid>,
mut commands: Commands,
game_assets: Res<GameAssets>,
) {
for spawn in events.read() {
let (mesh, material) = match spawn.size {
AsteroidSize::Small => game_assets.asteroid_small(),
AsteroidSize::Medium => game_assets.asteroid_medium(),
AsteroidSize::Large => game_assets.asteroid_large(),
};
let collider_radius = match spawn.size {
AsteroidSize::Small => 10.0,
AsteroidSize::Medium => 20.0,
AsteroidSize::Large => 40.0,
};
commands.spawn((
Asteroid(spawn.size),
Collider::ball(collider_radius),
Sensor,
Transform::from_translation(spawn.pos.extend(0.0)),
Velocity(spawn.vel),
Mesh2d(mesh),
MeshMaterial2d(material),
Lifetime(Timer::from_seconds(ASTEROID_LIFETIME, TimerMode::Once)),
));
}
}
/// Event listener for asteroid destruction events. Shrinks and multiplies
/// asteroids until they vanish.
///
/// - Large -> 2x Medium
/// - Medium -> 2x Small
/// - Small -> (despawned)
///
/// The velocity of the child asteroids is scattered somewhat, as if they were
/// explosively pushed apart.
pub fn split_asteroids(
mut destroy_events: MessageReader<AsteroidDestroy>,
mut respawn_events: MessageWriter<SpawnAsteroid>,
mut commands: Commands,
query: Query<(&Transform, &Asteroid, &Velocity)>,
) {
for event in destroy_events.read() {
if let Ok((transform, rock, velocity)) = query.get(event.0) {
let next_size = rock.0.next();
if let Some(size) = next_size {
let pos = transform.translation.xy();
let left_offset = Vec2::from_angle(0.4);
let right_offset = Vec2::from_angle(-0.4);
respawn_events.write(SpawnAsteroid {
pos,
vel: left_offset.rotate(velocity.0),
size,
});
respawn_events.write(SpawnAsteroid {
pos,
vel: right_offset.rotate(velocity.0),
size,
});
}
// Always despawn the asteroid. New ones (may) be spawned in it's
// place, but this one is gone.
commands.entity(event.0).despawn();
}
}
}
/// 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((
Collider::ball(0.7),
Sensor,
ActiveEvents::COLLISION_EVENTS,
ActiveCollisionTypes::STATIC_STATIC,
Ship,
Weapon(Timer::from_seconds(1.0 / SHIP_FIRE_RATE, TimerMode::Once)),
Wrapping,
Velocity(Vec2::ZERO),
AngularVelocity(0.0),
Mesh2d(game_assets.ship().0),
MeshMaterial2d(game_assets.ship().1),
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)),
));
}
/// Watch for [`BulletDestroy`] events and despawn
/// the associated bullet.
pub fn bullet_impact_listener(mut commands: Commands, mut events: MessageReader<BulletDestroy>) {
for event in events.read() {
commands.entity(event.0).despawn();
}
}
/// 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
/// - Respawn player
pub fn ship_impact_listener(
mut events: MessageReader<ShipDestroy>,
mut commands: Commands,
mut lives: ResMut<Lives>,
rocks: Query<Entity, With<Asteroid>>,
mut player: Single<(&mut Transform, &mut Velocity), With<Ship>>,
mut next_state: ResMut<NextState<GameState>>,
game_assets: Res<GameAssets>,
) {
for _ in events.read() {
// STEP 1: Clear asteroids
for rock in rocks {
commands.entity(rock).despawn();
}
// STEP 2: Decrement lives
if lives.0 == 0 {
// If the player has run out, return early with a state change.
next_state.set(GameState::GameOver);
return;
} else {
// Decrease life count.
lives.0 -= 1;
}
// STEP 3: spawn the debris field where the player used to be.
for i in 0..10 {
let angle_rads = (i as f32) / 10.0 * std::f32::consts::TAU;
let dir = Vec2::from_angle(angle_rads);
let vel = player.1.0 + dir * 100.0;
commands.spawn((
Debris,
Visibility::Visible, // make sure it's "visible" not "Inherited" so the cycle works right
Lifetime(Timer::from_seconds(DEBRIS_LIFETIME, TimerMode::Once)),
Sparkler::at_interval(0.15),
Mesh2d(game_assets.thruster_mesh()), // borrow the thruster mesh for now
MeshMaterial2d(game_assets.thruster_mat_active()), // ... and the active thruster material
player.0.clone(), // clone the player transform
Velocity(vel),
));
}
// STEP 4: Respawn player (teleport them to the origin)
player.0.translation = Vec3::ZERO;
player.1.0 = Vec2::ZERO;
}
}