From 75f5204cc96240f4c96bb0205786462a6633b18c Mon Sep 17 00:00:00 2001 From: core Date: Sun, 23 Nov 2025 21:52:06 -0500 Subject: [PATCH] feat: worky thrusters --- .../assets/config/parts/hearty.part.toml | 8 +- crates/unified/src/client/ship/thrusters.rs | 26 +++-- crates/unified/src/server/player/thrust.rs | 103 ++++++++++++------ crates/unified/src/thrust.rs | 6 +- 4 files changed, 90 insertions(+), 53 deletions(-) diff --git a/crates/unified/assets/config/parts/hearty.part.toml b/crates/unified/assets/config/parts/hearty.part.toml index 51ad38f8638bb9bbd6f2efd2ab04c87e6ceffe43..bc67795ff772eceb635804cbd7744b2923b22771 100644 --- a/crates/unified/assets/config/parts/hearty.part.toml +++ b/crates/unified/assets/config/parts/hearty.part.toml @@ -11,22 +11,22 @@ mass = 100 [[thruster]] id = "bottom left" apply_force_at_local = [ -25.0, -25.0 ] -thrust_vector = [ 0.0, 20.0 ] +thrust_vector = [ 0.0, 2500.0 ] [[thruster]] id = "bottom right" apply_force_at_local = [ 25.0, -25.0 ] -thrust_vector = [ 0.0, 20.0 ] +thrust_vector = [ 0.0, 2500.0 ] [[thruster]] id = "top left" apply_force_at_local = [ -25.0, 25.0 ] -thrust_vector = [ 0.0, -20.0 ] +thrust_vector = [ 0.0, -2500.0 ] [[thruster]] id = "top right" apply_force_at_local = [ 25.0, 25.0 ] -thrust_vector = [ 0.0, -20.0 ] +thrust_vector = [ 0.0, -2500.0 ] [[joint]] id = "Top" diff --git a/crates/unified/src/client/ship/thrusters.rs b/crates/unified/src/client/ship/thrusters.rs index effa53f3d06b874494dd36bb49e77c751352264c..765578a71a837d79360bf73045eab3a04f24b729 100644 --- a/crates/unified/src/client/ship/thrusters.rs +++ b/crates/unified/src/client/ship/thrusters.rs @@ -11,7 +11,7 @@ use crate::client::input::ClientAction; use crate::ecs::thruster::{PartThrusters, Thruster, ThrusterOfPart}; use crate::prelude::*; use crate::client::input::util::ActionStateExt; -use crate::ecs::{Me, ThrustEvent}; +use crate::ecs::Me; use crate::thrust::ThrustSolution; pub fn client_thrusters_plugin(app: &mut App) { @@ -46,9 +46,10 @@ fn draw_thruster_debug( if !thrust_solution.converged { color = RED; } + let rescaled_thrust_vector = thruster.0.thrust_vector / 200.0; gizmos.arrow_2d( thruster.2.translation().xy(), - thruster.2.translation().xy() + thruster.2.rotation().mul_vec3(thruster.0.thrust_vector.extend(0.0)).xy(), + thruster.2.translation().xy() + thruster.2.rotation().mul_vec3(rescaled_thrust_vector.extend(0.0)).xy(), color ); } @@ -60,7 +61,7 @@ fn solve_thrust( thrusters: Query<(&Thruster, &GlobalTransform)>, input: Res>, mut solution: ResMut, - mut events: MessageWriter, + mut events: MessageWriter, ) { if !( input.button_changed(&ClientAction::ThrustForward) @@ -71,7 +72,7 @@ fn solve_thrust( || input.button_changed(&ClientAction::ThrustLeft) ) { return; /* no changes, existing thrust solution is valid */ } - debug!("input changed, recalculating thrust solution"); + trace!("input changed, recalculating thrust solution"); let start = Instant::now(); solution.thrusters_on.clear(); solution.converged = false; @@ -111,10 +112,10 @@ fn solve_thrust( } if target_unit_vector == Vec3::ZERO { - debug!("no buttons are pressed; zeroing thrust solution"); - debug!("solved thrust in {}ms", start.elapsed().as_millis()); + trace!("no buttons are pressed; zeroing thrust solution"); + trace!("solved thrust in {}ms", start.elapsed().as_millis()); solution.converged = true; - events.write(ThrustEvent(solution.clone())); + events.write(solution.clone()); return; } @@ -154,7 +155,7 @@ fn solve_thrust( } // calculate thrust and torque values - debug!("found {} thrusters, computing coefficients", all_thrusters.len()); + trace!("found {} thrusters, computing coefficients", all_thrusters.len()); for thruster in &all_thrusters { trace!("thruster on ship: {:?}", thruster); @@ -197,8 +198,8 @@ fn solve_thrust( } }; - debug!("found thrust solution!"); - debug!("solution alignment (higher is better): {}", ssolution.objective()); + trace!("found thrust solution!"); + trace!("solution alignment (higher is better): {}", ssolution.objective()); let mut new_soln = ThrustSolution { thrusters_on: BTreeSet::default(), @@ -212,8 +213,9 @@ fn solve_thrust( } } - debug!("found thrust solution in {:?}", start.elapsed()); + let elapsed = start.elapsed(); + debug!(?elapsed, ?target_unit_vector, "solved for thrust"); *solution = new_soln; - events.write(ThrustEvent(solution.clone())); + events.write(solution.clone()); return; } diff --git a/crates/unified/src/server/player/thrust.rs b/crates/unified/src/server/player/thrust.rs index 720b856dc7e4ad1119e8a9fe292f64407eb8ab1b..41486cf1f57304b944dbeae9b8822ea02aa50e2a 100644 --- a/crates/unified/src/server/player/thrust.rs +++ b/crates/unified/src/server/player/thrust.rs @@ -1,23 +1,36 @@ +//! # Server thrust handling +//! The bulk of the actual thrust work is done in the thrust solver on the client. +//! It sends us it's `ThrustSolution` when it's done; in this file we process it +//! and apply it to the physics simulation. + +use crate::attachment::Parts; +use crate::ecs::Part; +use crate::ecs::thruster::{PartThrusters, Thruster, ThrusterOfPart}; use crate::prelude::*; use crate::server::ConnectedNetworkEntity; +use crate::thrust::ThrustSolution; pub fn server_thrust_plugin(app: &mut App) { app - .add_systems(Update, process_thrust_events); + .add_systems(Update, process_thrust_events) + .add_systems(Update, apply_thrust_solutions); } -pub fn process_thrust_events( +/// Handle new `ThrustSolution`s from clients as they come in +fn process_thrust_events( mut events: MessageReader>, clients: Query<&ConnectedNetworkEntity>, q_ls_me: Query>, mut commands: Commands ) { + // For each event from a client... for FromClient { client_id, message: thrust_solution, } in events.read() { + // Find the hearty entity of the player... let player_hearty_entity = match client_id { ClientId::Client(client_entity) => { let ConnectedNetworkEntity { @@ -27,44 +40,64 @@ pub fn process_thrust_events( }, ClientId::Server => &q_ls_me.iter().next().unwrap() }; -/ - commands.entity(player_hearty_entity).insert(thrust_solution); + + // and apply the new thrust solution + commands.entity(*player_hearty_entity).insert(thrust_solution.clone()); trace!("installed thrust solution {:?}", thrust_solution); + } +} - /* TODO: @tm85: have fun! - TODO: The ThrustSolution contains a set of thrusters that should be on. - TODO: All other thrusters should be off. - TODO: If a thruster should be on, apply it's force vector at it's point - TODO: (RigidBodyForces::apply_force_at_point, - TODO: note this is worldspace!! [GlobalTransform is your friend]) +/// Find all players, and apply their current `ThrustSolution`s +fn apply_thrust_solutions( + players: Query<(Entity, &ThrustSolution, Option<&Parts>)>, + thrusters: Query<(&Thruster, &ThrusterOfPart, &GlobalTransform)>, + mut parts: Query>, +) { + //gizmos.arrow_2d( + // thruster.2.translation().xy(), + // thruster.2.translation().xy() + thruster.2.rotation().mul_vec3(thruster.0.thrust_vector.extend(0.0)).xy(), + // color + // ); - TODO: Note that this is only an event handler, and will only run - TODO: when the client sends us a NEW thrust solution. + // Iterate through all players with a ThrustSolution + for (player_entity, thrust_solution, maybe_parts) in players { + // If their reported thrust solution didn't converge, do nothing + if !thrust_solution.converged { + debug!(?player_entity, "ignoring unconverged thrust solution"); + } + // If it's empty, exit early (no reason to waste time) + if thrust_solution.thrusters_on.is_empty() { continue } - TODO: You probably want to add a Component to the player entity (player_hearty_entity, - TODO: it's also the hearty part entity) that stores the current ThrustSolution - TODO: and applies it every tick, since avian's ExternalForces only apply for a single - TODO: physics tick and must be applied every tick to be continuous. - TODO: Hint: you probably want a mut commands: Commands in this system, - TODO: and then insert a component with commands.entity(hearty).insert(YourNewComponent) + // Collect a list of all parts in the player's ship, to validate + // the thrusters the player sends + let attached_parts = if let Some(parts) = maybe_parts { + parts.as_slice() + } else { + &[] + }; - TODO: To do this, create a new system (e.g. apply_thrust) or something, and apply thrust - TODO: there, every tick (Update). Register it with the plugin above. + // Go over each active thruster in the thrust solution... + for thruster_entity in &thrust_solution.thrusters_on { + // ...load it's data... + let Ok((thruster_info, parent_part, thruster_transform)) = thrusters.get(*thruster_entity) else { + debug!(?thruster_entity, "couldn't find thruster to apply force"); + continue + }; - TODO: I'll leave you to that. Have fun! ping me if you've got questions, I'll be up - TODO: a while so can help you decipher the attachment system if need be. + // ...verify this user is allowed to control this thruster... + let parent_is_hearty = parent_part.0 == player_entity; + let parent_is_in_ship = attached_parts.contains(&parent_part.0); + if !(parent_is_hearty || parent_is_in_ship) { + debug!(?thruster_entity, "ignoring disallowed thruster action"); + continue + } // not permitted - TODO: Overall, your goal is: - TODO: - find all parts in the ship (hint: query for Parts on the ship (hearty)) - TODO: - for each part, find all thrusters (hint: query for PartThrusters on the part) - TODO: - for each thruster, if it's on (check if it's in the player's current thrust soln) - TODO: - apply a force at the correct point - TODO: Much of the same logic is done in the client counterpart, to calculate effective - TODO: force for the solver. Take a look at client::ship::thrusters. - TODO: Note that relationship components (eg Parts, PartThrusters) wont exist if there are - TODO: no children - eg hearty won't have Parts if nothing is attached, a part won't have - TODO: PartThrusters if it has no thrusters. - TODO: Handle this with Option<&Component> in the query - */ + // great, it's valid; apply the force + let mut part_forces = parts.get_mut(parent_part.0).unwrap(); + part_forces.apply_force_at_point( + (thruster_transform.rotation() * thruster_info.thrust_vector.extend(0.0)).xy(), + thruster_transform.translation().xy() + ); + } } -} +} \ No newline at end of file diff --git a/crates/unified/src/thrust.rs b/crates/unified/src/thrust.rs index 4541a24dcfd0153f35692accea22ed120b9056b4..e7c03b2572777251e77b794b63714734a78f963e 100644 --- a/crates/unified/src/thrust.rs +++ b/crates/unified/src/thrust.rs @@ -1,11 +1,13 @@ use std::collections::BTreeSet; -use bevy::prelude::{Entity, Resource, Compenent}; +use bevy::ecs::entity::MapEntities; +use bevy::prelude::{Entity, Resource}; use serde::{Deserialize, Serialize}; +use crate::prelude::{Component, Message}; /// A thrust solution, found by the thrust solver on the client. /// `thrusters_on` is the set of thrusters that should be on. /// Any thrusters not in this set should be off. -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Resource, Component, MapEntities)] +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Resource, Component, MapEntities, Message)] pub struct ThrustSolution { #[entities] pub thrusters_on: BTreeSet,