replication for client side projectiles (#87)

This commit is contained in:
extrawurst
2025-12-14 19:41:45 +01:00
committed by GitHub
parent e7ebff2029
commit d93b38d1c8
15 changed files with 1139 additions and 962 deletions

1748
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,8 +55,8 @@ bevy_asset_loader = "=0.24.0-rc.1"
bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" }
bevy_common_assets = { version = "0.14.0", features = ["ron"] }
bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" }
bevy_replicon = "0.36.1"
bevy_replicon_renet = "0.12.0"
bevy_replicon = "0.37.1"
bevy_replicon_renet = "0.13.0"
bevy_sprite3d = "7.0.0"
bevy_trenchbroom = { version = "0.10", default-features = false, features = [
"physics-integration",

View File

@@ -6,6 +6,7 @@ use bevy::{
},
prelude::*,
};
use bevy_replicon::client::ClientSystems;
use shared::{
control::{
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
@@ -28,12 +29,21 @@ pub fn plugin(app: &mut App) {
)
.chain()
.in_set(ControllerSet::CollectInputs)
.before(ClientSystems::Receive)
.run_if(
in_state(GameState::Playing)
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
)
.add_systems(PreUpdate, overwrite_local_inputs);
);
// run this deliberately after local input processing ended
// TODO: can and should be ordered using a set to guarantee it gets send out ASAP but after local input processing
app.add_systems(
PreUpdate,
overwrite_local_inputs.after(ClientSystems::Receive).run_if(
in_state(GameState::Playing).and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
);
app.add_systems(
Update,

View File

@@ -1,18 +1,16 @@
use crate::{
GameState,
abilities::{BuildExplosionSprite, TriggerCurver},
abilities::{BuildExplosionSprite, ProjectileId, TriggerCurver},
heads_database::HeadsDatabase,
hitpoints::Hit,
physics_layers::GameLayer,
protocol::GltfSceneRoot,
tb_entities::EnemySpawn,
utils::{auto_rotate::AutoRotation, global_observer},
utils::global_observer,
};
use avian3d::prelude::*;
use bevy::prelude::*;
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
const MAX_SHOT_AGES: f32 = 15.;
@@ -21,6 +19,7 @@ const MAX_SHOT_AGES: f32 = 15.;
pub struct CurverProjectile {
time: f32,
damage: u32,
projectile: String,
}
pub fn plugin(app: &mut App) {
@@ -30,6 +29,8 @@ pub fn plugin(app: &mut App) {
Update,
(shot_collision, enemy_hit).run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "client")]
app.add_systems(Update, shot_visuals.run_if(in_state(GameState::Playing)));
app.add_systems(
FixedUpdate,
(update, timeout).run_if(in_state(GameState::Playing)),
@@ -38,6 +39,27 @@ pub fn plugin(app: &mut App) {
global_observer!(app, on_trigger_curver);
}
#[cfg(feature = "client")]
fn shot_visuals(
mut commands: Commands,
query: Query<(Entity, &CurverProjectile), Added<CurverProjectile>>,
) {
for (entity, projectile) in query.iter() {
if commands.get_entity(entity).is_ok() {
let child = commands
.spawn((
crate::utils::auto_rotate::AutoRotation(
Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3),
),
crate::protocol::GltfSceneRoot::Projectile(projectile.projectile.clone()),
))
.id();
commands.entity(entity).add_child(child);
}
}
}
fn on_trigger_curver(
trigger: On<TriggerCurver>,
mut commands: Commands,
@@ -63,30 +85,30 @@ fn on_trigger_curver(
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
transform.translation += transform.forward().as_vec3() * 2.0;
commands.spawn((
Name::new("projectile-missile"),
CurverProjectile {
time: time.elapsed_secs(),
damage: head.damage,
},
Collider::capsule_endpoints(0.4, Vec3::new(0., 0., 2.), Vec3::new(0., 0., -2.)),
CollisionLayers::new(
LayerMask(GameLayer::Projectile.to_bits()),
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
),
Sensor,
CollisionEventsEnabled,
Visibility::default(),
transform,
Replicated,
//TODO: put in client only system
children![(
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
GltfSceneRoot::Projectile(head.projectile.clone()),
let id = commands
.spawn((
Name::new("projectile-curver"),
CurverProjectile {
time: time.elapsed_secs(),
damage: head.damage,
projectile: head.projectile.clone(),
},
Collider::capsule_endpoints(0.5, Vec3::new(0., 0., 1.), Vec3::new(0., 0., -1.)),
CollisionLayers::new(
LayerMask(GameLayer::Projectile.to_bits()),
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
),
Sensor,
RigidBody::Kinematic,
CollisionEventsEnabled,
Visibility::default(),
transform,
Replicated,
),],
));
ProjectileId(state.trigger_id),
))
.id();
debug!(id=?id, trigger_id = state.trigger_id, "Curver");
}
fn enemy_hit(

View File

@@ -1,11 +1,12 @@
use super::TriggerGun;
use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, tb_entities::EnemySpawn,
utils::sprite_3d_animation::AnimationTimer,
GameState, abilities::ProjectileId, billboards::Billboard, global_observer,
heads_database::HeadsDatabase, hitpoints::Hit, loading_assets::GameAssets,
physics_layers::GameLayer, tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
};
use avian3d::prelude::*;
use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::Replicated;
use bevy_sprite3d::Sprite3d;
#[derive(Component)]
@@ -123,6 +124,8 @@ fn on_trigger_gun(
CollisionEventsEnabled,
Visibility::default(),
transform,
Replicated,
ProjectileId(state.trigger_id),
Children::spawn(Spawn(Gizmo {
handle: gizmo_assets.add({
let mut g = GizmoAsset::default();

View File

@@ -1,7 +1,7 @@
use super::TriggerMissile;
use crate::{
GameState,
abilities::{ExplodingProjectile, ExplodingProjectileSet},
abilities::{ExplodingProjectile, ExplodingProjectileSet, ProjectileId},
heads_database::HeadsDatabase,
physics_layers::GameLayer,
protocol::{GltfSceneRoot, PlaySound},
@@ -60,36 +60,41 @@ fn on_trigger_missile(
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
transform.translation += transform.forward().as_vec3() * 2.0;
commands.spawn((
Name::new("projectile-missile"),
MissileProjectile {
time: time.elapsed_secs(),
damage: head.damage,
},
SpawnTrail::new(
12,
LinearRgba::rgb(1., 0.0, 0.),
LinearRgba::rgb(0.9, 0.9, 0.),
10.,
)
.init_with_pos(),
Collider::capsule_endpoints(0.4, Vec3::new(0., 0., 2.), Vec3::new(0., 0., -2.)),
CollisionLayers::new(
LayerMask(GameLayer::Projectile.to_bits()),
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
),
Sensor,
RigidBody::Kinematic,
CollisionEventsEnabled,
Visibility::default(),
transform,
Replicated,
children![(
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
GltfSceneRoot::Projectile("missile".to_string()),
Replicated
)],
));
let id = commands
.spawn((
Name::new("projectile-missile"),
MissileProjectile {
time: time.elapsed_secs(),
damage: head.damage,
},
SpawnTrail::new(
12,
LinearRgba::rgb(1., 0.0, 0.),
LinearRgba::rgb(0.9, 0.9, 0.),
10.,
)
.init_with_pos(),
Collider::capsule_endpoints(0.4, Vec3::new(0., 0., 2.), Vec3::new(0., 0., -2.)),
CollisionLayers::new(
LayerMask(GameLayer::Projectile.to_bits()),
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
),
Sensor,
RigidBody::Kinematic,
CollisionEventsEnabled,
Visibility::default(),
transform,
Replicated,
ProjectileId(state.trigger_id),
children![(
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
GltfSceneRoot::Projectile("missile".to_string()),
Replicated
)],
))
.id();
debug!(id=?id, trigger_id = state.trigger_id, "Missile");
}
fn update(mut query: Query<&mut Transform, With<MissileProjectile>>) {

View File

@@ -6,18 +6,21 @@ pub mod missile;
pub mod thrown;
use crate::{
GameState, global_observer,
GameState,
control::ControllerSet,
global_observer,
loading_assets::GameAssets,
physics_layers::GameLayer,
player::Player,
protocol::PlaySound,
utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer},
};
use crate::{
aim::AimTarget, character::CharacterHierarchy, control::Inputs, heads::ActiveHeads,
heads_database::HeadsDatabase, player::Player,
heads_database::HeadsDatabase,
};
use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, Signature, ToClients};
use bevy_sprite3d::Sprite3d;
pub use healing::Healing;
use serde::{Deserialize, Serialize};
@@ -44,6 +47,7 @@ pub struct TriggerData {
pos: Vec3,
target_layer: GameLayer,
head: usize,
trigger_id: usize,
}
impl TriggerData {
@@ -53,6 +57,7 @@ impl TriggerData {
pos: Vec3,
target_layer: GameLayer,
head: usize,
trigger_id: usize,
) -> Self {
Self {
target,
@@ -60,6 +65,7 @@ impl TriggerData {
pos,
target_layer,
head,
trigger_id,
}
}
@@ -85,14 +91,13 @@ pub struct TriggerCurver(pub TriggerData);
#[reflect(Component)]
pub struct PlayerTriggerState {
next_trigger_timestamp: f32,
active: bool,
projectile_count: usize,
}
impl PlayerTriggerState {
pub fn is_active(&self) -> bool {
self.active
}
}
#[derive(Component, Reflect, Deserialize, Serialize, Hash)]
#[reflect(Component)]
#[require(Signature::of::<ProjectileId>())]
pub struct ProjectileId(pub usize);
#[derive(Component)]
#[component(storage = "SparseSet")]
@@ -107,6 +112,7 @@ pub struct ExplodingProjectile {
anim_time: f32,
}
// TODO: move explosions into separate modul
#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExplodingProjectileSet {
Mark,
@@ -128,12 +134,14 @@ pub fn plugin(app: &mut App) {
.run_if(in_state(GameState::Playing)),
);
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
FixedUpdate,
(on_trigger_state, update, update_heal_ability)
(update, update_heal_ability)
.chain()
.run_if(in_state(GameState::Playing)),
);
app.add_systems(
FixedUpdate,
explode_projectiles.in_set(ExplodingProjectileSet::Explode),
@@ -144,6 +152,8 @@ pub fn plugin(app: &mut App) {
fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingProjectile)>) {
for (shot_entity, projectile) in query.iter() {
debug!(id=?shot_entity, "Projectile explosion");
if let Ok(mut entity) = commands.get_entity(shot_entity) {
entity.try_despawn();
} else {
@@ -176,12 +186,6 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
}
}
fn on_trigger_state(mut players: Query<(&mut PlayerTriggerState, &Inputs), With<Player>>) {
for (mut trigger_state, inputs) in players.iter_mut() {
trigger_state.active = inputs.trigger;
}
}
fn update(
mut commands: Commands,
mut query: Query<
@@ -200,11 +204,15 @@ fn update(
character: CharacterHierarchy,
) {
for (player, mut active_heads, mut trigger_state, target, inputs) in query.iter_mut() {
if trigger_state.active && trigger_state.next_trigger_timestamp < time.elapsed_secs() {
if inputs.trigger && trigger_state.next_trigger_timestamp < time.elapsed_secs() {
let Some(state) = active_heads.current() else {
return;
};
if !state.has_ammo() {
return;
}
let target = if let Some(target) = target.0
&& query_transform.get(target).is_ok()
{
@@ -229,6 +237,7 @@ fn update(
active_heads.use_ammo(time.elapsed_secs());
trigger_state.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);
trigger_state.projectile_count += 1;
let trigger_state = TriggerData {
dir: Dir3::try_from(inputs.look_dir).unwrap_or(Dir3::NEG_Z),
@@ -236,6 +245,7 @@ fn update(
target,
target_layer: GameLayer::Npc,
head: state.head,
trigger_id: trigger_state.projectile_count,
};
match head.ability {
@@ -252,11 +262,11 @@ fn update(
fn update_heal_ability(
mut commands: Commands,
players: Query<(Entity, &ActiveHeads, Ref<PlayerTriggerState>), With<Player>>,
players: Query<(Entity, &ActiveHeads, Ref<Inputs>), With<Player>>,
heads_db: Res<HeadsDatabase>,
) {
for (player, active_heads, trigger_state) in players.iter() {
if trigger_state.is_changed() {
for (player, active_heads, inputs) in players.iter() {
if inputs.is_changed() {
let Some(state) = active_heads.current() else {
return;
};
@@ -268,7 +278,7 @@ fn update_heal_ability(
}
use crate::abilities::healing::HealingState;
if trigger_state.active {
if inputs.trigger {
use crate::abilities::healing::HealingStateChanged;
commands.trigger(HealingStateChanged {

View File

@@ -1,10 +1,11 @@
use super::TriggerThrow;
use crate::{
abilities::{ExplodingProjectile, ExplodingProjectileSet},
GameState,
abilities::{ExplodingProjectile, ExplodingProjectileSet, ProjectileId},
heads_database::HeadsDatabase,
physics_layers::GameLayer,
protocol::{GltfSceneRoot, PlaySound},
utils::{auto_rotate::AutoRotation, global_observer},
protocol::PlaySound,
utils::global_observer,
};
use avian3d::prelude::*;
use bevy::prelude::*;
@@ -16,17 +17,38 @@ use serde::{Deserialize, Serialize};
pub struct ThrownProjectile {
impact_animation: bool,
damage: u32,
projectile: String,
}
pub fn plugin(app: &mut App) {
app.add_systems(
FixedUpdate,
shot_collision.in_set(ExplodingProjectileSet::Mark),
shot_collision
.in_set(ExplodingProjectileSet::Mark)
.run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "client")]
app.add_systems(Update, shot_visuals.run_if(in_state(GameState::Playing)));
global_observer!(app, on_trigger_thrown);
}
#[cfg(feature = "client")]
fn shot_visuals(
mut commands: Commands,
query: Query<(Entity, &ThrownProjectile), Added<ThrownProjectile>>,
) {
for (entity, thrown) in query.iter() {
commands.entity(entity).try_insert((
crate::utils::auto_rotate::AutoRotation(
Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3),
),
crate::protocol::GltfSceneRoot::Projectile(thrown.projectile.clone()),
));
}
}
fn on_trigger_thrown(
trigger: On<TriggerThrow>,
mut commands: Commands,
@@ -57,13 +79,14 @@ fn on_trigger_thrown(
//TODO: projectile db?
let explosion_animation = !matches!(state.head, 8 | 16);
commands
let id = commands
.spawn((
Transform::from_translation(pos),
Name::new("projectile-thrown"),
ThrownProjectile {
impact_animation: explosion_animation,
damage: head.damage,
projectile: head.projectile.clone(),
},
Collider::sphere(0.4),
CollisionLayers::new(
@@ -77,12 +100,11 @@ fn on_trigger_thrown(
Visibility::default(),
Sensor,
Replicated,
ProjectileId(state.trigger_id),
))
.with_child((
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
GltfSceneRoot::Projectile(head.projectile.clone()),
Replicated,
));
.id();
debug!(id=?id, trigger_id = state.trigger_id, "Thrown");
}
fn shot_collision(

View File

@@ -169,6 +169,8 @@ fn engage_and_throw(
t.translation,
crate::physics_layers::GameLayer::Player,
npc_head.head,
// TODO: we probably need to make sure the ai's projectile does not get deduped, zero should not be used by anyone though
0,
)));
}
}

View File

@@ -1,8 +1,3 @@
#[cfg(feature = "client")]
use crate::{
backpack::backpack_ui::BackpackUiState, control::BackpackButtonPress, player::LocalPlayer,
protocol::PlaySound,
};
use crate::{
cash::CashCollectEvent,
global_observer,
@@ -62,11 +57,16 @@ pub fn plugin(app: &mut App) {
#[cfg(feature = "client")]
#[allow(clippy::too_many_arguments)]
fn backpack_inputs(
backpacks: Single<(&Backpack, &mut BackpackUiState), With<LocalPlayer>>,
mut backpack_inputs: MessageReader<BackpackButtonPress>,
backpacks: Single<
(&Backpack, &mut backpack_ui::BackpackUiState),
With<crate::player::LocalPlayer>,
>,
mut backpack_inputs: MessageReader<crate::control::BackpackButtonPress>,
mut commands: Commands,
time: Res<Time>,
) {
use crate::{control::BackpackButtonPress, protocol::PlaySound};
let (backpack, mut state) = backpacks.into_inner();
for input in backpack_inputs.read() {
@@ -117,7 +117,7 @@ fn backpack_inputs(
#[cfg(feature = "client")]
fn sync_on_change(
backpack: Query<Ref<Backpack>>,
mut state: Single<&mut BackpackUiState>,
mut state: Single<&mut backpack_ui::BackpackUiState>,
time: Res<Time>,
) {
for backpack in backpack.iter() {
@@ -128,7 +128,7 @@ fn sync_on_change(
}
#[cfg(feature = "client")]
fn sync_backpack_ui(backpack: &Backpack, state: &mut BackpackUiState, time: f32) {
fn sync_backpack_ui(backpack: &Backpack, state: &mut backpack_ui::BackpackUiState, time: f32) {
use crate::backpack::backpack_ui::BACKPACK_HEAD_SLOTS;
state.count = backpack.heads.len();

View File

@@ -1,7 +1,6 @@
use super::{ControllerSet, ControllerSwitchEvent};
use crate::{
GameState,
abilities::PlayerTriggerState,
animation::AnimationFlags,
control::{ControllerSettings, Inputs, SelectedController},
physics_layers::GameLayer,
@@ -43,16 +42,11 @@ pub fn plugin(app: &mut App) {
fn set_animation_flags(
mut player: Query<
(
&Grounding,
&mut AnimationFlags,
&Inputs,
&PlayerTriggerState,
),
(&Grounding, &mut AnimationFlags, &Inputs),
(With<Player>, Without<ConfirmHistory>),
>,
) {
for (grounding, mut flags, inputs, trigger_state) in player.iter_mut() {
for (grounding, mut flags, inputs) in player.iter_mut() {
let direction = inputs.move_dir;
let deadzone = 0.2;
@@ -64,7 +58,7 @@ fn set_animation_flags(
flags.any_direction = true;
}
flags.shooting = trigger_state.is_active();
flags.shooting = inputs.trigger;
// `apply_controls` sets the jump flag when the player actually jumps.
// Unset the flag on hitting the ground
@@ -97,14 +91,13 @@ pub fn reset_upon_switch(
let flat_look_dir = inputs.look_dir.with_y(0.0).normalize();
rig_transform.look_to(flat_look_dir, Dir3::Y);
match selected_controller.0 {
ControllerSet::ApplyControlsFly => {
match *selected_controller {
SelectedController::Flying => {
c.entity(controller).insert(FLYING_MOVEMENT_CONFIG);
}
ControllerSet::ApplyControlsRun => {
SelectedController::Running => {
c.entity(controller).insert(RUNNING_MOVEMENT_CONFIG);
}
_ => unreachable!(),
}
}
}

View File

@@ -16,16 +16,19 @@ pub mod controller_common;
pub mod controller_flying;
pub mod controller_running;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Default)]
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum ControllerSet {
CollectInputs,
ApplyControlsFly,
#[default]
ApplyControlsRun,
}
#[derive(Resource, Debug, Default, PartialEq)]
pub struct SelectedController(pub ControllerSet);
#[derive(Resource, Debug, Clone, Copy, PartialEq, Default)]
pub enum SelectedController {
Flying,
#[default]
Running,
}
pub fn plugin(app: &mut App) {
app.register_type::<ControllerSettings>()
@@ -48,12 +51,8 @@ pub fn plugin(app: &mut App) {
app.configure_sets(
FixedUpdate,
(
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsFly,
))),
ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsRun,
))),
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController::Flying)),
ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController::Running)),
)
.chain()
.run_if(in_state(GameState::Playing)),
@@ -104,7 +103,6 @@ impl MapEntities for Inputs {
pub struct ClientInputs(pub Inputs);
/// A cache to collect inputs into clientside, so that they don't get overwritten by replication from the server
#[cfg(feature = "client")]
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct LocalInputs(pub Inputs);
@@ -172,14 +170,14 @@ fn head_change(
for (entity, head) in query.iter() {
let stats = heads_db.head_stats(head.0);
let controller = match stats.controls {
HeadControls::Plane => ControllerSet::ApplyControlsFly,
HeadControls::Walk => ControllerSet::ApplyControlsRun,
HeadControls::Plane => SelectedController::Flying,
HeadControls::Walk => SelectedController::Running,
};
if selected_controller.0 != controller {
if *selected_controller != controller {
event_controller_switch.write(ControllerSwitchEvent { controller: entity });
selected_controller.0 = controller;
*selected_controller = controller;
}
}
}

View File

@@ -5,7 +5,6 @@ use crate::{
character::HedzCharacter,
protocol::PlayerId,
};
#[cfg(feature = "client")]
use crate::{backpack::backpack_ui::BackpackUiState, control::LocalInputs};
use avian3d::prelude::*;
use bevy::{
@@ -20,7 +19,6 @@ use serde::{Deserialize, Serialize};
#[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)]
pub struct Player;
#[cfg(feature = "client")]
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(LocalInputs, BackpackUiState)]

View File

@@ -26,8 +26,9 @@ pub enum NetworkedCollider {
half_extents: Vec3,
},
Capsule {
a: Vec3,
b: Vec3,
radius: f32,
length: f32,
},
/// If a collider value wasn't set up to be replicated, it is replicated as unknown
/// and a warning is logged, and unwraps to `sphere(0.1)` on the other side. Likely
@@ -47,8 +48,9 @@ impl From<Collider> for NetworkedCollider {
}
} else if let Some(value) = value.shape().as_capsule() {
NetworkedCollider::Capsule {
a: value.segment.a.into(),
b: value.segment.b.into(),
radius: value.radius,
length: value.height(),
}
} else {
warn!(
@@ -66,7 +68,9 @@ impl From<NetworkedCollider> for Collider {
NetworkedCollider::Cuboid { half_extents } => {
Collider::cuboid(half_extents.x, half_extents.y, half_extents.z)
}
NetworkedCollider::Capsule { radius, length } => Collider::capsule(radius, length),
NetworkedCollider::Capsule { a, b, radius } => {
Collider::capsule_endpoints(radius, a, b)
}
NetworkedCollider::Unknown => Collider::sphere(0.1),
}
}

View File

@@ -1,6 +1,8 @@
use crate::{
GameState,
abilities::{BuildExplosionSprite, curver::CurverProjectile, healing::Healing},
abilities::{
BuildExplosionSprite, curver::CurverProjectile, healing::Healing, thrown::ThrownProjectile,
},
animation::AnimationFlags,
backpack::{Backpack, BackpackSwapEvent},
camera::{CameraArmRotation, CameraTarget},
@@ -79,7 +81,7 @@ pub fn plugin(app: &mut App) {
app.replicate::<ChildOf>();
app.sync_related_entities::<ChildOf>();
app.replicate::<components::GltfSceneRoot>()
app.replicate_once::<components::GltfSceneRoot>()
.replicate_once::<components::PlayerId>()
.replicate::<components::TbMapEntityId>()
.replicate::<ActiveHead>()
@@ -88,7 +90,6 @@ pub fn plugin(app: &mut App) {
.replicate::<AnimatedCharacter>()
.replicate::<AnimationFlags>()
.replicate_once::<AutoRotation>()
.replicate_once::<CurverProjectile>()
.replicate::<Backpack>()
.replicate::<Billboard>()
.replicate_once::<CameraArmRotation>()
@@ -108,6 +109,9 @@ pub fn plugin(app: &mut App) {
.replicate::<UiActiveHeads>()
.replicate_as::<Visibility, SerVisibility>();
app.replicate_once::<ThrownProjectile>()
.replicate_once::<CurverProjectile>();
// Physics components
app.replicate::<AngularInertia>()
.replicate::<AngularVelocity>()