Skip to content

Commit

Permalink
Make changes to NetworkTick, now a Resource, trigger sends.
Browse files Browse the repository at this point in the history
Allows for more flexible fixedtimestep integration, with
manual incrementing of the network tick.
  • Loading branch information
RJ committed Oct 2, 2023
1 parent 5b13e47 commit af6a5d1
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 63 deletions.
2 changes: 1 addition & 1 deletion examples/tic_tac_toe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn main() {
}),
..Default::default()
}))
.add_plugins((ReplicationPlugins, TicTacToePlugin))
.add_plugins((ReplicationPlugins::default(), TicTacToePlugin))
.run();
}

Expand Down
67 changes: 52 additions & 15 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ use bevy_renet::{renet::RenetClient, transport::NetcodeClientPlugin, RenetClient
use bincode::{DefaultOptions, Options};

use crate::replicon_core::{
replication_rules::{Mapper, Replication, ReplicationRules},
replication_rules::{EntityDespawnFn, Mapper, Replication, ReplicationRules},
NetworkTick, REPLICATION_CHANNEL_ID,
};

pub struct ClientPlugin;
#[derive(Default)]
pub struct ClientPlugin {
despawn_fn: Option<EntityDespawnFn>,
}

impl Plugin for ClientPlugin {
fn build(&self, app: &mut App) {
Expand Down Expand Up @@ -45,10 +48,24 @@ impl Plugin for ClientPlugin {
Self::reset_system.run_if(resource_removed::<RenetClient>()),
),
);
if let Some(entity_despawn_fn) = self.despawn_fn {
app.world
.resource_scope(|_, mut replication_rules: Mut<ReplicationRules>| {
replication_rules.set_despawn_fn(entity_despawn_fn);
});
}
}
}

