Controller Replication (#57)

This commit is contained in:
PROMETHIA-27
2025-07-20 09:43:10 -04:00
committed by GitHub
parent 4c23288511
commit fdee4cf37c
15 changed files with 198 additions and 57 deletions

View File

@@ -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::<u16>().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<Client>>,
commands: Commands,
asset_server: Res<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) {
if disconnected.state == ClientState::Disconnected {
shared::player::spawn(commands, query, asset_server, heads_db)
}
}

View File

@@ -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<OnAdd, Connected>,
mut commands: Commands,
id: Query<&PeerAddr>,
asset_server: Res<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> 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(())
}

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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::<ActiveHead>()
.expect("player must be spawned with an `ActiveHead`")
.0;
let controls = world.resource::<HeadsDatabase>().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(),
),
}
}
}

View File

@@ -14,6 +14,7 @@ use bevy::{
},
prelude::*,
};
use serde::{Deserialize, Serialize};
pub fn plugin(app: &mut App) {
app.init_resource::<Controls>();
@@ -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,

View File

@@ -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);

View File

@@ -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<Handle<Image>>,
}
#[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<HeadState>; 5],

View File

@@ -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<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>,
asset_server: Res<AssetServer>,
heads_db: Res<HeadsDatabase>,
) {
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<HeadsDatabase>) -> 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<Kill>,
mut commands: Commands,

View File

@@ -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::<Transform>();
app.register_component::<Transform>()
.add_prediction(PredictionMode::Full)
.add_should_rollback(transform_should_rollback);
app.register_component::<GltfSceneRoot>();
app.register_component::<Player>();
app.register_component::<PlayerBodyMesh>();
app.register_component::<Name>();
app.register_component::<ActiveHead>();
app.register_component::<ActiveHeads>();
app.register_component::<CameraTarget>();
app.register_component::<CameraArmRotation>();
app.register_component::<CollisionLayers>();
app.register_component::<AnimatedCharacter>();
app.register_component::<PlayerCharacterController>();
app.register_component::<LinearVelocity>();
app.register_component::<AngularVelocity>();
app.register_component::<KinematicVelocity>();
app.register_component::<Character>();
app.register_component::<MoveInput>();
app.register_component::<MovementSpeedFactor>();
app.register_component::<CharacterMovement>();
app.register_component::<SteppingConfig>();
app.register_component::<GroundingConfig>();
app.register_component::<Grounding>();
app.register_component::<GroundingState>();
app.register_component::<CharacterGravity>();
app.register_component::<GroundFriction>();
app.register_component::<CharacterDrag>();
app.register_component::<ControllerSettings>();
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
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 {

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
use bevy::ecs::{resource::Resource, system::Res};
pub fn resource_absent<R: Resource>(res: Option<Res<R>>) -> bool {
res.is_none()
}