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

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