Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clients visibility #174

Merged
merged 48 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3b28b5b
Initial prototype
Shatur Jan 13, 2024
5767abe
Fix tests compilation
Shatur Jan 13, 2024
c1eea50
Do not panic on visibility change with `ClientVisibility::All`
Shatur Jan 14, 2024
0b655e1
Use proposed naming
Shatur Jan 14, 2024
05cc66a
Use more efficient iteration approach
Shatur Jan 14, 2024
f3b5692
Fix docs and add to prelude
Shatur Jan 14, 2024
56c7880
Remove `Box` from iterator
Shatur Jan 14, 2024
02a820e
Export simpler visibility getter to users
Shatur Jan 14, 2024
222f63e
Combine loops
Shatur Jan 14, 2024
53cb84c
Turn `ClientVisibility` into a struct with the inner enum
Shatur Jan 14, 2024
ebd9341
Refactor API to avoid extra lookup
Shatur Jan 14, 2024
a24fb9c
Update docs
Shatur Jan 14, 2024
2e9e791
Fix visibility check logic
Shatur Jan 14, 2024
e34fa97
Fix unhiding logic for blacklist
Shatur Jan 14, 2024
ecc9f1e
Put `VisibilityPolicy` to prelude
Shatur Jan 14, 2024
f10d0a1
Add tests
Shatur Jan 14, 2024
f9e91b7
Improve docs
Shatur Jan 14, 2024
a936335
Fix typo in the test
Shatur Jan 14, 2024
c8996ab
Refactor tests
Shatur Jan 14, 2024
64839be
Apply suggestions from code review [skip ci]
Shatur Jan 14, 2024
ac64583
Apply more docs [skip ci]
Shatur Jan 14, 2024
156d235
Rename `set_visible` into `set_visibility`
Shatur Jan 14, 2024
b63c8e1
Apply `VisibilityFilter` docs suggestions
Shatur Jan 14, 2024
d7813f1
Apply suggested change to `EntityState` enum
Shatur Jan 15, 2024
fc17651
Apply suggested naming about entity_state
Shatur Jan 15, 2024
7515312
Fix remove_despawned logic
Shatur Jan 15, 2024
e6bac4b
Add panicking versions for getting `ClientInfo`
Shatur Jan 15, 2024
b5071ab
Use enums instead of bools
Shatur Jan 15, 2024
2df564b
Write more comments about the logic
Shatur Jan 15, 2024
1f00634
Apply suggestions from code review [skip ci]
Shatur Jan 15, 2024
643f2fc
Remove size_hint
Shatur Jan 15, 2024
9641dea
Use drain instead of iteration
Shatur Jan 15, 2024
832bbca
Apply docs suggestions
Shatur Jan 15, 2024
761254f
Fix warning about doc comment
Shatur Jan 15, 2024
8497b87
Fix bug about re-adding in the list
Shatur Jan 15, 2024
ed3881b
Refactor visibility logic as suggested
Shatur Jan 15, 2024
f440312
Clear removed and added in update too to avoid confusion
Shatur Jan 15, 2024
bda6ac7
Add unit tests for many combinations and simplify integration tests
Shatur Jan 15, 2024
a991a20
Fix removal and insertion on the same tick
Shatur Jan 15, 2024
7696b53
Add more comments
Shatur Jan 15, 2024
6b70f9c
Apply clippy suggestion
Shatur Jan 15, 2024
2f9eefb
Add despawn tests
Shatur Jan 15, 2024
042097d
Apply suggestions from code review
Shatur Jan 15, 2024
e35ed31
Fix duplicate insertion for blacklist
Shatur Jan 15, 2024
c0b2d8a
Add whitelist despawn test
Shatur Jan 15, 2024
b3ed333
Simplify despawn tests
Shatur Jan 15, 2024
6021346
Add two more asserts
Shatur Jan 15, 2024
6c87d2b
Use non-mut method
Shatur Jan 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,9 @@ pub mod prelude {
NetworkChannels, ReplicationChannel, RepliconCorePlugin,
},
server::{
clients_info::ClientsInfo, has_authority, ClientEntityMap, ClientMapping, ServerPlugin,
ServerSet, TickPolicy, SERVER_ID,
clients_info::{client_visibility::ClientVisibility, ClientInfo, ClientsInfo},
has_authority, ClientEntityMap, ClientMapping, ServerPlugin, ServerSet, TickPolicy,
VisibilityPolicy, SERVER_ID,
},
ReplicationPlugins,
};
Expand Down
5 changes: 1 addition & 4 deletions src/network_event/server_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,7 @@ pub fn send_with<T>(
}
SendMode::Direct(client_id) => {
if client_id != SERVER_ID {
if let Some(client_info) = clients_info
.iter()
.find(|client_info| client_info.id() == client_id)
{
if let Some(client_info) = clients_info.get_client(client_id) {
let message = serialize_with(client_info, None, &serialize_fn)?;
server.send_message(client_info.id(), channel, message.bytes);
}
Expand Down
48 changes: 40 additions & 8 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use bevy_renet::{
use crate::replicon_core::{
replication_rules::ReplicationRules, replicon_tick::RepliconTick, ReplicationChannel,
};
use clients_info::{ClientBuffers, ClientInfo, ClientsInfo};
use clients_info::{client_visibility::Visibility, ClientBuffers, ClientInfo, ClientsInfo};
use despawn_buffer::{DespawnBuffer, DespawnBufferPlugin};
use removal_buffer::{RemovalBuffer, RemovalBufferPlugin};
use replicated_archetypes_info::ReplicatedArchetypesInfo;
Expand All @@ -39,6 +39,9 @@ pub struct ServerPlugin {
/// Tick configuration.
pub tick_policy: TickPolicy,

/// Visibility configuration.
pub visibility_policy: VisibilityPolicy,

/// The time after which updates will be considered lost if an acknowledgment is not received for them.
///
/// In practice updates will live at least `update_timeout`, and at most `2*update_timeout`.
Expand All @@ -49,6 +52,7 @@ impl Default for ServerPlugin {
fn default() -> Self {
Self {
tick_policy: TickPolicy::MaxTickRate(30),
visibility_policy: Default::default(),
update_timeout: Duration::from_secs(10),
}
}
Expand All @@ -62,9 +66,9 @@ impl Plugin for ServerPlugin {
RenetServerPlugin,
NetcodeServerPlugin,
))
.init_resource::<ClientsInfo>()
.init_resource::<ClientBuffers>()
.init_resource::<ClientEntityMap>()
.insert_resource(ClientsInfo::new(self.visibility_policy))
.configure_sets(PreUpdate, ServerSet::Receive.after(RenetReceive))
.configure_sets(PostUpdate, ServerSet::Send.before(RenetSend))
.add_systems(
Expand Down Expand Up @@ -291,9 +295,12 @@ fn collect_changes(
};

for entity in archetype.entities() {
for (init_message, update_message) in messages.iter_mut() {
for (init_message, update_message, client_info) in messages.iter_mut_with_info() {
init_message.start_entity_data(entity.entity());
update_message.start_entity_data(entity.entity());
client_info
.visibility_mut()
.cache_visibility(entity.entity());
}

// SAFETY: all replicated archetypes have marker component with table storage.
Expand Down Expand Up @@ -326,7 +333,12 @@ fn collect_changes(

let mut shared_bytes = None;
for (init_message, update_message, client_info) in messages.iter_mut_with_info() {
let new_entity = marker_added || client_info.just_connected;
let visibility = client_info.visibility().cached_visibility();
if visibility == Visibility::Hidden {
continue;
}

let new_entity = marker_added || visibility == Visibility::Gained;
if new_entity || ticks.is_added(change_tick.last_run(), change_tick.this_run())
{
init_message.write_component(
Expand All @@ -352,7 +364,12 @@ fn collect_changes(
}

for (init_message, update_message, client_info) in messages.iter_mut_with_info() {
let new_entity = marker_added || client_info.just_connected;
let visibility = client_info.visibility().cached_visibility();
if visibility == Visibility::Hidden {
continue;
}

let new_entity = marker_added || visibility == Visibility::Gained;
if new_entity || init_message.entity_data_size() != 0 {
// If there is any insertion or we must initialize, include all updates into init message
// and bump the last acknowledged tick to keep entity updates atomic.
Expand All @@ -367,8 +384,7 @@ fn collect_changes(
}
}

for (init_message, _, client_info) in messages.iter_mut_with_info() {
client_info.just_connected = false;
for (init_message, _) in messages.iter_mut() {
init_message.end_array()?;
}

Expand Down Expand Up @@ -422,7 +438,11 @@ fn collect_despawns(
}
}

for (message, _) in messages.iter_mut() {
for (message, _, client_info) in messages.iter_mut_with_info() {
for entity in client_info.remove_lost_visibility() {
message.write_entity(&mut None, entity)?;
}

message.end_array()?;
}

Expand Down Expand Up @@ -496,6 +516,18 @@ pub enum TickPolicy {
Manual,
}

/// Controls how visibility will be managed via [`ClientVisibility`](clients_info::client_visibility::ClientVisibility).
#[derive(Default, Debug, Clone, Copy)]
pub enum VisibilityPolicy {
/// All entities are visible by default and visibility can't be changed.
#[default]
All,
/// All entities are hidden by default and should be explicitly registered to be visible.
Blacklist,
/// All entities are visible by default and should be explicitly registered to be hidden.
Whitelist,
}

/**
A resource that exists on the server for mapping server entities to
entities that clients have already spawned. The mappings are sent to clients as part of replication
Expand Down
128 changes: 104 additions & 24 deletions src/server/clients_info.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod client_visibility;

use std::mem;

use bevy::{
Expand All @@ -7,11 +9,15 @@ use bevy::{
};
use bevy_renet::renet::ClientId;

use crate::replicon_core::replicon_tick::RepliconTick;
use crate::{replicon_core::replicon_tick::RepliconTick, server::VisibilityPolicy};
use client_visibility::ClientVisibility;

/// Stores meta-information about connected clients.
#[derive(Default, Resource)]
pub struct ClientsInfo(Vec<ClientInfo>);
#[derive(Resource, Default)]
pub struct ClientsInfo {
info: Vec<ClientInfo>,
policy: VisibilityPolicy,
}

/// Reusable buffers for [`ClientsInfo`] and [`ClientInfo`].
#[derive(Default, Resource)]
Expand All @@ -28,19 +34,73 @@ pub(crate) struct ClientBuffers {
}

impl ClientsInfo {
/// Returns an iterator over clients information.
pub(crate) fn iter(&self) -> impl Iterator<Item = &ClientInfo> {
self.0.iter()
pub(super) fn new(policy: VisibilityPolicy) -> Self {
Self {
info: Default::default(),
policy,
}
}

/// Returns a reference to a connected client's info.
///
/// This operation is *O*(*n*).
UkoeHB marked this conversation as resolved.
Show resolved Hide resolved
/// See also [`Self::get_client`] for the fallible version.
///
/// # Panics
///
/// Panics if passed client ID is not connected.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub fn client(&self, client_id: ClientId) -> &ClientInfo {
self.get_client(client_id)
.unwrap_or_else(|| panic!("{client_id:?} should be connected"))
}

/// Returns a mutable iterator over clients information.
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = &mut ClientInfo> {
self.0.iter_mut()
/// Returns a mutable reference to a connected client's info.
///
/// This operation is *O*(*n*).
/// See also [`Self::get_client_mut`] for the fallible version.
///
/// # Panics
///
/// Panics if passed client ID is not connected.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub fn client_mut(&mut self, client_id: ClientId) -> &mut ClientInfo {
self.get_client_mut(client_id)
.unwrap_or_else(|| panic!("{client_id:?} should be connected"))
}

/// Returns number of connected clients.
pub(super) fn len(&self) -> usize {
self.0.len()
/// Returns a reference to a connected client's info.
///
/// This operation is *O*(*n*).
/// See also [`Self::client`] for the panicking version.
pub fn get_client(&self, client_id: ClientId) -> Option<&ClientInfo> {
self.info.iter().find(|info| info.id == client_id)
}

/// Returns a mutable reference to a connected client's info.
///
/// This operation is *O*(*n*).
/// See also [`Self::client`] for the panicking version.
pub fn get_client_mut(&mut self, client_id: ClientId) -> Option<&mut ClientInfo> {
self.info.iter_mut().find(|info| info.id == client_id)
}

/// Returns an iterator over client information.
pub fn iter(&self) -> impl Iterator<Item = &ClientInfo> {
self.info.iter()
}

/// Returns a mutable iterator over client information.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ClientInfo> {
self.info.iter_mut()
}

/// Returns the number of connected clients.
pub fn len(&self) -> usize {
self.info.len()
}

/// Returns `true` if no clients are connected.
pub fn is_empty(&self) -> bool {
self.info.is_empty()
}

/// Initializes a new [`ClientInfo`] for this client.
Expand All @@ -51,22 +111,22 @@ impl ClientsInfo {
client_info.reset(client_id);
client_info
} else {
ClientInfo::new(client_id)
ClientInfo::new(client_id, self.policy)
};

self.0.push(client_info);
self.info.push(client_info);
}

/// Removes info for the client.
///
/// Keeps allocated memory in the buffers for reuse.
pub(super) fn remove(&mut self, client_buffers: &mut ClientBuffers, client_id: ClientId) {
let index = self
.0
.info
.iter()
.position(|info| info.id == client_id)
.expect("clients info should contain all connected clients");
let mut client_info = self.0.remove(index);
let mut client_info = self.info.remove(index);
client_buffers.entities.extend(client_info.drain_entities());
client_buffers.info.push(client_info);
}
Expand All @@ -75,23 +135,23 @@ impl ClientsInfo {
///
/// Keeps allocated memory in the buffers for reuse.
pub(super) fn clear(&mut self, client_buffers: &mut ClientBuffers) {
for mut client_info in self.0.drain(..) {
for mut client_info in self.info.drain(..) {
client_buffers.entities.extend(client_info.drain_entities());
client_buffers.info.push(client_info);
}
}
}

pub(crate) struct ClientInfo {
pub struct ClientInfo {
/// Client's ID.
id: ClientId,

/// Indicates whether the client connected in this tick.
pub(super) just_connected: bool,

/// Lowest tick for use in change detection for each entity.
ticks: EntityHashMap<Entity, Tick>,

/// Entity visibility settings.
visibility: ClientVisibility,

/// The last tick in which a replicated entity was spawned, despawned, or gained/lost a component from the perspective
/// of the client.
///
Expand All @@ -108,11 +168,11 @@ pub(crate) struct ClientInfo {
}

impl ClientInfo {
fn new(id: ClientId) -> Self {
fn new(id: ClientId, policy: VisibilityPolicy) -> Self {
Self {
id,
just_connected: true,
ticks: Default::default(),
visibility: ClientVisibility::new(policy),
change_tick: Default::default(),
updates: Default::default(),
next_update_index: Default::default(),
Expand All @@ -124,6 +184,16 @@ impl ClientInfo {
self.id
}

/// Returns a reference to the client's visibility settings.
pub fn visibility(&self) -> &ClientVisibility {
&self.visibility
}

/// Returns a mutable reference to the client's visibility settings.
pub fn visibility_mut(&mut self) -> &mut ClientVisibility {
&mut self.visibility
}

/// Clears all entities for unacknowledged updates, returning them as an iterator.
///
/// Keeps the allocated memory for reuse.
Expand All @@ -138,7 +208,7 @@ impl ClientInfo {
/// Keeps the allocated memory for reuse.
fn reset(&mut self, id: ClientId) {
self.id = id;
self.just_connected = true;
self.visibility.clear();
self.ticks.clear();
self.updates.clear();
self.next_update_index = 0;
Expand Down Expand Up @@ -229,10 +299,20 @@ impl ClientInfo {
/// Removes a despawned entity tracked by this client.
pub fn remove_despawned(&mut self, entity: Entity) {
self.ticks.remove(&entity);
self.visibility.remove_despawned(entity);
// We don't clean up `self.updates` for efficiency reasons.
// `Self::acknowledge()` will properly ignore despawned entities.
}

/// Iterates over all entities for which visibility was lost during this tick and removes them.
///
/// Removal happens lazily during the iteration.
pub(super) fn remove_lost_visibility(&mut self) -> impl Iterator<Item = Entity> + '_ {
self.visibility.iter_lost_visibility().inspect(|entity| {
self.ticks.remove(entity);
})
}
UkoeHB marked this conversation as resolved.
Show resolved Hide resolved

/// Removes all updates older then `min_timestamp`.
///
/// Keeps allocated memory in the buffers for reuse.
Expand Down
Loading