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 17 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_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::EntityState, 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()
.read_entity_state(entity.entity());
UkoeHB marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 entity_state = client_info.visibility().entity_state();
if entity_state == EntityState::Hidden {
continue;
UkoeHB marked this conversation as resolved.
Show resolved Hide resolved
}

let new_entity = marker_added || entity_state == EntityState::JustVisible;
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 entity_state = client_info.visibility().entity_state();
if entity_state == EntityState::Hidden {
continue;
}

let new_entity = marker_added || entity_state == EntityState::JustVisible;
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 marked to be visible.
Blacklist,
/// All entities are visible by default and should be explicitly marked to be hidden.
Whitelist,
Shatur marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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
94 changes: 73 additions & 21 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,45 @@ pub(crate) struct ClientBuffers {
}

impl ClientsInfo {
pub(super) fn new(policy: VisibilityPolicy) -> Self {
Self {
info: Default::default(),
policy,
}
}

/// Returns reference to connected client info.
Copy link
Contributor Author

@Shatur Shatur Jan 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should rename it into ClientState and the parent struct into ClientsState.
But I think it's better to do as part of a separate PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe: ClientCache for the parent struct, then ClientState for the per-client struct. ClientsState is pretty awkward.

Shatur marked this conversation as resolved.
Show resolved Hide resolved
///
/// This operation is *O*(*n*).
UkoeHB marked this conversation as resolved.
Show resolved Hide resolved
pub fn get(&self, client_id: ClientId) -> Option<&ClientInfo> {
UkoeHB marked this conversation as resolved.
Show resolved Hide resolved
self.info.iter().find(|info| info.id == client_id)
}

/// Returns mutable reference to connected client info.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
///
/// This operation is *O*(*n*).
pub fn get_mut(&mut self, client_id: ClientId) -> Option<&mut ClientInfo> {
self.info.iter_mut().find(|info| info.id == client_id)
}

/// Returns an iterator over clients information.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) fn iter(&self) -> impl Iterator<Item = &ClientInfo> {
self.0.iter()
pub fn iter(&self) -> impl Iterator<Item = &ClientInfo> {
self.info.iter()
}

/// Returns a mutable iterator over clients information.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = &mut ClientInfo> {
self.0.iter_mut()
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ClientInfo> {
self.info.iter_mut()
}

/// Returns number of connected clients.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub(super) fn len(&self) -> usize {
self.0.len()
pub fn len(&self) -> usize {
self.info.len()
}

/// Returns `true` if no clients connected.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub fn is_empty(&self) -> bool {
self.info.is_empty()
}

/// Initializes a new [`ClientInfo`] for this client.
Expand All @@ -51,22 +83,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 +107,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 +140,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 +156,16 @@ impl ClientInfo {
self.id
}

/// Returns reference to client visibility settings.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
pub fn visibility(&self) -> &ClientVisibility {
&self.visibility
}

/// Returns mutable reference to client visibility settings.
Shatur marked this conversation as resolved.
Show resolved Hide resolved
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 +180,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 +271,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