Skip to content

Commit

Permalink
Multiplayer: impl transform syncing (#736)
Browse files Browse the repository at this point in the history
  • Loading branch information
Indy2222 authored Sep 19, 2023
1 parent c373022 commit 80cac40
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions crates/movement/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ categories.workspace = true
de_core.workspace = true
de_index.workspace = true
de_map.workspace = true
de_messages.workspace = true
de_multiplayer.workspace = true
de_objects.workspace = true
de_pathing.workspace = true
de_types.workspace = true

# Other
bevy.workspace = true
parry3d.workspace = true
parry2d.workspace = true
fastrand.workspace = true
glam.workspace = true
parry2d.workspace = true
parry3d.workspace = true
3 changes: 3 additions & 0 deletions crates/movement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod movement;
mod obstacles;
mod pathing;
mod repulsion;
mod syncing;

use std::f32::consts::PI;

Expand All @@ -16,6 +17,7 @@ use movement::MovementPlugin;
use obstacles::ObstaclesPlugin;
use pathing::PathingPlugin;
use repulsion::RepulsionPlugin;
use syncing::SyncingPlugin;

/// Maximum object horizontal speed in meters per second.
const MAX_H_SPEED: f32 = 10.;
Expand Down Expand Up @@ -43,5 +45,6 @@ impl PluginGroup for MovementPluginGroup {
.add(RepulsionPlugin)
.add(KinematicsPlugin)
.add(AltitudePlugin)
.add(SyncingPlugin)
}
}
112 changes: 112 additions & 0 deletions crates/movement/src/syncing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use std::time::Duration;

use bevy::prelude::*;
use de_core::{
gamestate::GameState,
gconfig::GameConfig,
objects::{Local, MovableSolid},
schedule::{Movement, PreMovement},
state::AppState,
};
use de_messages::ToPlayers;
use de_multiplayer::{NetEntities, NetRecvTransformEvent, ToPlayersEvent};

use crate::movement::MovementSet;

const MIN_SYNC_PERIOD: Duration = Duration::from_secs(2);
const SYNC_RANDOMIZATION_MS: u64 = 2_000;

pub(crate) struct SyncingPlugin;

impl Plugin for SyncingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PreMovement,
setup_entities
.run_if(is_multiplayer)
.run_if(in_state(AppState::InGame)),
)
.add_systems(
Movement,
(
receive_transforms
.run_if(on_event::<NetRecvTransformEvent>())
.after(MovementSet::UpdateTransform),
send_transforms
.run_if(is_multiplayer)
.after(MovementSet::UpdateTransform),
)
.run_if(in_state(GameState::Playing)),
);
}
}

#[derive(Component)]
struct SyncTimer(Duration);

impl SyncTimer {
fn schedule(time: Duration) -> Duration {
let jitter = Duration::from_millis(fastrand::u64(0..SYNC_RANDOMIZATION_MS));
time + MIN_SYNC_PERIOD + jitter
}

fn new(time: Duration) -> Self {
Self(Self::schedule(time))
}

/// Sets sync expiration to the future relative to the current time.
fn refresh(&mut self, time: Duration) {
self.0 = Self::schedule(time);
}

/// Returns true if transform sync is already due.
fn outdated(&self, time: Duration) -> bool {
time >= self.0
}
}

type NotSetUp = (With<MovableSolid>, With<Local>, Without<SyncTimer>);

fn is_multiplayer(config: Option<Res<GameConfig>>) -> bool {
match config {
Some(config) => config.multiplayer(),
None => false,
}
}

fn setup_entities(mut commands: Commands, time: Res<Time>, entities: Query<Entity, NotSetUp>) {
let time = time.elapsed();
for entity in entities.iter() {
commands.entity(entity).insert(SyncTimer::new(time));
}
}

