14 Commits

Author SHA1 Message Date
dea8a0dc1a Fix: apply thrust input properly
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m11s
Yay for funny coordinate spaces. I was, again, using the wrong
operations to get the 2D Cartesian angle and apply force to the ship.
2025-08-09 16:25:19 -05:00
6191fde25a Fix: apply steering input properly
The ship in Asteroids isn't expected to spin up while holding a steering
direction, but that's exactly what I just made it do.

Fix that problem by assigning, not accumulating, the angular velocity
for the ship.
2025-08-09 16:09:54 -05:00
d4f11faf5a Fix: integrate rotation *correctly*
Something something quaternions are hard. I thought I would take the two
"things", add them together, and then put them back into the transform.

That's clearly not how it works :v
2025-08-09 16:04:51 -05:00
e841facf73 Remove Rotation component, update usage sites 2025-08-09 16:04:12 -05:00
939ffc70a1 Add AngularVelocity, begin removal of Rotation
The rotation component is also redundant with Bevy's transform
component.

This new component and system fill the physics role, but the input
handling code needs to be updated.

Player steering still functions because it uses the `Rotation` component
to direct the force vector. The force is correctly applied to the linear
velocity vector. An improvement would be to have a force/impulse
accumulator so I could account for mass, but I'm not going to do that
right now.
2025-08-09 15:50:54 -05:00
877c7f93d7 Drop TODO to combine phys fns, update docstring
Replacing the `Position` component with direct manipulation of the
transform functionally completed the TODO. The functions weren't exactly
combined, but the resulting behavior might as well have been.

I've updated the docstring to (exist, actually. yay syntax) describe the
system accurately.
2025-08-09 15:26:28 -05:00
2b1a0f386e Add an angular velocity component 2025-08-09 15:14:05 -05:00
515ecaac27 autoformat 2025-08-09 15:11:57 -05:00
3922cac3d7 Finish removal of Position
The component has been completely removed, and all call sites are fixed
to use the Transform directly.
2025-08-09 15:10:44 -05:00
e834d94b8a Use Transform not Position in wrapping system 2025-08-09 15:08:15 -05:00
ad5e86a06b Begin removal of Position component
The position component is redundant with the built-in Bevy Transform.
I've updated the velocity integrator to use the transform directly.

Physics still works, but things still set their initial locations
through the Position component. I'll need to fix all those call sites.
2025-08-09 15:05:02 -05:00
3d0da6df2d Fix module-level docstring in asteroids & config
I made a comment to help me remember the purpose, but if I do it this
way `cargo doc` will render it out to the actual documentation.
2025-08-09 14:55:07 -05:00
73b97ad15c Re-order use statements, prefer crate mods first 2025-08-09 14:53:50 -05:00
61c57783f1 Split physics parts into a submodule
The physics sim bits (that aren't Rapier2d) are now in their own
submodule. I've included the `Wrapping` marker component because it
doesn't really have anywhere else to live, and it's kinda sorta related
to the physics. It controls the motion of objects... that's physics. :p

There are name collisions between Rapier2d's `Velocity` and my own, but
that's a problem for later. I've used more specific type paths where
necessary to maintain the previous behavior.

The `physics::Position` component can go away completely. It's just an
extra copy of some of the built-in `Transform` data. I'm pretty sure it
only exists because I didn't realize I could rely on directly
manipulating the transform when I started this project.
2025-08-09 14:46:56 -05:00
6 changed files with 98 additions and 94 deletions

View File

@@ -1,12 +1,13 @@
//! This is the module containing all the rock-related things.
//! Not... not the whole game.
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use std::time::Duration; use std::time::Duration;
/// This is the module containing all the rock-related things
/// not... not the whole game.
use bevy::prelude::*; use bevy::prelude::*;
use crate::{GameAssets, Position, Rotation, Velocity, WorldSize}; use crate::{GameAssets, WorldSize, physics::Velocity};
#[derive(Component, Deref, DerefMut)] #[derive(Component, Deref, DerefMut)]
pub struct Asteroid(AsteroidSize); pub struct Asteroid(AsteroidSize);
@@ -119,9 +120,8 @@ pub fn spawn_asteroid(
Asteroid(AsteroidSize::Small), Asteroid(AsteroidSize::Small),
Collider::ball(collider_radius), Collider::ball(collider_radius),
Sensor, Sensor,
Position(spawn.pos), Transform::from_translation(spawn.pos.extend(0.0)),
Velocity(spawn.vel), Velocity(spawn.vel),
Rotation(0.0),
Mesh2d(mesh), Mesh2d(mesh),
MeshMaterial2d(material), MeshMaterial2d(material),
)); ));

