M crates/unified/assets/config/world.wc.toml => crates/unified/assets/config/world.wc.toml +4 -1
@@ 4,4 4,7 @@ gravity = 2
[part]
default_height = 50
default_width = 50
-default_mass = 100>
\ No newline at end of file
+default_mass = 100
+
+[hearty]
+thrust = 50000
M crates/unified/src/client/incoming_parts.rs => crates/unified/src/client/incoming_parts.rs +1 -1
@@ 41,4 41,4 @@ fn handle_updated_parts(mut commands: Commands, mut updated_parts: Query<(Entity
}));
info!(?updated_part, "updated part");
}
-}>
\ No newline at end of file
+}
A crates/unified/src/client/key_input.rs => crates/unified/src/client/key_input.rs +36 -0
@@ 0,0 1,36 @@
+use bevy::{app::{App, Update}, ecs::{event::EventWriter, system::Res}, input::{keyboard::KeyCode, ButtonInput}};
+
+use crate::ecs::ThrustEvent;
+
+pub fn key_input_plugin(app: &mut App) {
+ app.add_systems(Update, directional_keys);
+}
+
+pub fn directional_keys(
+ keys: Res<ButtonInput<KeyCode>>,
+ mut thrust_event: EventWriter<ThrustEvent>,
+) {
+ if keys.just_pressed(KeyCode::KeyW) || keys.just_pressed(KeyCode::ArrowUp) {
+ thrust_event.write(ThrustEvent::Up(true));
+ } else if keys.just_released(KeyCode::KeyW) || keys.just_released(KeyCode::ArrowUp) {
+ thrust_event.write(ThrustEvent::Up(false));
+ }
+
+ if keys.just_pressed(KeyCode::KeyS) || keys.just_pressed(KeyCode::ArrowDown) {
+ thrust_event.write(ThrustEvent::Down(true));
+ } else if keys.just_released(KeyCode::KeyS) || keys.just_released(KeyCode::ArrowDown) {
+ thrust_event.write(ThrustEvent::Down(false));
+ }
+
+ if keys.just_pressed(KeyCode::KeyA) || keys.just_pressed(KeyCode::ArrowLeft) {
+ thrust_event.write(ThrustEvent::Left(true));
+ } else if keys.just_released(KeyCode::KeyA) || keys.just_released(KeyCode::ArrowLeft) {
+ thrust_event.write(ThrustEvent::Left(false));
+ }
+
+ if keys.just_pressed(KeyCode::KeyD) || keys.just_pressed(KeyCode::ArrowRight) {
+ thrust_event.write(ThrustEvent::Right(true));
+ } else if keys.just_released(KeyCode::KeyD) || keys.just_released(KeyCode::ArrowRight) {
+ thrust_event.write(ThrustEvent::Right(false));
+ }
+}
M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +5 -2
@@ 1,5 1,6 @@
mod incoming_planets;
mod incoming_parts;
+mod key_input;
use std::net::{SocketAddr, UdpSocket};
use std::time::SystemTime;
@@ 13,6 14,7 @@ use bevy_replicon_renet2::renet2::{ConnectionConfig, RenetClient};
use bevy_replicon_renet2::RenetChannelsExt;
use crate::client::incoming_parts::incoming_parts_plugin;
use crate::client::incoming_planets::incoming_planets_plugin;
+use crate::client::key_input::key_input_plugin;
use crate::ecs::{Ball, CursorWorldCoordinates, Ground, MainCamera, Player, SendBallHere};
pub struct ClientPlugin {
@@ 50,7 52,8 @@ impl Plugin for ClientPlugin {
.add_systems(Update, follow_camera)
.add_systems(Update, find_me)
.add_plugins(incoming_planets_plugin)
- .add_plugins(incoming_parts_plugin);
+ .add_plugins(incoming_parts_plugin)
+ .add_plugins(key_input_plugin);
}
}
@@ 94,4 97,4 @@ fn update_cursor_position(
} else {
coords.0 = None;
}
-}>
\ No newline at end of file
+}
M crates/unified/src/config/world.rs => crates/unified/src/config/world.rs +7 -1
@@ 6,6 6,7 @@ use serde::Deserialize;
pub struct GlobalWorldConfig {
pub world: WorldConfig,
pub part: PartConfig,
+ pub hearty: HeartyConfig,
}
#[derive(Deserialize, Asset, TypePath, Clone)]
@@ 18,4 19,9 @@ pub struct PartConfig {
pub default_width: f32,
pub default_height: f32,
pub default_mass: f32
-}>
\ No newline at end of file
+}
+
+#[derive(Deserialize, Asset, TypePath, Clone)]
+pub struct HeartyConfig {
+ pub thrust: f32,
+}
M crates/unified/src/ecs.rs => crates/unified/src/ecs.rs +17 -1
@@ 19,6 19,14 @@ pub struct CursorWorldCoordinates(pub Option<Vec2>);
#[derive(Debug, Default, Deserialize, Event, Serialize)]
pub struct SendBallHere(pub Vec2);
+#[derive(Debug, Deserialize, Event, Serialize)]
+pub enum ThrustEvent {
+ Up(bool),
+ Down(bool),
+ Left(bool),
+ Right(bool),
+}
+
#[derive(Component, Serialize, Deserialize, Debug)]
#[require(ReadMassProperties, RigidBody::Dynamic, ExternalForce, ExternalImpulse)]
pub struct Part {
@@ 38,4 46,12 @@ pub struct PartBundle {
#[derive(Component, Serialize, Deserialize, Debug)]
pub struct Player {
pub client: Entity
-}>
\ No newline at end of file
+}
+
+#[derive(Component, Default, Serialize, Deserialize, Debug)]
+pub struct PlayerThrust {
+ pub up: bool,
+ pub down: bool,
+ pub left: bool,
+ pub right: bool,
+}
M crates/unified/src/server/gravity.rs => crates/unified/src/server/gravity.rs +2 -6
@@ 5,7 5,7 @@ use crate::config::planet::Planet;
use crate::ecs::Part;
use crate::server::world_config::WorldConfigResource;
-pub fn newtonian_gravity_plugin(mut app: &mut App) {
+pub fn newtonian_gravity_plugin(app: &mut App) {
app.add_systems(Update, update_gravity);
}
@@ 25,10 25,6 @@ fn update_gravity(
let Some(world_config) = &world_config.config else { return; };
for (part_transform, part_mass, mut forces, mut impulses) in &mut part_query {
- forces.force = Vec2::ZERO;
- impulses.impulse = Vec2::ZERO;
- forces.torque = 0.0;
-
let part_mass = part_mass.mass;
let part_translation = part_transform.translation;
@@ 43,4 39,4 @@ fn update_gravity(
forces.force += direction.xy();
}
}
-}>
\ No newline at end of file
+}
M crates/unified/src/server/mod.rs => crates/unified/src/server/mod.rs +1 -1
@@ 55,4 55,4 @@ impl Plugin for ServerPlugin {
.add_plugins(newtonian_gravity_plugin)
.add_plugins(player_management_plugin);
}
-}>
\ No newline at end of file
+}
M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +85 -6
@@ 1,12 1,26 @@
+use std::f32::consts::PI;
+
use bevy::prelude::*;
-use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider, MassProperties, ReadMassProperties, RigidBody};
-use bevy_replicon::prelude::{ConnectedClient, Replicated};
+use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider, ExternalForce, ExternalImpulse, MassProperties, ReadMassProperties, RigidBody, Sensor};
+use bevy_replicon::prelude::{ConnectedClient, FromClient, Replicated};
use crate::config::planet::Planet;
-use crate::ecs::{Part, PartBundle, Player};
+use crate::ecs::{Part, PartBundle, Player, PlayerThrust, ThrustEvent};
use crate::server::world_config::WorldConfigResource;
-pub fn player_management_plugin(mut app: &mut App) {
- app.add_systems(Update, handle_new_players);
+pub fn player_management_plugin(app: &mut App) {
+ app
+ .add_systems(PreUpdate, reset_movement)
+ .add_systems(Update, (handle_new_players, player_thrust));
+}
+
+fn reset_movement(
+ mut players: Query<(&mut ExternalForce, &mut ExternalImpulse)>
+) {
+ for (mut force, mut impulse) in &mut players {
+ force.force = Vec2::ZERO;
+ force.torque = 0.0;
+ impulse.impulse = Vec2::ZERO;
+ }
}
fn handle_new_players(mut commands: Commands, q_new_clients: Query<Entity, Added<ConnectedClient>>, world_config: Res<WorldConfigResource>, planets: Query<(&Transform, &Planet)>) {
@@ 38,8 52,73 @@ fn handle_new_players(mut commands: Commands, q_new_clients: Query<Entity, Added
})
})
.insert(Replicated)
+ .insert(ExternalForce::default())
+ .insert(PlayerThrust::default())
.insert(Player {
client: joined_player,
});
}
-}>
\ No newline at end of file
+}
+
+fn player_thrust(
+ mut players: Query<(&Transform, &mut ExternalForce, &mut PlayerThrust)>,
+ mut thrust_event: EventReader<FromClient<ThrustEvent>>,
+ world_config: Res<WorldConfigResource>,
+) {
+ use ThrustEvent::*;
+ for event in thrust_event.read() {
+ let FromClient { client_entity, event } = event;
+ let Ok((_, _, mut thrust)) = players.get_mut(*client_entity) else { continue };
+ match *event {
+ Up(on) => thrust.up = on,
+ Down(on) => thrust.down = on,
+ Left(on) => thrust.left = on,
+ Right(on) => thrust.right = on,
+ }
+ }
+ for (transform, mut force, thrust) in &mut players {
+ let Some(world_config) = &world_config.config else { return; };
+
+ let forward = (transform.rotation * Vec3::Y).xy();
+ let mut external_force = ExternalForce::default();
+ let mut thrusters = [0.0; 4]; // counterclockwise wrt +y axis
+ if thrust.up {
+ thrusters[1] = 1.0;
+ thrusters[2] = 1.0;
+ }
+ if thrust.down {
+ thrusters[0] = 1.0;
+ thrusters[3] = 1.0;
+ }
+ if thrust.left {
+ thrusters[0] = 1.0;
+ thrusters[2] = 1.0;
+ }
+ if thrust.right {
+ thrusters[1] = 1.0;
+ thrusters[3] = 1.0;
+ }
+ let half_size = Vec2::new(world_config.part.default_width/2.0, world_config.part.default_height/2.0).length();
+ external_force += ExternalForce::at_point(
+ -forward*thrusters[0]*world_config.hearty.thrust,
+ transform.translation.xy() + half_size*Vec2::new((1.0*PI/4.0).cos(), (1.0*PI/4.0).sin()).rotate(forward),
+ transform.translation.xy(),
+ );
+ external_force += ExternalForce::at_point(
+ forward*thrusters[1]*world_config.hearty.thrust,
+ transform.translation.xy() + half_size*Vec2::new((3.0*PI/4.0).cos(), (3.0*PI/4.0).sin()).rotate(forward),
+ transform.translation.xy(),
+ );
+ external_force += ExternalForce::at_point(
+ forward*thrusters[2]*world_config.hearty.thrust,
+ transform.translation.xy() + half_size*Vec2::new((5.0*PI/4.0).cos(), (5.0*PI/4.0).sin()).rotate(forward),
+ transform.translation.xy(),
+ );
+ external_force += ExternalForce::at_point(
+ -forward*thrusters[3]*world_config.hearty.thrust,
+ transform.translation.xy() + half_size*Vec2::new((7.0*PI/4.0).cos(), (7.0*PI/4.0).sin()).rotate(forward),
+ transform.translation.xy(),
+ );
+ *force += external_force;
+ }
+}
M crates/unified/src/shared_plugins.rs => crates/unified/src/shared_plugins.rs +4 -3
@@ 1,9 1,9 @@
use bevy::app::{App, PluginGroup, PluginGroupBuilder};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
-use bevy_replicon::prelude::{AppRuleExt, Channel, ClientEventAppExt};
+use bevy_replicon::prelude::{AppRuleExt, Channel, ClientEventAppExt, FromClient, ServerEventAppExt};
use crate::config::planet::Planet;
-use crate::ecs::{Ball, Ground, Part, Player, SendBallHere};
+use crate::ecs::{Ball, Ground, Part, Player, SendBallHere, ThrustEvent};
pub struct SharedPluginGroup;
@@ 21,6 21,7 @@ impl PluginGroup for SharedPluginGroup {
pub fn register_everything(app: &mut App) {
app
.add_client_event::<SendBallHere>(Channel::Ordered)
+ .add_client_event::<ThrustEvent>(Channel::Ordered)
.replicate::<Transform>()
.replicate::<Ball>()
.replicate::<Ground>()
@@ 40,4 41,4 @@ fn setup_physics(
) {
let mut cfg = rapier_config.single_mut().unwrap();
cfg.gravity = Vec2::ZERO;
-}>
\ No newline at end of file
+}