fn receive_transforms(
mut entities: Query<&mut Transform>,
mut events: EventReader<NetRecvTransformEvent>,
) {
for event in events.iter() {
if let Ok(mut transform) = entities.get_mut(event.entity()) {
*transform = event.transform();
}
}
}

fn send_transforms(
time: Res<Time>,
net_entities: NetEntities,
mut entities: Query<(Entity, &mut SyncTimer, &Transform)>,
mut net_events: EventWriter<ToPlayersEvent>,
) {
let time = time.elapsed();
for (entity, mut sync, transform) in entities.iter_mut() {
if sync.outdated(time) {
sync.refresh(time);

net_events.send(ToPlayersEvent::new(ToPlayers::Transform {
entity: net_entities.local_net_id(entity),
transform: transform.into(),
}));
}
}
}
2 changes: 1 addition & 1 deletion crates/multiplayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub use crate::{
netstate::NetState,
playermsg::{
GameNetSet, NetEntities, NetEntityCommands, NetRecvDespawnActiveEvent, NetRecvHealthEvent,
NetRecvSpawnActiveEvent,
NetRecvSpawnActiveEvent, NetRecvTransformEvent,
},
};
use crate::{netstate::NetStatePlugin, network::NetworkPlugin};
Expand Down
34 changes: 32 additions & 2 deletions crates/multiplayer/src/playermsg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ impl Plugin for PlayerMsgPlugin {
app.add_event::<NetRecvSpawnActiveEvent>()
.add_event::<NetRecvDespawnActiveEvent>()
.add_event::<NetRecvHealthEvent>()
.add_event::<NetRecvTransformEvent>()
.add_systems(OnEnter(AppState::InGame), setup)
.add_systems(OnExit(AppState::InGame), cleanup)
.add_systems(
Expand Down Expand Up @@ -124,6 +125,26 @@ impl NetRecvHealthEvent {
}
}

#[derive(Event)]
pub struct NetRecvTransformEvent {
entity: Entity,
transform: Transform,
}

impl NetRecvTransformEvent {
fn new(entity: Entity, transform: Transform) -> Self {
Self { entity, transform }
}

pub fn entity(&self) -> Entity {
self.entity
}

pub fn transform(&self) -> Transform {
self.transform
}
}

#[derive(SystemParam)]
pub struct NetEntities<'w> {
config: Res<'w, GameConfig>,
Expand Down Expand Up @@ -172,10 +193,13 @@ impl<'w> NetEntityCommands<'w> {
}

fn local_id(&self, entity: EntityNet) -> Option<Entity> {
self.map
.translate_remote(entity)
self.remote_local_id(entity)
.or_else(|| self.entities.resolve_from_id(entity.index().into()))
}

fn remote_local_id(&self, entity: EntityNet) -> Option<Entity> {
self.map.translate_remote(entity)
}
}

/// Mapping between remote and local entity IDs for non-locally simulated
Expand Down Expand Up @@ -305,6 +329,7 @@ fn recv_messages(
mut inputs: EventReader<FromPlayersEvent>,
mut spawn_events: EventWriter<NetRecvSpawnActiveEvent>,
mut despawn_events: EventWriter<NetRecvDespawnActiveEvent>,
mut transform_events: EventWriter<NetRecvTransformEvent>,
mut health_events: EventWriter<NetRecvHealthEvent>,
) {
for input in inputs.iter() {
Expand All @@ -329,6 +354,11 @@ fn recv_messages(
let local = net_commands.deregister(*entity);
despawn_events.send(NetRecvDespawnActiveEvent::new(local));
}
ToPlayers::Transform { entity, transform } => {
if let Some(local) = net_commands.remote_local_id(*entity) {
transform_events.send(NetRecvTransformEvent::new(local, transform.into()));
}
}
ToPlayers::ChangeHealth { entity, delta } => {
let Some(local) = net_commands.local_id(*entity) else {
warn!("Received net health update of unrecognized entity: {entity:?}");
Expand Down

0 comments on commit 80cac40

Please sign in to comment.