From c661d35e2789cd74e4272c87330886cc9ff030e2 Mon Sep 17 00:00:00 2001 From: RJ Date: Thu, 5 Oct 2023 10:17:56 +0100 Subject: [PATCH] The server's entity map is the API resource, naming is hard --- src/client.rs | 4 -- src/lib.rs | 13 ++-- src/server.rs | 12 ++-- ...tion_tracker.rs => replicon_entity_map.rs} | 72 +++++++------------ tests/replication.rs | 2 +- 5 files changed, 39 insertions(+), 64 deletions(-) rename src/server/{prediction_tracker.rs => replicon_entity_map.rs} (63%) diff --git a/src/client.rs b/src/client.rs index f3868fce..94a0d60e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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. diff --git a/src/lib.rs b/src/lib.rs index 56ac7d09..36c0e967 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 @@ -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, }; diff --git a/src/server.rs b/src/server.rs index 4ec38596..ba584def 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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; @@ -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; @@ -54,7 +54,7 @@ impl Plugin for ServerPlugin { )) .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() .configure_set( PreUpdate, ServerSet::Receive.after(NetcodeServerPlugin::update_system), @@ -116,7 +116,7 @@ impl ServerPlugin { fn acks_receiving_system( mut acked_ticks: ResMut, mut server: ResMut, - mut predictions: ResMut, + mut predictions: ResMut, ) { for client_id in server.clients_id() { while let Some(message) = server.receive_message(client_id, REPLICATION_CHANNEL_ID) { @@ -160,7 +160,7 @@ impl ServerPlugin { despawn_tracker: Res, replicon_tick: Res, removal_trackers: Query<(Entity, &RemovalTracker)>, - predictions: Res, + predictions: Res, ) -> Result<(), bincode::Error> { let mut acked_ticks = set.p2(); acked_ticks.register_tick(*replicon_tick, change_tick.this_run()); @@ -223,7 +223,7 @@ fn prepare_buffers<'a>( fn collect_mappings( buffers: &mut [ReplicationBuffer], acked_ticks: &ResMut, - predictions: &Res, + predictions: &Res, ) -> Result<(), bincode::Error> { for buffer in &mut *buffers { // Include all entity mappings since the last acknowledged tick. diff --git a/src/server/prediction_tracker.rs b/src/server/replicon_entity_map.rs similarity index 63% rename from src/server/prediction_tracker.rs rename to src/server/replicon_entity_map.rs index 1fbe8487..60d0e157 100644 --- a/src/server/prediction_tracker.rs +++ b/src/server/replicon_entity_map.rs @@ -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: @@ -25,12 +29,12 @@ use super::RepliconTick; /// send_shoot_command_to_server(client_predicted_entity); /// } /// // on server: -/// fn apply_inputs_system(mut predictions: ResMut, tick: Res) { +/// fn apply_inputs_system(mut entity_map: ResMut, tick: Res) { /// // ... /// 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, @@ -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` 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, Added)>, -/// mut commands: Commands, -/// ) { -/// for entity in q.iter() { -/// commands.entity(entity).remove::(); -/// } -/// } -/// ``` -/// -/// 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>, } pub(crate) type EntityMapping = (RepliconTick, ServerEntity, ClientEntity); @@ -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( @@ -100,11 +82,11 @@ 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> { let Some(v) = self.mappings.get(&client_id) else { return None; @@ -112,7 +94,7 @@ impl PredictionTracker { 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 @@ -121,18 +103,12 @@ impl PredictionTracker { .collect::>(), ) } - /// 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); } } diff --git a/tests/replication.rs b/tests/replication.rs index 7a4469a5..3c32d299 100644 --- a/tests/replication.rs +++ b/tests/replication.rs @@ -142,7 +142,7 @@ fn spawn_prediction_replication() { // and registers the client's predicted entity server_app .world - .resource_scope(|_world, mut pt: Mut| { + .resource_scope(|_world, mut pt: Mut| { 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,