Skip to content

Commit

Permalink
The server's entity map is the API resource, naming is hard
Browse files Browse the repository at this point in the history
  • Loading branch information
RJ committed Oct 5, 2023
1 parent 67e59b1 commit c661d35
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 64 deletions.
4 changes: 0 additions & 4 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,6 @@ pub enum ClientSet {
Send,
}

/// Signature for callback, when an entity matched to a predicted client entity
/// typically you want to remove any prediction component in here.
pub type PredictionHitFn = fn(&mut EntityMut);

/// Maps server entities to client entities and vice versa.
///
/// Used only on client.
Expand Down
13 changes: 8 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,13 @@ to the client in the custom deserialization, despawn and component removal funct
One use for this is rollback networking: you may want to rollback time and apply the update
for the tick frame, which is in the past, then resimulate.
### Client-predicted spawns
### Mapping to existing client entities
If you want to spawn entities on the client before the server replicates the spawn
to the client, see [`PredictionTracker`].
If you want to spawn entities on the client before the server replicates the spawn back
to the client, and use the existing client entity to replicate data into, see [`RepliconEntityMap`]
This can be useful for certain types of game. Eg, spawning bullets on the client immediately without
waiting on replication.
### "Blueprints" pattern
Expand Down Expand Up @@ -405,8 +408,8 @@ pub mod prelude {
NetworkChannels, RepliconCorePlugin,
},
server::{
has_authority, prediction_tracker::PredictionTracker, AckedTicks, ServerPlugin,
ServerSet, TickPolicy, SERVER_ID,
has_authority, AckedTicks, RepliconEntityMap, ServerPlugin, ServerSet, TickPolicy,
SERVER_ID,
},
ReplicationPlugins,
};
Expand Down
12 changes: 6 additions & 6 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub(super) mod despawn_tracker;
pub(super) mod prediction_tracker;
pub(super) mod removal_tracker;
pub(super) mod replication_buffer;
pub(super) mod replicon_entity_map;

use std::time::Duration;

Expand All @@ -26,9 +26,9 @@ use crate::replicon_core::{
replication_rules::ReplicationRules, replicon_tick::RepliconTick, REPLICATION_CHANNEL_ID,
};
use despawn_tracker::{DespawnTracker, DespawnTrackerPlugin};
use prediction_tracker::PredictionTracker;
use removal_tracker::{RemovalTracker, RemovalTrackerPlugin};
use replication_buffer::ReplicationBuffer;
pub use replicon_entity_map::RepliconEntityMap;

pub const SERVER_ID: u64 = 0;