impl ClientPlugin {
/// only useful in case you need to replace the default entity despawn function.
/// otherwis just use `ClientPlugin::default()`
pub fn new(despawn_fn: EntityDespawnFn) -> Self {
Self {
despawn_fn: Some(despawn_fn),
}
}

fn diff_receiving_system(world: &mut World) -> Result<(), bincode::Error> {
world.resource_scope(|world, mut client: Mut<RenetClient>| {
world.resource_scope(|world, mut entity_map: Mut<NetworkEntityMap>| {
Expand All @@ -57,7 +74,9 @@ impl ClientPlugin {
let end_pos: u64 = message.len().try_into().unwrap();
let mut cursor = Cursor::new(message);

if !deserialize_tick(&mut cursor, world)? {
let (network_tick, was_updated) = deserialize_tick(&mut cursor, world)?;

if !was_updated {
continue;
}
if cursor.position() == end_pos {
Expand All @@ -70,6 +89,7 @@ impl ClientPlugin {
&mut entity_map,
&replication_rules,
DiffKind::Change,
network_tick,
)?;
if cursor.position() == end_pos {
continue;
Expand All @@ -81,12 +101,19 @@ impl ClientPlugin {
&mut entity_map,
&replication_rules,
DiffKind::Removal,
network_tick,
)?;
if cursor.position() == end_pos {
continue;
}

deserialize_despawns(&mut cursor, world, &mut entity_map)?;
deserialize_despawns(
&mut cursor,
world,
&mut entity_map,
network_tick,
replication_rules.get_despawn_fn(),
)?;
}

Ok(())
Expand All @@ -109,16 +136,19 @@ impl ClientPlugin {

/// Deserializes server tick and applies it to [`LastTick`] if it is newer.
///
/// Returns true if [`LastTick`] has been updated.
fn deserialize_tick(cursor: &mut Cursor<Bytes>, world: &mut World) -> Result<bool, bincode::Error> {
let tick = bincode::deserialize_from(cursor)?;
/// Returns (network_tick, true) if [`LastTick`] has been updated, otherwise (network_tick, false).
fn deserialize_tick(
cursor: &mut Cursor<Bytes>,
world: &mut World,
) -> Result<(NetworkTick, bool), bincode::Error> {
let network_tick = bincode::deserialize_from(cursor)?;

let mut last_tick = world.resource_mut::<LastTick>();
if last_tick.0 < tick {
last_tick.0 = tick;
Ok(true)
if last_tick.0 < network_tick {
last_tick.0 = network_tick;
Ok((network_tick, true))
} else {
Ok(false)
Ok((network_tick, false))
}
}

Expand All @@ -129,6 +159,7 @@ fn deserialize_component_diffs(
entity_map: &mut NetworkEntityMap,
replication_rules: &ReplicationRules,
diff_kind: DiffKind,
tick: NetworkTick,
) -> Result<(), bincode::Error> {
let entities_count: u16 = bincode::deserialize_from(&mut *cursor)?;
for _ in 0..entities_count {
Expand All @@ -141,9 +172,9 @@ fn deserialize_component_diffs(
let replication_info = unsafe { replication_rules.get_info_unchecked(replication_id) };
match diff_kind {
DiffKind::Change => {
(replication_info.deserialize)(&mut entity, entity_map, cursor)?
(replication_info.deserialize)(&mut entity, entity_map, cursor, tick)?
}
DiffKind::Removal => (replication_info.remove)(&mut entity),
DiffKind::Removal => (replication_info.remove)(&mut entity, tick),
}
}
}
Expand All @@ -156,18 +187,24 @@ fn deserialize_despawns(
cursor: &mut Cursor<Bytes>,
world: &mut World,
entity_map: &mut NetworkEntityMap,
tick: NetworkTick,
custom_entity_despawn_fn: Option<EntityDespawnFn>,
) -> Result<(), bincode::Error> {
let entities_count: u16 = bincode::deserialize_from(&mut *cursor)?;
for _ in 0..entities_count {
// The entity might have already been despawned because of hierarchy or
// with the last diff, but the server might not yet have received confirmation
// from the client and could include the deletion in the latest diff.
let server_entity = deserialize_entity(&mut *cursor)?;
if let Some(client_entity) = entity_map
if let Some(mut client_entity) = entity_map
.remove_by_server(server_entity)
.and_then(|entity| world.get_entity_mut(entity))
{
client_entity.despawn_recursive();
if let Some(despawn_fn) = custom_entity_despawn_fn {
(despawn_fn)(&mut client_entity, tick);
} else {
client_entity.despawn_recursive();
}
}
}

Expand Down
26 changes: 21 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ app.add_plugins(MinimalPlugins)
.add_plugins(ReplicationPlugins);
```
This group contains necessary replication stuff and setups server and client
plugins to let you host and join games from the same application. If you
planning to separate client and server you can use
This group contains necessary replication stuff and sets up the server and client
plugins to let you host and join games from the same application.
If you are planning to separate client and server you can use
`disable()` to disable [`ClientPlugin`] or
[`ServerPlugin`]. You can also configure how often updates are sent from
server to clients with [`ServerPlugin`]'s [`TickPolicy`].:
Expand Down Expand Up @@ -115,6 +116,7 @@ fn deserialize_transform(
entity: &mut EntityMut,
_entity_map: &mut NetworkEntityMap,
cursor: &mut Cursor<Bytes>,
_tick: NetworkTick,
) -> Result<(), bincode::Error> {
let translation: Vec3 = bincode::deserialize_from(cursor)?;
entity.insert(Transform::from_translation(translation));
Expand All @@ -131,6 +133,18 @@ will be replicated.
If you need to disable replication for specific component for specific entity,
you can insert [`Ignored<T>`] component and replication will be skipped for `T`.
### NetworkTick, and fixed timestep games.
The [`ServerPlugin`] sends replication data in `PostUpdate` any time the [`NetworkTick`] resource
changes. By default, NetworkTick is incremented in PostUpdate per the [`TickPolicy`].
If you set `TickPolicy::Manual`, you can increment [`NetworkTick`] at the start of your
`FixedTimestep` game loop. This value can represent your simulation step, and is made available
to the client in the custom deserialization, despawn andcomponent removal functions.
One use for this is rollback networking: you may want to rollback time and apply the update
for the NetworkTick frame, which is in the past, then resimulate.
### "Blueprints" pattern
The idea was borrowed from [iyes_scene_tools](https://github.com/IyesGames/iyes_scene_tools#blueprints-pattern).
Expand Down Expand Up @@ -175,7 +189,7 @@ fn player_init_system(
#[derive(Component, Deserialize, Serialize)]
struct Player;
# fn serialize_transform(_: Ptr, _: &mut Cursor<Vec<u8>>) -> Result<(), bincode::Error> { unimplemented!() }
# fn deserialize_transform(_: &mut EntityMut, _: &mut NetworkEntityMap, _: &mut Cursor<Bytes>) -> Result<(), bincode::Error> { unimplemented!() }
# fn deserialize_transform(_: &mut EntityMut, _: &mut NetworkEntityMap, _: &mut Cursor<Bytes>, _: NetworkTick) -> Result<(), bincode::Error> { unimplemented!() }
```
This pairs nicely with server state serialization and keeps saves clean.
Expand Down Expand Up @@ -393,14 +407,16 @@ pub use bevy_renet::*;
pub use bincode;
use prelude::*;

/// Plugin Group for all replicon plugins.
#[derive(Default)]
pub struct ReplicationPlugins;

impl PluginGroup for ReplicationPlugins {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(RepliconCorePlugin)
.add(ParentSyncPlugin)
.add(ClientPlugin)
.add(ClientPlugin::default())
.add(ServerPlugin::default())
}
}
17 changes: 12 additions & 5 deletions src/replicon_core.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
pub mod replication_rules;

use std::cmp::Ordering;

use bevy::prelude::*;
use bevy_renet::renet::{ChannelConfig, SendType};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;

use replication_rules::ReplicationRules;

Expand Down Expand Up @@ -74,11 +73,19 @@ fn channel_configs(channels: &[SendType]) -> Vec<ChannelConfig> {
channel_configs
}

/// Corresponds to the number of server update.
/// A tick that increments each time we need the server to compute and sends an update.
/// This is mapped to the bevy Tick within [`ServerTicks`].
///
/// See also [`crate::server::TickPolicy`].
#[derive(Clone, Copy, Default, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct NetworkTick(u32);
#[derive(Clone, Copy, Default, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Resource)]
pub struct NetworkTick(pub u32);

impl std::ops::Deref for NetworkTick {
type Target = u32;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl NetworkTick {
/// Creates a new [`NetworkTick`] wrapping the given value.
Expand Down
Loading

0 comments on commit af6a5d1

Please sign in to comment.