M crates/unified/assets/config/parts/hearty.part.toml => crates/unified/assets/config/parts/hearty.part.toml +4 -4
@@ 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"
M crates/unified/src/client/ship/thrusters.rs => crates/unified/src/client/ship/thrusters.rs +14 -12
@@ 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<ActionState<ClientAction>>,
mut solution: ResMut<ThrustSolution>,
- mut events: MessageWriter<ThrustEvent>,
+ mut events: MessageWriter<ThrustSolution>,
) {
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;
}
M crates/unified/src/server/player/thrust.rs => crates/unified/src/server/player/thrust.rs +68 -35
@@ 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<FromClient<ThrustSolution>>,
clients: Query<&ConnectedNetworkEntity>,
q_ls_me: Query<Entity, With<crate::ecs::Me>>,
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<Forces, With<Part>>,
+) {
+ //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
M crates/unified/src/thrust.rs => crates/unified/src/thrust.rs +4 -2
@@ 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<Entity>,