View File

@@ -1,7 +1,5 @@
/* //! Global constants used all over the program. Rather than leaving them scattered
Global constants used all over the program. Rather than leaving them scattered //! where ever they happen to be needed, I'm concentrating them here.
where ever they happen to be needed, I'm concentrating them here.
*/
use bevy::color::Color; use bevy::color::Color;
@@ -15,6 +13,6 @@ pub(crate) const ASTEROID_SMALL_COLOR: Color = Color::srgb(1.0, 0., 0.);
// TODO: asteroid medium & large // 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 = 4.0; // +/- rotation speed in... radians per frame
pub const RNG_SEED: [u8; 32] = *b"12345678909876543210123456789098"; pub const RNG_SEED: [u8; 32] = *b"12345678909876543210123456789098";

View File

@@ -1,24 +1,27 @@
mod asteroids; mod asteroids;
pub mod config; pub mod config;
mod event; mod event;
mod physics;
mod preparation_widget; mod preparation_widget;
mod ship; mod ship;
mod title_screen; mod title_screen;
use crate::config::{BACKGROUND_COLOR, PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST, WINDOW_SIZE}; use crate::asteroids::{Asteroid, AsteroidSpawner};
use crate::config::{
ASTEROID_SMALL_COLOR, BACKGROUND_COLOR, PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST,
SHIP_THRUSTER_COLOR_ACTIVE, SHIP_THRUSTER_COLOR_INACTIVE, WINDOW_SIZE,
};
use crate::physics::AngularVelocity;
use crate::ship::Ship;
use asteroids::{Asteroid, AsteroidSpawner};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_inspector_egui::InspectorOptions; use bevy_inspector_egui::InspectorOptions;
use bevy_inspector_egui::prelude::ReflectInspectorOptions; use bevy_inspector_egui::prelude::ReflectInspectorOptions;
use bevy_rapier2d::{ use bevy_rapier2d::{
plugin::{NoUserData, RapierPhysicsPlugin}, plugin::{NoUserData, RapierPhysicsPlugin},
prelude::*, prelude::*,
render::RapierDebugRenderPlugin, render::RapierDebugRenderPlugin,
}; };
use config::{ASTEROID_SMALL_COLOR, SHIP_THRUSTER_COLOR_ACTIVE, SHIP_THRUSTER_COLOR_INACTIVE};
use ship::Ship;
pub struct AsteroidPlugin; pub struct AsteroidPlugin;
@@ -47,7 +50,7 @@ impl Plugin for AsteroidPlugin {
( (
input_ship_thruster, input_ship_thruster,
input_ship_rotation, input_ship_rotation,
wrap_entities, physics::wrap_entities,
asteroids::tick_asteroid_manager, asteroids::tick_asteroid_manager,
asteroids::spawn_asteroid.after(asteroids::tick_asteroid_manager), asteroids::spawn_asteroid.after(asteroids::tick_asteroid_manager),
collision_listener, collision_listener,
@@ -58,13 +61,16 @@ impl Plugin for AsteroidPlugin {
) )
.add_systems( .add_systems(
FixedPostUpdate, FixedPostUpdate,
(integrate_velocity, update_positions, apply_rotation_to_mesh) (
physics::integrate_velocity,
physics::integrate_angular_velocity,
)
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
) )
.add_event::<asteroids::SpawnAsteroid>() .add_event::<asteroids::SpawnAsteroid>()
.add_event::<event::AsteroidDestroy>() .add_event::<event::AsteroidDestroy>()
.add_event::<event::ShipDestroy>(); .add_event::<event::ShipDestroy>();
app.insert_state(GameState::TitleScreen); app.insert_state(GameState::Playing);
} }
} }
@@ -130,19 +136,6 @@ pub enum GameState {
GameOver, // Game has ended. Present game over dialogue and await user restart GameOver, // Game has ended. Present game over dialogue and await user restart
} }
#[derive(Component)]
struct Position(bevy::math::Vec2);
#[derive(Component)]
struct Velocity(bevy::math::Vec2);
#[derive(Component)]
struct Rotation(f32);
/// Marker for any entity that should wrap on screen edges
#[derive(Component)]
struct Wrapping;
#[derive(Resource, Debug, Deref, Clone, Copy)] #[derive(Resource, Debug, Deref, Clone, Copy)]
struct Score(i32); struct Score(i32);
@@ -244,14 +237,14 @@ fn spawn_camera(mut commands: Commands) {
*/ */
fn input_ship_thruster( fn input_ship_thruster(
keyboard_input: Res<ButtonInput<KeyCode>>, keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut Velocity, &Rotation, &mut Children), With<Ship>>, mut query: Query<(&mut physics::Velocity, &Transform, &mut Children), With<Ship>>,
mut commands: Commands, mut commands: Commands,
game_assets: Res<GameAssets>, game_assets: Res<GameAssets>,
) { ) {
// TODO: Maybe change for a Single<Ship>> so this only runs for the one ship // 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 // buuut... that would silently do nothing if there are 0 or >1 ships, and
// I might want to crash on purpose in that case. // I might want to crash on purpose in that case.
let Ok((mut velocity, rotation, children)) = query.single_mut() else { let Ok((mut velocity, transform, children)) = 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}.");
}; };
@@ -261,7 +254,7 @@ fn input_ship_thruster(
.expect("Couldn't find first child, which should be the thruster"); .expect("Couldn't find first child, which should be the thruster");
if keyboard_input.pressed(KeyCode::KeyW) { if keyboard_input.pressed(KeyCode::KeyW) {
velocity.0 += Vec2::from_angle(rotation.0) * SHIP_THRUST; velocity.0 += (transform.rotation * Vec3::X).xy() * SHIP_THRUST;
commands commands
.entity(*thrusters) .entity(*thrusters)
.insert(MeshMaterial2d(game_assets.thruster_mat_active())); .insert(MeshMaterial2d(game_assets.thruster_mat_active()));
@@ -278,67 +271,19 @@ fn input_ship_thruster(
*/ */
fn input_ship_rotation( 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 AngularVelocity, With<Ship>>,
) { ) {
let Ok(mut rotation) = query.single_mut() else { let Ok(mut angular_vel) = 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}.");
}; };
if keyboard_input.pressed(KeyCode::KeyA) { if keyboard_input.pressed(KeyCode::KeyA) {
rotation.0 += SHIP_ROTATION; angular_vel.0 = SHIP_ROTATION;
} else if keyboard_input.pressed(KeyCode::KeyD) { } else if keyboard_input.pressed(KeyCode::KeyD) {
rotation.0 -= SHIP_ROTATION; angular_vel.0 = -SHIP_ROTATION;
} } else {
} angular_vel.0 = 0.0;
// 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
*/
fn integrate_velocity(mut query: Query<(&mut Position, &Velocity)>, time: Res<Time>) {
for (mut position, velocity) in &mut query {
position.0 += velocity.0 * time.delta_secs();
}
}
fn update_positions(mut query: Query<(&mut Transform, &Position)>) {
for (mut transform, position) in &mut query {
transform.translation.x = position.0.x;
transform.translation.y = position.0.y;
}
}
/*
Assigns the rotation to the transform by copying it from the Rotation component.
*/
fn apply_rotation_to_mesh(mut query: Query<(&mut Transform, &Rotation)>) {
for (mut transform, rotation) in &mut query {
transform.rotation = Quat::from_rotation_z(rotation.0);
}
}
fn wrap_entities(mut query: Query<&mut Position, With<Wrapping>>, world_size: Res<WorldSize>) {
let right = world_size.width / 2.0;
let left = -right;
let top = world_size.height / 2.0;
let bottom = -top;
for mut pos in query.iter_mut() {
if pos.0.x > right {
pos.0.x = left;
} else if pos.0.x < left {
pos.0.x = right;
}
if pos.0.y > top {
pos.0.y = bottom;
} else if pos.0.y < bottom {
pos.0.y = top;
}
} }
} }

59
src/physics.rs Normal file
View File

@@ -0,0 +1,59 @@
//! Custom physics items
//! TODO: Refactor in terms of Rapier2D, *or* implement colliders and remove it.
use crate::WorldSize;
use bevy::prelude::*;
#[derive(Component)]
pub(crate) struct Velocity(pub(crate) bevy::math::Vec2);
#[derive(Component)]
pub(crate) struct AngularVelocity(pub(crate) f32);
/// Marker for any entity that should wrap on screen edges
#[derive(Component)]
pub(crate) struct Wrapping;
/// Integrate linear velocity and update the entity's transform.
pub(crate) fn integrate_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
for (mut transform, velocity) in &mut query {
let delta = velocity.0 * time.delta_secs();
transform.translation += delta.extend(0.0);
}
}
/// Integrate angular velocity and update the entity's transform.
pub(crate) fn integrate_angular_velocity(
mut objects: Query<(&mut Transform, &AngularVelocity)>,
time: Res<Time>,
) {
for (mut transform, ang_vel) in &mut objects {
let delta = ang_vel.0 * time.delta_secs();
transform.rotate_z(delta);
}
}
pub(crate) fn wrap_entities(
mut query: Query<&mut Transform, With<Wrapping>>,
world_size: Res<WorldSize>,
) {
let right = world_size.width / 2.0;
let left = -right;
let top = world_size.height / 2.0;
let bottom = -top;
for mut pos in query.iter_mut() {
if pos.translation.x > right {
pos.translation.x = left;
} else if pos.translation.x < left {
pos.translation.x = right;
}
if pos.translation.y > top {
pos.translation.y = bottom;
} else if pos.translation.y < bottom {
pos.translation.y = top;
}
}
}

