Synchronize Trenchbroom map entities (#65)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -7196,7 +7196,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"avian3d",
|
"avian3d",
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy-inspector-egui",
|
|
||||||
"bevy-steamworks",
|
"bevy-steamworks",
|
||||||
"bevy-ui-gradients",
|
"bevy-ui-gradients",
|
||||||
"bevy_common_assets",
|
"bevy_common_assets",
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
use bevy::{prelude::*, utils::synccell::SyncCell};
|
use avian3d::prelude::{
|
||||||
|
Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderMassProperties,
|
||||||
|
CollisionEventsEnabled, CollisionLayers, Sensor,
|
||||||
|
};
|
||||||
|
use bevy::{
|
||||||
|
ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance, utils::synccell::SyncCell,
|
||||||
|
};
|
||||||
|
use bevy_trenchbroom::geometry::Brushes;
|
||||||
use lightyear::{
|
use lightyear::{
|
||||||
netcode::Key,
|
netcode::Key,
|
||||||
prelude::{client::NetcodeConfig, input::native::InputMarker, *},
|
prelude::{client::NetcodeConfig, input::native::InputMarker, *},
|
||||||
};
|
};
|
||||||
use nil::prelude::Mutex;
|
use nil::prelude::Mutex;
|
||||||
use shared::{GameState, control::ControlState, global_observer, player::Player};
|
use shared::{
|
||||||
|
GameState,
|
||||||
|
control::ControlState,
|
||||||
|
global_observer,
|
||||||
|
player::Player,
|
||||||
|
protocol::{DespawnTbMapEntity, TbMapEntityId, TbMapEntityMapping},
|
||||||
|
tb_entities::{Platform, PlatformTarget},
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
env::current_exe,
|
env::current_exe,
|
||||||
io::{BufRead, BufReader},
|
io::{BufRead, BufReader},
|
||||||
@@ -23,12 +37,14 @@ pub fn plugin(app: &mut App) {
|
|||||||
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
||||||
);
|
);
|
||||||
app.add_systems(Last, close_server_processes);
|
app.add_systems(Last, close_server_processes);
|
||||||
|
app.add_systems(FixedUpdate, despawn_absent_map_entities);
|
||||||
|
|
||||||
global_observer!(app, on_connecting);
|
global_observer!(app, on_connecting);
|
||||||
global_observer!(app, on_connection_failed);
|
global_observer!(app, on_connection_failed);
|
||||||
global_observer!(app, on_connection_succeeded);
|
global_observer!(app, on_connection_succeeded);
|
||||||
global_observer!(app, temp_give_player_marker);
|
global_observer!(app, temp_give_player_marker);
|
||||||
global_observer!(app, connect_on_local_server_started);
|
global_observer!(app, connect_on_local_server_started);
|
||||||
|
global_observer!(app, received_remote_map_entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close_server_processes(mut app_exit: EventReader<AppExit>) {
|
fn close_server_processes(mut app_exit: EventReader<AppExit>) {
|
||||||
@@ -190,3 +206,74 @@ fn temp_give_player_marker(trigger: Trigger<OnAdd, Player>, mut commands: Comman
|
|||||||
.entity(trigger.target())
|
.entity(trigger.target())
|
||||||
.insert(InputMarker::<ControlState>::default());
|
.insert(InputMarker::<ControlState>::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn received_remote_map_entity(
|
||||||
|
trigger: Trigger<OnAdd, TbMapEntityId>,
|
||||||
|
world: &mut World,
|
||||||
|
mut child_buffer: Local<Vec<Entity>>,
|
||||||
|
) {
|
||||||
|
let serverside = trigger.target();
|
||||||
|
|
||||||
|
if world.get::<Replicated>(serverside).is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = *world.get::<TbMapEntityId>(serverside).unwrap();
|
||||||
|
|
||||||
|
let Some(clientside) = world.resource_mut::<TbMapEntityMapping>().0.remove(&id.id) else {
|
||||||
|
warn!("received unknown MapEntity ID `{id:?}`");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// cannot just use `take` directly with a bundle because then any missing component would cause
|
||||||
|
// the entire bundle to fail
|
||||||
|
move_component::<Brushes>(world, clientside, serverside);
|
||||||
|
move_component::<(
|
||||||
|
Collider,
|
||||||
|
ColliderAabb,
|
||||||
|
ColliderDensity,
|
||||||
|
ColliderMarker,
|
||||||
|
ColliderMassProperties,
|
||||||
|
CollisionLayers,
|
||||||
|
)>(world, clientside, serverside);
|
||||||
|
move_component::<CollisionEventsEnabled>(world, clientside, serverside);
|
||||||
|
move_component::<Platform>(world, clientside, serverside);
|
||||||
|
move_component::<PlatformTarget>(world, clientside, serverside);
|
||||||
|
move_component::<SceneInstance>(world, clientside, serverside);
|
||||||
|
move_component::<SceneRoot>(world, clientside, serverside);
|
||||||
|
move_component::<Sensor>(world, clientside, serverside);
|
||||||
|
|
||||||
|
if let Some(children) = world.get::<Children>(clientside) {
|
||||||
|
child_buffer.extend(children.iter());
|
||||||
|
for child in child_buffer.drain(..) {
|
||||||
|
world.entity_mut(child).insert(ChildOf(serverside));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
world.entity_mut(clientside).despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_component<B: Bundle + BundleFromComponents>(world: &mut World, from: Entity, to: Entity) {
|
||||||
|
let comp = world.entity_mut(from).take::<B>();
|
||||||
|
if let Some(comp) = comp {
|
||||||
|
world.entity_mut(to).insert(comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn despawn_absent_map_entities(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut messages: Query<&mut MessageReceiver<DespawnTbMapEntity>>,
|
||||||
|
mut map: ResMut<TbMapEntityMapping>,
|
||||||
|
) {
|
||||||
|
for mut recv in messages.iter_mut() {
|
||||||
|
for msg in recv.receive() {
|
||||||
|
// the server may double-send DespawnTbMapEntity for a given ID, so ignore it if the entity
|
||||||
|
// was already despawned.
|
||||||
|
let Some(entity) = map.0.remove(&msg.0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ edition = "2024"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["shared/server"]
|
default = ["shared/server"]
|
||||||
dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"]
|
dbg = ["avian3d/debug-plugin", "shared/dbg"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
avian3d = { workspace = true }
|
avian3d = { workspace = true }
|
||||||
bevy = { workspace = true, default-features = false }
|
bevy = { workspace = true, default-features = false }
|
||||||
bevy-inspector-egui = { workspace = true, optional = true }
|
|
||||||
bevy-steamworks = { workspace = true }
|
bevy-steamworks = { workspace = true }
|
||||||
bevy-ui-gradients = { workspace = true }
|
bevy-ui-gradients = { workspace = true }
|
||||||
bevy_common_assets = { workspace = true }
|
bevy_common_assets = { workspace = true }
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::utils::{auto_rotate, explosions};
|
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
app::plugin_group,
|
app::plugin_group,
|
||||||
@@ -11,14 +10,13 @@ use bevy_common_assets::ron::RonAssetPlugin;
|
|||||||
use bevy_sprite3d::Sprite3dPlugin;
|
use bevy_sprite3d::Sprite3dPlugin;
|
||||||
use bevy_trenchbroom::prelude::*;
|
use bevy_trenchbroom::prelude::*;
|
||||||
use bevy_ui_gradients::UiGradientsPlugin;
|
use bevy_ui_gradients::UiGradientsPlugin;
|
||||||
use heads_database::HeadDatabaseAsset;
|
|
||||||
use lightyear::prelude::server::ServerPlugins;
|
use lightyear::prelude::server::ServerPlugins;
|
||||||
use shared::*;
|
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use utils::{billboards, sprite_3d_animation, squish_animation, trail};
|
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod tb_entities;
|
||||||
|
|
||||||
plugin_group! {
|
plugin_group! {
|
||||||
pub struct DefaultPlugins {
|
pub struct DefaultPlugins {
|
||||||
@@ -105,10 +103,6 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(feature = "dbg")]
|
#[cfg(feature = "dbg")]
|
||||||
{
|
{
|
||||||
app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin {
|
|
||||||
enable_multipass_for_primary_context: true,
|
|
||||||
});
|
|
||||||
app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::new());
|
|
||||||
app.add_plugins(PhysicsDebugPlugin::default());
|
app.add_plugins(PhysicsDebugPlugin::default());
|
||||||
|
|
||||||
// app.add_plugins(bevy::pbr::wireframe::WireframePlugin)
|
// app.add_plugins(bevy::pbr::wireframe::WireframePlugin)
|
||||||
@@ -118,43 +112,45 @@ fn main() {
|
|||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.add_plugins(ai::plugin);
|
app.add_plugins(shared::abilities::plugin);
|
||||||
app.add_plugins(animation::plugin);
|
app.add_plugins(shared::ai::plugin);
|
||||||
app.add_plugins(character::plugin);
|
app.add_plugins(shared::aim::plugin);
|
||||||
app.add_plugins(cash::plugin);
|
app.add_plugins(shared::animation::plugin);
|
||||||
|
app.add_plugins(shared::backpack::plugin);
|
||||||
|
app.add_plugins(shared::camera::plugin);
|
||||||
|
app.add_plugins(shared::cash::plugin);
|
||||||
|
app.add_plugins(shared::cash_heal::plugin);
|
||||||
|
app.add_plugins(shared::character::plugin);
|
||||||
|
app.add_plugins(shared::control::plugin);
|
||||||
|
app.add_plugins(shared::cutscene::plugin);
|
||||||
|
app.add_plugins(shared::gates::plugin);
|
||||||
|
app.add_plugins(shared::head_drop::plugin);
|
||||||
|
app.add_plugins(shared::heads::plugin);
|
||||||
|
app.add_plugins(shared::heal_effect::plugin);
|
||||||
|
app.add_plugins(shared::hitpoints::plugin);
|
||||||
|
app.add_plugins(shared::keys::plugin);
|
||||||
|
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
||||||
|
app.add_plugins(shared::loading_map::plugin);
|
||||||
|
app.add_plugins(shared::movables::plugin);
|
||||||
|
app.add_plugins(shared::npc::plugin);
|
||||||
|
app.add_plugins(shared::platforms::plugin);
|
||||||
|
app.add_plugins(shared::player::plugin);
|
||||||
|
app.add_plugins(shared::protocol::plugin);
|
||||||
|
app.add_plugins(shared::sounds::plugin);
|
||||||
|
app.add_plugins(shared::steam::plugin);
|
||||||
|
app.add_plugins(shared::tb_entities::plugin);
|
||||||
|
app.add_plugins(shared::utils::auto_rotate::plugin);
|
||||||
|
app.add_plugins(shared::utils::billboards::plugin);
|
||||||
|
app.add_plugins(shared::utils::explosions::plugin);
|
||||||
|
app.add_plugins(shared::utils::sprite_3d_animation::plugin);
|
||||||
|
app.add_plugins(shared::utils::squish_animation::plugin);
|
||||||
|
app.add_plugins(shared::utils::trail::plugin);
|
||||||
|
app.add_plugins(shared::utils::plugin);
|
||||||
|
app.add_plugins(shared::water::plugin);
|
||||||
|
|
||||||
app.add_plugins(config::plugin);
|
app.add_plugins(config::plugin);
|
||||||
app.add_plugins(player::plugin);
|
|
||||||
app.add_plugins(gates::plugin);
|
|
||||||
app.add_plugins(platforms::plugin);
|
|
||||||
app.add_plugins(movables::plugin);
|
|
||||||
app.add_plugins(billboards::plugin);
|
|
||||||
app.add_plugins(aim::plugin);
|
|
||||||
app.add_plugins(protocol::plugin);
|
|
||||||
app.add_plugins(server::plugin);
|
app.add_plugins(server::plugin);
|
||||||
app.add_plugins(npc::plugin);
|
|
||||||
app.add_plugins(keys::plugin);
|
|
||||||
app.add_plugins(squish_animation::plugin);
|
|
||||||
app.add_plugins(cutscene::plugin);
|
|
||||||
app.add_plugins(control::plugin);
|
|
||||||
app.add_plugins(sounds::plugin);
|
|
||||||
app.add_plugins(camera::plugin);
|
|
||||||
app.add_plugins(backpack::plugin);
|
|
||||||
app.add_plugins(loading_assets::LoadingPlugin);
|
|
||||||
app.add_plugins(loading_map::plugin);
|
|
||||||
app.add_plugins(sprite_3d_animation::plugin);
|
|
||||||
app.add_plugins(abilities::plugin);
|
|
||||||
app.add_plugins(heads::plugin);
|
|
||||||
app.add_plugins(hitpoints::plugin);
|
|
||||||
app.add_plugins(cash_heal::plugin);
|
|
||||||
app.add_plugins(utils::plugin);
|
|
||||||
app.add_plugins(water::plugin);
|
|
||||||
app.add_plugins(head_drop::plugin);
|
|
||||||
app.add_plugins(trail::plugin);
|
|
||||||
app.add_plugins(auto_rotate::plugin);
|
|
||||||
app.add_plugins(heal_effect::plugin);
|
|
||||||
app.add_plugins(tb_entities::plugin);
|
app.add_plugins(tb_entities::plugin);
|
||||||
app.add_plugins(explosions::plugin);
|
|
||||||
app.add_plugins(steam::plugin);
|
|
||||||
|
|
||||||
app.init_state::<GameState>();
|
app.init_state::<GameState>();
|
||||||
|
|
||||||
|
|||||||
43
crates/server/src/tb_entities.rs
Normal file
43
crates/server/src/tb_entities.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use lightyear::prelude::{ActionsChannel, Connected, MessageSender};
|
||||||
|
use shared::{
|
||||||
|
GameState, global_observer,
|
||||||
|
protocol::{DespawnTbMapEntity, TbMapEntityId},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn plugin(app: &mut App) {
|
||||||
|
app.register_type::<DespawnedTbEntityCache>();
|
||||||
|
|
||||||
|
app.init_resource::<DespawnedTbEntityCache>();
|
||||||
|
|
||||||
|
app.add_systems(
|
||||||
|
OnEnter(GameState::MapLoading),
|
||||||
|
|mut cache: ResMut<DespawnedTbEntityCache>| cache.0.clear(),
|
||||||
|
);
|
||||||
|
|
||||||
|
global_observer!(app, add_despawned_entities_to_cache);
|
||||||
|
global_observer!(app, send_new_client_despawned_cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_despawned_entities_to_cache(
|
||||||
|
trigger: Trigger<OnRemove, TbMapEntityId>,
|
||||||
|
id: Query<&TbMapEntityId>,
|
||||||
|
mut cache: ResMut<DespawnedTbEntityCache>,
|
||||||
|
) {
|
||||||
|
cache.0.push(id.get(trigger.target()).unwrap().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Resource, Reflect)]
|
||||||
|
#[reflect(Resource)]
|
||||||
|
pub struct DespawnedTbEntityCache(pub Vec<u64>);
|
||||||
|
|
||||||
|
fn send_new_client_despawned_cache(
|
||||||
|
trigger: Trigger<OnAdd, Connected>,
|
||||||
|
cache: Res<DespawnedTbEntityCache>,
|
||||||
|
mut send: Query<&mut MessageSender<DespawnTbMapEntity>>,
|
||||||
|
) {
|
||||||
|
let mut send = send.get_mut(trigger.target()).unwrap();
|
||||||
|
for &id in cache.0.iter() {
|
||||||
|
send.send::<ActionsChannel>(DespawnTbMapEntity(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,14 +79,13 @@ fn update_player_aim(
|
|||||||
spatial_query: SpatialQuery,
|
spatial_query: SpatialQuery,
|
||||||
) {
|
) {
|
||||||
for (player, state, mut aim_target, children) in player_aim.iter_mut() {
|
for (player, state, mut aim_target, children) in player_aim.iter_mut() {
|
||||||
assert_eq!(
|
let player_rot_child = children
|
||||||
children.len(),
|
.iter()
|
||||||
1,
|
.find(|&child| player_rot.contains(child))
|
||||||
"expected player to have one direct child"
|
.expect("expected child with PlayerBodyMesh");
|
||||||
);
|
|
||||||
|
|
||||||
let (player_pos, player_forward) = player_rot
|
let (player_pos, player_forward) = player_rot
|
||||||
.get(*children.first().unwrap())
|
.get(player_rot_child)
|
||||||
.map(|(t, global)| (global.translation(), t.forward()))
|
.map(|(t, global)| (global.translation(), t.forward()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub struct DebugVisuals {
|
|||||||
pub cam_follow: bool,
|
pub cam_follow: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
#[derive(States, Default, Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||||
pub enum GameState {
|
pub enum GameState {
|
||||||
#[default]
|
#[default]
|
||||||
AssetLoading,
|
AssetLoading,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
#[cfg(feature = "server")]
|
||||||
|
use crate::protocol::TbMapEntityId;
|
||||||
use crate::{GameState, physics_layers::GameLayer};
|
use crate::{GameState, physics_layers::GameLayer};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_trenchbroom::physics::SceneCollidersReady;
|
use bevy_trenchbroom::physics::SceneCollidersReady;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use lightyear::prelude::{DisableReplicateHierarchy, NetworkTarget, Replicate};
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.add_systems(OnEnter(GameState::MapLoading), setup_scene);
|
app.add_systems(OnEnter(GameState::MapLoading), setup_scene);
|
||||||
@@ -12,14 +16,33 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
.spawn((
|
.spawn((
|
||||||
CollisionLayers::new(LayerMask(GameLayer::Level.to_bits()), LayerMask::ALL),
|
CollisionLayers::new(LayerMask(GameLayer::Level.to_bits()), LayerMask::ALL),
|
||||||
SceneRoot(asset_server.load("maps/map1.map#Scene")),
|
SceneRoot(asset_server.load("maps/map1.map#Scene")),
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
Replicate::to_clients(NetworkTarget::All),
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
DisableReplicateHierarchy,
|
||||||
))
|
))
|
||||||
.observe(
|
.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<
|
#[cfg(any(feature = "client", feature = "server"))] mut next_game_state: ResMut<
|
||||||
NextState<GameState>,
|
NextState<GameState>,
|
||||||
>| {
|
>| {
|
||||||
info!("map loaded");
|
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"));
|
assert!(cfg!(feature = "client") ^ cfg!(feature = "server"));
|
||||||
|
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ use crate::{
|
|||||||
GameState,
|
GameState,
|
||||||
tb_entities::{Platform, PlatformTarget},
|
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 bevy_trenchbroom::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Component, Reflect, Default, Debug)]
|
#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
struct ActivePlatform {
|
pub struct ActivePlatform {
|
||||||
pub start: Vec3,
|
pub start: Vec3,
|
||||||
pub target: Vec3,
|
pub target: Vec3,
|
||||||
}
|
}
|
||||||
@@ -15,6 +18,7 @@ struct ActivePlatform {
|
|||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.register_type::<ActivePlatform>();
|
app.register_type::<ActivePlatform>();
|
||||||
app.add_systems(OnEnter(GameState::Playing), init);
|
app.add_systems(OnEnter(GameState::Playing), init);
|
||||||
|
#[cfg(feature = "server")]
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
move_active.run_if(in_state(GameState::Playing)),
|
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)>) {
|
fn move_active(time: Res<Time>, mut platforms: Query<(&mut Transform, &mut ActivePlatform)>) {
|
||||||
for (mut transform, active) in platforms.iter_mut() {
|
for (mut transform, active) in platforms.iter_mut() {
|
||||||
let t = (sin(time.elapsed_secs() * 0.4) + 1.) / 2.;
|
let t = (sin(time.elapsed_secs() * 0.4) + 1.) / 2.;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
GameState,
|
||||||
abilities::BuildExplosionSprite,
|
abilities::BuildExplosionSprite,
|
||||||
animation::AnimationFlags,
|
animation::AnimationFlags,
|
||||||
backpack::Backpack,
|
backpack::Backpack,
|
||||||
@@ -15,11 +16,15 @@ use crate::{
|
|||||||
heads::ActiveHeads,
|
heads::ActiveHeads,
|
||||||
hitpoints::Hitpoints,
|
hitpoints::Hitpoints,
|
||||||
loading_assets::{GameAssets, HeadDropAssets},
|
loading_assets::{GameAssets, HeadDropAssets},
|
||||||
|
platforms::ActivePlatform,
|
||||||
player::{Player, PlayerBodyMesh},
|
player::{Player, PlayerBodyMesh},
|
||||||
utils::triggers::TriggerAppExt,
|
utils::triggers::TriggerAppExt,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
||||||
use bevy::prelude::*;
|
use bevy::{
|
||||||
|
ecs::{component::HookContext, world::DeferredWorld},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use happy_feet::{
|
use happy_feet::{
|
||||||
grounding::GroundingState,
|
grounding::GroundingState,
|
||||||
prelude::{
|
prelude::{
|
||||||
@@ -28,19 +33,31 @@ use happy_feet::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lightyear::prelude::{
|
use lightyear::prelude::{
|
||||||
ActionsChannel, AppComponentExt, PredictionMode, PredictionRegistrationExt,
|
ActionsChannel, AppComponentExt, AppMessageExt, NetworkDirection, PredictionMode,
|
||||||
input::native::InputPlugin,
|
PredictionRegistrationExt, input::native::InputPlugin,
|
||||||
};
|
};
|
||||||
use lightyear_serde::{
|
use lightyear_serde::{
|
||||||
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.add_plugins(InputPlugin::<ControlState>::default());
|
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::<ActiveHead>();
|
||||||
app.register_component::<ActiveHeads>();
|
app.register_component::<ActiveHeads>();
|
||||||
|
app.register_component::<ActivePlatform>();
|
||||||
app.register_component::<AngularVelocity>();
|
app.register_component::<AngularVelocity>();
|
||||||
app.register_component::<AnimatedCharacter>();
|
app.register_component::<AnimatedCharacter>();
|
||||||
app.register_component::<AnimationFlags>();
|
app.register_component::<AnimationFlags>();
|
||||||
@@ -69,6 +86,7 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.register_component::<PlayerBodyMesh>();
|
app.register_component::<PlayerBodyMesh>();
|
||||||
app.register_component::<PlayerCharacterController>();
|
app.register_component::<PlayerCharacterController>();
|
||||||
app.register_component::<SteppingConfig>();
|
app.register_component::<SteppingConfig>();
|
||||||
|
app.register_component::<TbMapEntityId>();
|
||||||
app.register_component::<Transform>()
|
app.register_component::<Transform>()
|
||||||
.add_prediction(PredictionMode::Full)
|
.add_prediction(PredictionMode::Full)
|
||||||
.add_should_rollback(transform_should_rollback);
|
.add_should_rollback(transform_should_rollback);
|
||||||
@@ -89,9 +107,66 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
|
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
|
||||||
app.replicate_trigger::<StartCutscene, 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);
|
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 {
|
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
|
||||||
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
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 avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
ecs::{component::HookContext, world::DeferredWorld},
|
ecs::{component::HookContext, world::DeferredWorld},
|
||||||
@@ -7,6 +9,7 @@ use bevy::{
|
|||||||
};
|
};
|
||||||
use bevy_trenchbroom::prelude::*;
|
use bevy_trenchbroom::prelude::*;
|
||||||
use happy_feet::prelude::PhysicsMover;
|
use happy_feet::prelude::PhysicsMover;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
#[derive(PointClass, Component, Reflect, Default)]
|
#[derive(PointClass, Component, Reflect, Default)]
|
||||||
@@ -72,7 +75,7 @@ pub struct PlatformTarget {
|
|||||||
pub targetname: String,
|
pub targetname: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(SolidClass, Component, Reflect, Default)]
|
#[derive(SolidClass, Component, Reflect, Default, Serialize, Deserialize, PartialEq)]
|
||||||
#[reflect(QuakeClass, Component)]
|
#[reflect(QuakeClass, Component)]
|
||||||
#[base(Transform, Target)]
|
#[base(Transform, Target)]
|
||||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||||
@@ -202,4 +205,15 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.register_type::<EnemySpawn>();
|
app.register_type::<EnemySpawn>();
|
||||||
app.register_type::<CashSpawn>();
|
app.register_type::<CashSpawn>();
|
||||||
app.register_type::<SecretHead>();
|
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