From fdee4cf37c2051b141ea0e0234abc0f6d9978abe Mon Sep 17 00:00:00 2001 From: PROMETHIA-27 <42193387+PROMETHIA-27@users.noreply.github.com> Date: Sun, 20 Jul 2025 09:43:10 -0400 Subject: [PATCH] Controller Replication (#57) --- Cargo.lock | 4 +- Cargo.toml | 5 +- crates/client/src/client.rs | 41 ++++++++++- crates/server/src/server.rs | 7 +- crates/shared/src/camera.rs | 5 +- crates/shared/src/character.rs | 3 +- .../shared/src/control/controller_common.rs | 38 +++++++++- crates/shared/src/control/controls.rs | 3 +- crates/shared/src/head.rs | 3 +- crates/shared/src/heads/mod.rs | 5 +- crates/shared/src/player.rs | 73 ++++++++++--------- crates/shared/src/protocol.rs | 58 ++++++++++++++- crates/shared/src/utils/mod.rs | 1 + crates/shared/src/utils/run_conditions.rs | 5 ++ justfile | 4 +- 15 files changed, 198 insertions(+), 57 deletions(-) create mode 100644 crates/shared/src/utils/run_conditions.rs diff --git a/Cargo.lock b/Cargo.lock index c330c43..4d61c95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3665,10 +3665,11 @@ dependencies = [ [[package]] name = "happy_feet" version = "0.1.0" -source = "git+https://github.com/PROMETHIA-27/happy_feet.git?rev=5a87760d8a7970c74e07f30bc31ceaafad9a69b6#5a87760d8a7970c74e07f30bc31ceaafad9a69b6" +source = "git+https://github.com/atornity/happy_feet.git?rev=1b24ed95f166e63af35e7b6f9f0053d6d28e1f1a#1b24ed95f166e63af35e7b6f9f0053d6d28e1f1a" dependencies = [ "avian3d", "bevy", + "serde", ] [[package]] @@ -4411,6 +4412,7 @@ dependencies = [ "lightyear_interpolation", "lightyear_link", "lightyear_messages", + "lightyear_prediction", "lightyear_replication", "lightyear_sync", "lightyear_transport", diff --git a/Cargo.toml b/Cargo.toml index 14b1d17..5bdb91b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,13 @@ bevy_common_assets = { version = "0.13.0", features = ["ron"] } bevy_debug_log = "0.6.0" bevy_sprite3d = "5.0.0" bevy_trenchbroom = { version = "0.8.1", features = ["avian"] } -happy_feet = { git = "https://github.com/PROMETHIA-27/happy_feet.git", rev = "5a87760d8a7970c74e07f30bc31ceaafad9a69b6" } +happy_feet = { git = "https://github.com/atornity/happy_feet.git", rev = "1b24ed95f166e63af35e7b6f9f0053d6d28e1f1a", features = [ + "serde", +] } lightyear = { git = "https://github.com/cBournhonesque/lightyear.git", rev = "03cbf419a2c0595261b64420bc0332fc3fe1cc3f", default-features = false, features = [ "interpolation", "netcode", + "prediction", "replication", "std", "steam", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 6a927d3..8e20173 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,20 +1,37 @@ use bevy::prelude::*; use lightyear::{ + connection::client::ClientState, netcode::Key, prelude::{client::NetcodeConfig, *}, }; +use shared::{GameState, heads_database::HeadsDatabase, tb_entities::SpawnPoint}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; pub fn plugin(app: &mut App) { app.add_systems(Startup, temp_connect_on_startup); + app.add_systems( + FixedUpdate, + spawn_disconnected_player.run_if(in_state(GameState::Playing)), + ); } fn temp_connect_on_startup(mut commands: Commands) -> Result { - let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25564); + let mut args = std::env::args(); + let client_port = loop { + match args.next().as_deref() { + Some("--port") => { + break args.next().unwrap().parse::().unwrap(); + } + Some(_) => (), + None => break 25564, + } + }; + + let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), client_port); let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565); let auth = Authentication::Manual { server_addr, - client_id: 0, + client_id: client_port as u64, private_key: Key::default(), protocol_id: 0, }; @@ -26,10 +43,28 @@ fn temp_connect_on_startup(mut commands: Commands) -> Result { PeerAddr(server_addr), Link::new(None), ReplicationReceiver::default(), - client::NetcodeClient::new(auth, NetcodeConfig::default())?, + client::NetcodeClient::new( + auth, + NetcodeConfig { + client_timeout_secs: 1, + ..default() + }, + )?, UdpIo::default(), )) .trigger(Connect); Ok(()) } + +fn spawn_disconnected_player( + disconnected: Single<&Client, Changed>, + commands: Commands, + asset_server: Res, + query: Query<&Transform, With>, + heads_db: Res, +) { + if disconnected.state == ClientState::Disconnected { + shared::player::spawn(commands, query, asset_server, heads_db) + } +} diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 53011f2..2c0fbe1 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -3,7 +3,7 @@ use lightyear::prelude::{ server::{NetcodeConfig, NetcodeServer, ServerUdpIo}, *, }; -use shared::utils::commands::IsServer; +use shared::{heads_database::HeadsDatabase, tb_entities::SpawnPoint, utils::commands::IsServer}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; pub fn plugin(app: &mut App) { @@ -17,6 +17,9 @@ fn handle_new_client( trigger: Trigger, mut commands: Commands, id: Query<&PeerAddr>, + asset_server: Res, + query: Query<&Transform, With>, + heads_db: Res, ) -> Result { let id = id.get(trigger.target())?; @@ -26,6 +29,8 @@ fn handle_new_client( .entity(trigger.target()) .insert(ReplicationSender::default()); + shared::player::spawn(commands, query, asset_server, heads_db); + Ok(()) } diff --git a/crates/shared/src/camera.rs b/crates/shared/src/camera.rs index 7c96aeb..05ae325 100644 --- a/crates/shared/src/camera.rs +++ b/crates/shared/src/camera.rs @@ -3,11 +3,12 @@ use crate::{ }; use avian3d::prelude::*; use bevy::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Component, Reflect, Debug)] +#[derive(Component, Reflect, Debug, Serialize, Deserialize, PartialEq)] pub struct CameraTarget; -#[derive(Component, Reflect, Debug)] +#[derive(Component, Reflect, Debug, Serialize, Deserialize, PartialEq)] pub struct CameraArmRotation; /// Requested camera rotation based on various input sources (keyboard, gamepad) diff --git a/crates/shared/src/character.rs b/crates/shared/src/character.rs index 721186c..85c4ecb 100644 --- a/crates/shared/src/character.rs +++ b/crates/shared/src/character.rs @@ -10,12 +10,13 @@ use bevy::{ animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap, prelude::*, scene::SceneInstanceReady, }; +use serde::{Deserialize, Serialize}; use std::{f32::consts::PI, time::Duration}; #[derive(Component, Debug)] pub struct ProjectileOrigin; -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, PartialEq)] pub struct AnimatedCharacter { head: usize, } diff --git a/crates/shared/src/control/controller_common.rs b/crates/shared/src/control/controller_common.rs index 0e9ea18..0d02115 100644 --- a/crates/shared/src/control/controller_common.rs +++ b/crates/shared/src/control/controller_common.rs @@ -5,16 +5,22 @@ use crate::{ animation::AnimationFlags, character::HasCharacterAnimations, control::{Controls, SelectedController, controls::ControllerSettings}, - heads_database::HeadControls, + head::ActiveHead, + heads_database::{HeadControls, HeadsDatabase}, + physics_layers::GameLayer, player::{Player, PlayerBodyMesh}, }; use avian3d::{math::*, prelude::*}; -use bevy::prelude::*; +use bevy::{ + ecs::{component::HookContext, world::DeferredWorld}, + prelude::*, +}; use happy_feet::prelude::{ Character, CharacterDrag, CharacterGravity, CharacterMovement, CharacterPlugins, GroundFriction, Grounding, GroundingConfig, KinematicVelocity, MoveInput, SteppingBehaviour, SteppingConfig, }; +use serde::{Deserialize, Serialize}; pub fn plugin(app: &mut App) { app.add_plugins(CharacterPlugins::default()); @@ -148,10 +154,28 @@ fn decelerate( } } -#[derive(Component, Reflect)] +#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)] #[reflect(Component)] pub struct MovementSpeedFactor(pub f32); +#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)] +#[reflect(Component)] +#[component(on_add = add_controller_bundle)] +pub struct PlayerCharacterController; + +fn add_controller_bundle(mut world: DeferredWorld, ctx: HookContext) { + let head = world + .entity(ctx.entity) + .get::() + .expect("player must be spawned with an `ActiveHead`") + .0; + let controls = world.resource::().head_stats(head).controls; + world + .commands() + .entity(ctx.entity) + .insert(CharacterControllerBundle::new(controls)); +} + /// A bundle that contains the components needed for a basic /// kinematic character controller. #[derive(Bundle)] @@ -163,11 +187,13 @@ pub struct CharacterControllerBundle { collision_events: CollisionEventsEnabled, movement_config: MovementConfig, interpolation: TransformInterpolation, + layers: CollisionLayers, } impl CharacterControllerBundle { - pub fn new(collider: Collider, controls: HeadControls) -> Self { + pub fn new(controls: HeadControls) -> Self { // Create shape caster as a slightly smaller version of collider + let collider = Collider::capsule(0.9, 1.2); let mut caster_shape = collider.clone(); caster_shape.set_scale(Vector::ONE * 0.98, 10); @@ -184,6 +210,10 @@ impl CharacterControllerBundle { collision_events: CollisionEventsEnabled, movement_config: config, interpolation: TransformInterpolation, + layers: CollisionLayers::new( + LayerMask(GameLayer::Player.to_bits()), + LayerMask::ALL & !GameLayer::CollectiblePhysics.to_bits(), + ), } } } diff --git a/crates/shared/src/control/controls.rs b/crates/shared/src/control/controls.rs index 4ae5cb6..c11586b 100644 --- a/crates/shared/src/control/controls.rs +++ b/crates/shared/src/control/controls.rs @@ -14,6 +14,7 @@ use bevy::{ }, prelude::*, }; +use serde::{Deserialize, Serialize}; pub fn plugin(app: &mut App) { app.init_resource::(); @@ -43,7 +44,7 @@ pub fn plugin(app: &mut App) { ); } -#[derive(Component, Clone, PartialEq, Reflect)] +#[derive(Component, Clone, PartialEq, Reflect, Serialize, Deserialize)] #[reflect(Component)] pub struct ControllerSettings { pub deceleration_factor: f32, diff --git a/crates/shared/src/head.rs b/crates/shared/src/head.rs index 20faa8b..ed4436a 100644 --- a/crates/shared/src/head.rs +++ b/crates/shared/src/head.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, PartialEq)] pub struct ActiveHead(pub usize); diff --git a/crates/shared/src/heads/mod.rs b/crates/shared/src/heads/mod.rs index 9789a1a..417c236 100644 --- a/crates/shared/src/heads/mod.rs +++ b/crates/shared/src/heads/mod.rs @@ -12,6 +12,7 @@ use crate::{ sounds::PlaySound, }; use bevy::prelude::*; +use serde::{Deserialize, Serialize}; pub static HEAD_COUNT: usize = 18; pub static HEAD_SLOTS: usize = 5; @@ -21,7 +22,7 @@ pub struct HeadsImages { pub heads: Vec>, } -#[derive(Clone, Copy, Debug, PartialEq, Reflect)] +#[derive(Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)] pub struct HeadState { pub head: usize, pub health: u32, @@ -51,7 +52,7 @@ impl HeadState { } } -#[derive(Component, Default, Reflect, Debug)] +#[derive(Component, Default, Reflect, Debug, Serialize, Deserialize, PartialEq)] #[reflect(Component)] pub struct ActiveHeads { heads: [Option; 5], diff --git a/crates/shared/src/player.rs b/crates/shared/src/player.rs index 322b936..d263b5b 100644 --- a/crates/shared/src/player.rs +++ b/crates/shared/src/player.rs @@ -3,7 +3,7 @@ use crate::{ camera::{CameraArmRotation, CameraTarget}, cash::{Cash, CashCollectEvent}, character::AnimatedCharacter, - control::controller_common::CharacterControllerBundle, + control::controller_common::PlayerCharacterController, global_observer, head::ActiveHead, head_drop::HeadDrops, @@ -12,9 +12,9 @@ use crate::{ hitpoints::{Hitpoints, Kill}, loading_assets::AudioAssets, npc::SpawnCharacter, - physics_layers::GameLayer, sounds::PlaySound, tb_entities::SpawnPoint, + utils::commands::EntityCommandExt, }; use avian3d::prelude::*; use bevy::{ @@ -22,17 +22,18 @@ use bevy::{ prelude::*, window::{CursorGrabMode, PrimaryWindow}, }; +use lightyear::prelude::{NetworkTarget, PredictionTarget, Replicate}; +use serde::{Deserialize, Serialize}; -#[derive(Component, Default)] +#[derive(Component, Default, Serialize, Deserialize, PartialEq)] pub struct Player; -#[derive(Component, Default)] +#[derive(Component, Default, Serialize, Deserialize, PartialEq)] #[require(Transform, Visibility)] pub struct PlayerBodyMesh; pub fn plugin(app: &mut App) { app.add_systems(Startup, (toggle_cursor_system, cursor_recenter)); - app.add_systems(OnEnter(GameState::Playing), spawn); app.add_systems( Update, ( @@ -46,10 +47,10 @@ pub fn plugin(app: &mut App) { global_observer!(app, on_update_head_mesh); } -fn spawn( +pub fn spawn( mut commands: Commands, - asset_server: Res, query: Query<&Transform, With>, + asset_server: Res, heads_db: Res, ) { let Some(spawn) = query.iter().next() else { @@ -58,35 +59,11 @@ fn spawn( let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.)); - let collider = Collider::capsule(0.9, 1.2); - commands - .spawn(( - Name::from("player"), - Player, - ActiveHead(0), - ActiveHeads::new([ - Some(HeadState::new(0, heads_db.as_ref())), - Some(HeadState::new(3, heads_db.as_ref())), - Some(HeadState::new(6, heads_db.as_ref())), - Some(HeadState::new(10, heads_db.as_ref())), - Some(HeadState::new(9, heads_db.as_ref())), - ]), - Hitpoints::new(100), - CameraTarget, - transform, - Visibility::default(), - CollisionLayers::new( - LayerMask(GameLayer::Player.to_bits()), - LayerMask::ALL & !GameLayer::CollectiblePhysics.to_bits(), - ), - CharacterControllerBundle::new(collider, heads_db.head_stats(0).controls), - children![( - Name::new("player-rig"), - PlayerBodyMesh, - CameraArmRotation, - children![AnimatedCharacter::new(0)] - )], + .spawn(player_bundle(transform, &heads_db)) + .insert_server(( + Replicate::to_clients(NetworkTarget::All), + PredictionTarget::to_clients(NetworkTarget::All), )) .observe(on_kill); @@ -98,6 +75,32 @@ fn spawn( commands.trigger(SpawnCharacter(transform.translation)); } +fn player_bundle(transform: Transform, heads_db: &Res) -> impl Bundle { + ( + Name::from("player"), + Player, + ActiveHead(0), + ActiveHeads::new([ + Some(HeadState::new(0, heads_db.as_ref())), + Some(HeadState::new(3, heads_db.as_ref())), + Some(HeadState::new(6, heads_db.as_ref())), + Some(HeadState::new(10, heads_db.as_ref())), + Some(HeadState::new(9, heads_db.as_ref())), + ]), + Hitpoints::new(100), + CameraTarget, + transform, + Visibility::default(), + PlayerCharacterController, + children![( + Name::new("player-rig"), + PlayerBodyMesh, + CameraArmRotation, + children![AnimatedCharacter::new(0)] + )], + ) +} + fn on_kill( trigger: Trigger, mut commands: Commands, diff --git a/crates/shared/src/protocol.rs b/crates/shared/src/protocol.rs index b1ff2b4..21e91a7 100644 --- a/crates/shared/src/protocol.rs +++ b/crates/shared/src/protocol.rs @@ -1,20 +1,72 @@ use crate::{ - abilities::BuildExplosionSprite, global_observer, loading_assets::GameAssets, + abilities::BuildExplosionSprite, + camera::{CameraArmRotation, CameraTarget}, + character::AnimatedCharacter, + control::{ + controller_common::{MovementSpeedFactor, PlayerCharacterController}, + controls::ControllerSettings, + }, + global_observer, + head::ActiveHead, + heads::ActiveHeads, + loading_assets::GameAssets, + player::{Player, PlayerBodyMesh}, utils::triggers::TriggerAppExt, }; +use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity}; use bevy::prelude::*; -use lightyear::prelude::{ActionsChannel, AppComponentExt}; +use happy_feet::{ + grounding::GroundingState, + prelude::{ + Character, CharacterDrag, CharacterGravity, CharacterMovement, GroundFriction, Grounding, + GroundingConfig, KinematicVelocity, MoveInput, SteppingConfig, + }, +}; +use lightyear::prelude::{ + ActionsChannel, AppComponentExt, PredictionMode, PredictionRegistrationExt, +}; use serde::{Deserialize, Serialize}; pub fn plugin(app: &mut App) { - app.register_component::(); + app.register_component::() + .add_prediction(PredictionMode::Full) + .add_should_rollback(transform_should_rollback); app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); + app.register_component::(); app.replicate_trigger::(); global_observer!(app, spawn_gltf_scene_roots); } +fn transform_should_rollback(this: &Transform, that: &Transform) -> bool { + this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.) +} + #[derive(Component, Reflect, Serialize, Deserialize, PartialEq)] #[reflect(Component)] pub enum GltfSceneRoot { diff --git a/crates/shared/src/utils/mod.rs b/crates/shared/src/utils/mod.rs index f98fa8f..82b2a32 100644 --- a/crates/shared/src/utils/mod.rs +++ b/crates/shared/src/utils/mod.rs @@ -3,6 +3,7 @@ pub mod billboards; pub mod commands; pub mod explosions; pub mod observers; +pub mod run_conditions; pub mod sprite_3d_animation; pub mod squish_animation; pub mod trail; diff --git a/crates/shared/src/utils/run_conditions.rs b/crates/shared/src/utils/run_conditions.rs new file mode 100644 index 0000000..27b32bd --- /dev/null +++ b/crates/shared/src/utils/run_conditions.rs @@ -0,0 +1,5 @@ +use bevy::ecs::{resource::Resource, system::Res}; + +pub fn resource_absent(res: Option>) -> bool { + res.is_none() +} diff --git a/justfile b/justfile index babcd61..3a32171 100644 --- a/justfile +++ b/justfile @@ -5,8 +5,8 @@ tb_setup_mac: ln -s $(pwd)/trenchbroom/hedz/hedz.fgd "$HOME/Library/Application Support/TrenchBroom/games/hedz/hedz.fgd" | true ln -s $(pwd)/trenchbroom/hedz/GameConfig.cfg "$HOME/Library/Application Support/TrenchBroom/games/hedz/GameConfig.cfg" | true -run: - RUST_BACKTRACE=1 cargo r --bin hedz_reloaded +run *args: + RUST_BACKTRACE=1 cargo r --bin hedz_reloaded -- {{args}} server: RUST_BACKTRACE=1 cargo r --bin server