View File

@@ -1,10 +1,10 @@
use crate::GameState;
use bevy::{ use bevy::{
color::palettes::css::{BLACK, GREEN, LIGHT_BLUE, RED}, color::palettes::css::{BLACK, GREEN, LIGHT_BLUE, RED},
prelude::*, prelude::*,
}; };
use crate::GameState;
pub fn preparation_widget_plugin(app: &mut App) { pub fn preparation_widget_plugin(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_get_ready)

View File

@@ -1,8 +1,11 @@
use crate::{
AngularVelocity, GameAssets,
physics::{Velocity, Wrapping},
};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use crate::{GameAssets, Position, Rotation, Velocity, Wrapping};
#[derive(Component)] #[derive(Component)]
pub struct Ship; pub struct Ship;
@@ -15,9 +18,8 @@ pub fn spawn_player(mut commands: Commands, game_assets: Res<GameAssets>) {
ActiveCollisionTypes::STATIC_STATIC, ActiveCollisionTypes::STATIC_STATIC,
Ship, Ship,
Wrapping, Wrapping,
Position(Vec2::default()),
Velocity(Vec2::ZERO), Velocity(Vec2::ZERO),
Rotation(0.0), AngularVelocity(0.0),
Mesh2d(game_assets.ship().0), Mesh2d(game_assets.ship().0),
MeshMaterial2d(game_assets.ship().1), MeshMaterial2d(game_assets.ship().1),
Transform::default().with_scale(Vec3::new(20.0, 20.0, 20.0)), Transform::default().with_scale(Vec3::new(20.0, 20.0, 20.0)),