Expand All @@ -54,7 +54,7 @@ impl Plugin for ServerPlugin {
))
.init_resource::<AckedTicks>()
.init_resource::<RepliconTick>()
.init_resource::<PredictionTracker>()
.init_resource::<RepliconEntityMap>()
.configure_set(
PreUpdate,
ServerSet::Receive.after(NetcodeServerPlugin::update_system),
Expand Down Expand Up @@ -116,7 +116,7 @@ impl ServerPlugin {
fn acks_receiving_system(
mut acked_ticks: ResMut<AckedTicks>,
mut server: ResMut<RenetServer>,
mut predictions: ResMut<PredictionTracker>,
mut predictions: ResMut<RepliconEntityMap>,
) {
for client_id in server.clients_id() {
while let Some(message) = server.receive_message(client_id, REPLICATION_CHANNEL_ID) {
Expand Down Expand Up @@ -160,7 +160,7 @@ impl ServerPlugin {
despawn_tracker: Res<DespawnTracker>,
replicon_tick: Res<RepliconTick>,
removal_trackers: Query<(Entity, &RemovalTracker)>,
predictions: Res<PredictionTracker>,
predictions: Res<RepliconEntityMap>,
) -> Result<(), bincode::Error> {
let mut acked_ticks = set.p2();
acked_ticks.register_tick(*replicon_tick, change_tick.this_run());
Expand Down Expand Up @@ -223,7 +223,7 @@ fn prepare_buffers<'a>(
fn collect_mappings(
buffers: &mut [ReplicationBuffer],
acked_ticks: &ResMut<AckedTicks>,
predictions: &Res<PredictionTracker>,
predictions: &Res<RepliconEntityMap>,
) -> Result<(), bincode::Error> {
for buffer in &mut *buffers {
// Include all entity mappings since the last acknowledged tick.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ use bevy::{
};

use super::RepliconTick;
/// Tracks client-predicted entities, which are sent along with spawn data when the corresponding
/// entity is created on the server.
/// ['RepliconEntityMap'] is a resource that exists on the server for mapping server entities to
/// entities that clients have already spawned. The mappings are sent to clients and injected into
/// the client's [`crate::client::NetworkEntityMap`].
///
/// Sometimes you don't want to wait for the server to spawn something before it appears on the
/// client. When a client presses shoot, they can immediately spawn the bullet, then match up that
/// client – when a client presses shoot, they can immediately spawn the bullet, then match up that
/// entity with the eventual replicated bullet the server spawns, rather than have replication spawn
/// a brand new bullet on the client.
///
/// ### Example usage:
/// In this situation, the server can write the client `Entity` it sent (in your game's custom
/// protocol) into the [`RepliconEntityMap`], associating it with the newly spawned server entity.
///
/// Replication packets will send a list of such mappings
/// to clients, which will be inserted into the client's [`crate::client::NetworkEntityMap`].
///
/// Your client presses shoot and spawns a predicted bullet immediately, in anticipation of the
/// server replicating a newly spawned bullet, and matching it up to our predicted entity:
/// ### Example usage:
///
/// ```rust,ignore
/// // on client:
Expand All @@ -25,12 +29,12 @@ use super::RepliconTick;
/// send_shoot_command_to_server(client_predicted_entity);
/// }
/// // on server:
/// fn apply_inputs_system(mut predictions: ResMut<PredictionTracker>, tick: Res<RepliconTick>) {
/// fn apply_inputs_system(mut entity_map: ResMut<RepliconEntityMap>, tick: Res<RepliconTick>) {
/// // ...
/// if player_input.pressed_shoot() {
/// let server_entity = commands.spawn((Bullet, Replication, Etc)).id();
/// // your game's netcode checks for a client predicted entity, and registers it here:
/// predictions.insert(
/// entity_map.insert(
/// player_input.client_id,
/// server_entity,
/// player_input.client_predicted_entity,
Expand All @@ -42,37 +46,13 @@ use super::RepliconTick;
///
/// Provided that `client_predicted_entity` exists when the replication data for `server_entity`
/// arrives, replicated data will be applied to that entity instead of spawning a new one.
/// You can detect when this happens by querying for `Added<Replication>` on your client entity.
///
/// If `client_predicted_entity` is not found, a new entity will be spawned on the client,
/// just the same as when no client prediction is provided.
///
/// ### Successful prediction detection
///
/// Upon successful replication, the predicted client entity will receive the Replication component.
///
/// Check for this in a system to perform cleanup:
///
/// ```rust
/// # use bevy::prelude::*;
/// # #[derive(Component)]
/// # struct Prediction;
/// # #[derive(Component)]
/// # struct Replication;
/// fn cleanup_successful_predictions(
/// q: Query<Entity, (With<Prediction>, Added<Replication>)>,
/// mut commands: Commands,
/// ) {
/// for entity in q.iter() {
/// commands.entity(entity).remove::<Prediction>();
/// }
/// }
/// ```
///
/// Typically your Prediction marker component might include a TTL or timeout, after which the
/// predicted entity would be despawned by your game's misprediction cleanup system.
///
#[derive(Resource, Debug, Default)]
pub struct PredictionTracker {
pub struct RepliconEntityMap {
mappings: HashMap<u64, Vec<EntityMapping>>,
}
pub(crate) type EntityMapping = (RepliconTick, ServerEntity, ClientEntity);
Expand All @@ -81,9 +61,11 @@ pub(crate) type EntityMapping = (RepliconTick, ServerEntity, ClientEntity);
pub(crate) type ServerEntity = Entity;
pub(crate) type ClientEntity = Entity;

impl PredictionTracker {
impl RepliconEntityMap {
/// Register that the server spawned `server_entity` as a result of `client_id` sending a
/// command which also included a `client_entity` denoting the client's predicted local spawn.
/// command which included a `client_entity` they already spawned. This will be sent and added
/// to the client's [`crate::client::NetworkEntityMap`].
///
/// The current `tick` is needed so that this prediction data can be cleaned up once the tick
/// has been acked by the client.
pub fn insert(
Expand All @@ -100,19 +82,19 @@ impl PredictionTracker {
self.mappings.insert(client_id, vec![new_entry]);
}
}
/// gives an optional iter over (tick, server_entity, client_entity)
/// Get entity mappings for a client that have been added since the `from_tick`.
pub(crate) fn get_mappings(
&self,
client_id: u64,
tick: RepliconTick,
from_tick: RepliconTick,
) -> Option<Vec<(ServerEntity, ClientEntity)>> {
let Some(v) = self.mappings.get(&client_id) else {
return None;
};
Some(
v.iter()
.filter_map(|(entry_tick, server_entity, client_entity)| {
if *entry_tick >= tick {
if *entry_tick >= from_tick {
Some((*server_entity, *client_entity))
} else {
None
Expand All @@ -121,18 +103,12 @@ impl PredictionTracker {
.collect::<Vec<(Entity, Entity)>>(),
)
}
/// remove predicted entities in cases where the RepliconTick at which that entity was spawned
/// has been acked by a client.
/// remove predicted entities in cases where the RepliconTick at which that entity mapping
/// was created has been acked by the client.
pub(crate) fn cleanup_acked(&mut self, client_id: u64, acked_tick: RepliconTick) {
let Some(v) = self.mappings.get_mut(&client_id) else {
return;
};
v.retain(|(tick, _, _)| {
if *tick > acked_tick {
// not acked yet, retain it
return true;
}
false
});
v.retain(|(tick, _, _)| *tick > acked_tick);
}
}
2 changes: 1 addition & 1 deletion tests/replication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ fn spawn_prediction_replication() {
// and registers the client's predicted entity
server_app
.world
.resource_scope(|_world, mut pt: Mut<PredictionTracker>| {
.resource_scope(|_world, mut pt: Mut<RepliconEntityMap>| {
pt.insert(client_id, server_entity, client_predicted_entity, tick)
});
// an edge case to test is when the server spawns an entity that has a predicted entity,
Expand Down

0 comments on commit c661d35

Please sign in to comment.