Synchronize Trenchbroom map entities (#65)
This commit is contained in:
@@ -79,14 +79,13 @@ fn update_player_aim(
|
||||
spatial_query: SpatialQuery,
|
||||
) {
|
||||
for (player, state, mut aim_target, children) in player_aim.iter_mut() {
|
||||
assert_eq!(
|
||||
children.len(),
|
||||
1,
|
||||
"expected player to have one direct child"
|
||||
);
|
||||
let player_rot_child = children
|
||||
.iter()
|
||||
.find(|&child| player_rot.contains(child))
|
||||
.expect("expected child with PlayerBodyMesh");
|
||||
|
||||
let (player_pos, player_forward) = player_rot
|
||||
.get(*children.first().unwrap())
|
||||
.get(player_rot_child)
|
||||
.map(|(t, global)| (global.translation(), t.forward()))
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ pub struct DebugVisuals {
|
||||
pub cam_follow: bool,
|
||||
}
|
||||
|
||||
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(States, Default, Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
pub enum GameState {
|
||||
#[default]
|
||||
AssetLoading,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#[cfg(feature = "server")]
|
||||
use crate::protocol::TbMapEntityId;
|
||||
use crate::{GameState, physics_layers::GameLayer};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use bevy_trenchbroom::physics::SceneCollidersReady;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{DisableReplicateHierarchy, NetworkTarget, Replicate};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::MapLoading), setup_scene);
|
||||
@@ -12,14 +16,33 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
.spawn((
|
||||
CollisionLayers::new(LayerMask(GameLayer::Level.to_bits()), LayerMask::ALL),
|
||||
SceneRoot(asset_server.load("maps/map1.map#Scene")),
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
#[cfg(feature = "server")]
|
||||
DisableReplicateHierarchy,
|
||||
))
|
||||
.observe(
|
||||
|_t: Trigger<SceneCollidersReady>,
|
||||
|t: Trigger<SceneCollidersReady>,
|
||||
children: Query<&Children>,
|
||||
#[cfg(feature = "server")] map_entities: Query<&TbMapEntityId>,
|
||||
mut commands: Commands,
|
||||
#[cfg(any(feature = "client", feature = "server"))] mut next_game_state: ResMut<
|
||||
NextState<GameState>,
|
||||
>| {
|
||||
info!("map loaded");
|
||||
|
||||
for child in children.get(t.target()).unwrap() {
|
||||
commands.entity(*child).remove::<ChildOf>();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
if map_entities.contains(*child) {
|
||||
commands.entity(*child).insert((
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
DisableReplicateHierarchy,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(cfg!(feature = "client") ^ cfg!(feature = "server"));
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
|
||||
@@ -2,12 +2,15 @@ use crate::{
|
||||
GameState,
|
||||
tb_entities::{Platform, PlatformTarget},
|
||||
};
|
||||
use bevy::{math::ops::sin, prelude::*};
|
||||
#[cfg(feature = "server")]
|
||||
use bevy::math::ops::sin;
|
||||
use bevy::prelude::*;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
struct ActivePlatform {
|
||||
pub struct ActivePlatform {
|
||||
pub start: Vec3,
|
||||
pub target: Vec3,
|
||||
}
|
||||
@@ -15,6 +18,7 @@ struct ActivePlatform {
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<ActivePlatform>();
|
||||
app.add_systems(OnEnter(GameState::Playing), init);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
move_active.run_if(in_state(GameState::Playing)),
|
||||
@@ -47,6 +51,7 @@ fn init(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn move_active(time: Res<Time>, mut platforms: Query<(&mut Transform, &mut ActivePlatform)>) {
|
||||
for (mut transform, active) in platforms.iter_mut() {
|
||||
let t = (sin(time.elapsed_secs() * 0.4) + 1.) / 2.;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
abilities::BuildExplosionSprite,
|
||||
animation::AnimationFlags,
|
||||
backpack::Backpack,
|
||||
@@ -15,11 +16,15 @@ use crate::{
|
||||
heads::ActiveHeads,
|
||||
hitpoints::Hitpoints,
|
||||
loading_assets::{GameAssets, HeadDropAssets},
|
||||
platforms::ActivePlatform,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
utils::triggers::TriggerAppExt,
|
||||
};
|
||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
||||
use bevy::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{component::HookContext, world::DeferredWorld},
|
||||
prelude::*,
|
||||
};
|
||||
use happy_feet::{
|
||||
grounding::GroundingState,
|
||||
prelude::{
|
||||
@@ -28,19 +33,31 @@ use happy_feet::{
|
||||
},
|
||||
};
|
||||
use lightyear::prelude::{
|
||||
ActionsChannel, AppComponentExt, PredictionMode, PredictionRegistrationExt,
|
||||
input::native::InputPlugin,
|
||||
ActionsChannel, AppComponentExt, AppMessageExt, NetworkDirection, PredictionMode,
|
||||
PredictionRegistrationExt, input::native::InputPlugin,
|
||||
};
|
||||
use lightyear_serde::{
|
||||
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(InputPlugin::<ControlState>::default());
|
||||
|
||||
app.register_type::<TbMapEntityId>();
|
||||
app.register_type::<TbMapIdCounter>();
|
||||
app.register_type::<TbMapEntityMapping>();
|
||||
|
||||
app.init_resource::<TbMapIdCounter>();
|
||||
app.init_resource::<TbMapEntityMapping>();
|
||||
|
||||
app.add_message::<DespawnTbMapEntity>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
|
||||
app.register_component::<ActiveHead>();
|
||||
app.register_component::<ActiveHeads>();
|
||||
app.register_component::<ActivePlatform>();
|
||||
app.register_component::<AngularVelocity>();
|
||||
app.register_component::<AnimatedCharacter>();
|
||||
app.register_component::<AnimationFlags>();
|
||||
@@ -69,6 +86,7 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_component::<PlayerBodyMesh>();
|
||||
app.register_component::<PlayerCharacterController>();
|
||||
app.register_component::<SteppingConfig>();
|
||||
app.register_component::<TbMapEntityId>();
|
||||
app.register_component::<Transform>()
|
||||
.add_prediction(PredictionMode::Full)
|
||||
.add_should_rollback(transform_should_rollback);
|
||||
@@ -89,9 +107,66 @@ pub fn plugin(app: &mut App) {
|
||||
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
|
||||
app.replicate_trigger::<StartCutscene, ActionsChannel>();
|
||||
|
||||
app.add_systems(
|
||||
OnEnter(GameState::MapLoading),
|
||||
|mut counter: ResMut<TbMapIdCounter>| counter.reset(),
|
||||
);
|
||||
|
||||
global_observer!(app, spawn_gltf_scene_roots);
|
||||
}
|
||||
|
||||
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
|
||||
/// has already despawned it but the client has just loaded the map and connected
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct DespawnTbMapEntity(pub u64);
|
||||
|
||||
/// A unique ID assigned to every entity spawned by the trenchbroom map loader. This allows synchronizing
|
||||
/// them across the network even when they are spawned initially by both sides.
|
||||
#[derive(Clone, Copy, Component, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
#[component(on_insert = TbMapEntityId::insert_id, on_remove = TbMapEntityId::remove_id)]
|
||||
pub struct TbMapEntityId {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl TbMapEntityId {
|
||||
fn insert_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
world
|
||||
.resource_mut::<TbMapEntityMapping>()
|
||||
.insert(id, ctx.entity);
|
||||
}
|
||||
|
||||
fn remove_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
world.resource_mut::<TbMapEntityMapping>().remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// A global allocator for `TBMapEntityId` values. Should be reset when a map begins loading.
|
||||
#[derive(Resource, Reflect, Default)]
|
||||
#[reflect(Resource)]
|
||||
pub struct TbMapIdCounter(u64);
|
||||
|
||||
impl TbMapIdCounter {
|
||||
pub fn reset(&mut self) {
|
||||
self.0 = 0;
|
||||
}
|
||||
|
||||
pub fn alloc(&mut self) -> TbMapEntityId {
|
||||
let id = self.0;
|
||||
self.0 += 1;
|
||||
TbMapEntityId { id }
|
||||
}
|
||||
}
|
||||
|
||||
/// A mapping from TbMapEntityId to clientside map entity. When the serverside is spawned and the client's
|
||||
/// components migrated to it, or the clientside is despawned because the serverside is already despawned,
|
||||
/// the Id entry is removed from this mapping.
|
||||
#[derive(Resource, Reflect, Default, Deref, DerefMut)]
|
||||
#[reflect(Resource)]
|
||||
pub struct TbMapEntityMapping(pub HashMap<u64, Entity>);
|
||||
|
||||
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
|
||||
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer};
|
||||
use crate::{
|
||||
cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer, protocol::TbMapIdCounter,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{component::HookContext, world::DeferredWorld},
|
||||
@@ -7,6 +9,7 @@ use bevy::{
|
||||
};
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use happy_feet::prelude::PhysicsMover;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
@@ -72,7 +75,7 @@ pub struct PlatformTarget {
|
||||
pub targetname: String,
|
||||
}
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[derive(SolidClass, Component, Reflect, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform, Target)]
|
||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||
@@ -202,4 +205,15 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_type::<EnemySpawn>();
|
||||
app.register_type::<CashSpawn>();
|
||||
app.register_type::<SecretHead>();
|
||||
|
||||
app.add_observer(tb_component_setup::<CashSpawn>);
|
||||
app.add_observer(tb_component_setup::<Platform>);
|
||||
app.add_observer(tb_component_setup::<PlatformTarget>);
|
||||
app.add_observer(tb_component_setup::<Movable>);
|
||||
}
|
||||
|
||||
fn tb_component_setup<C: Component>(trigger: Trigger<OnAdd, C>, world: &mut World) {
|
||||
let id = world.resource_mut::<TbMapIdCounter>().alloc();
|
||||
|
||||
world.entity_mut(trigger.target()).insert_if_new(id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user