Make clients subscribed to nearby regions and only send physics updates from those regions.

This commit is contained in:
Imbris 2019-10-06 13:35:47 -04:00 committed by Imbris
parent 24d1f6d970
commit b09bddda79
8 changed files with 595 additions and 291 deletions

3
Cargo.lock generated
View File

@ -3625,6 +3625,8 @@ dependencies = [
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hibitset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"portpicker 0.1.0 (git+https://github.com/wusyong/portpicker-rs?branch=fix_ipv6)",
@ -3637,6 +3639,7 @@ dependencies = [
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"specs 0.14.3 (registry+https://github.com/rust-lang/crates.io-index)",
"specs-idvs 0.1.0 (git+https://gitlab.com/veloren/specs-idvs.git)",
"uvth 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"vek 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)",
"veloren-common 0.4.0",

View File

@ -501,6 +501,11 @@ impl Client {
ServerMsg::EcsSync(sync_package) => {
self.state.ecs_mut().sync_with_package(sync_package)
}
ServerMsg::DeleteEntity(entity) => {
if let Some(entity) = self.state.ecs().entity_from_uid(entity) {
let _ = self.state.ecs_mut().delete_entity(entity);
}
}
ServerMsg::EntityPos { entity, pos } => {
if let Some(entity) = self.state.ecs().entity_from_uid(entity) {
self.state.write_component(entity, pos);

View File

@ -41,6 +41,7 @@ pub enum ServerMsg {
},
SetPlayerEntity(u64),
EcsSync(sphynx::SyncPackage<EcsCompPacket, EcsResPacket>),
DeleteEntity(u64),
EntityPos {
entity: u64,
pos: comp::Pos,

View File

@ -1,38 +1,64 @@
use crate::comp::{Pos, Vel};
use hashbrown::hash_map::DefaultHashBuilder;
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
use hibitset::BitSetLike;
use indexmap::IndexMap;
use specs::{BitSet, Entities, Join, ReadStorage};
use specs::{BitSet, Entities, Entity as EcsEntity, Join, ReadStorage};
use vek::*;
pub enum Event {
// Contains the key of the region the entity moved to
Left(u32, Option<Vec2<i32>>),
// Contains the key of the region the entity came from
Entered(u32, Option<Vec2<i32>>),
}
/// Region consisting of a bitset of entities within it
struct Region<S> {
pub struct Region {
// Use specs bitset for simplicity (and joinability)
bitset: BitSet,
// Indices of neighboring regions
neighbors: [Option<usize>; 8],
// Keep track of subscribers
subscribers: Vec<S>,
// TODO consider SmallVec for these
// Entites that left or entered this region
events: Vec<Event>,
}
impl<S> Region<S> {
fn with_entity(entity: u32) -> Self {
let mut bitset = BitSet::new();
bitset.add(entity);
impl Region {
fn new() -> Self {
Self {
bitset,
bitset: BitSet::new(),
neighbors: [None; 8],
subscribers: Vec::new(),
events: Vec::new(),
}
}
/// Checks if the region contains no entities and no events
fn removable(&self) -> bool {
self.bitset.is_empty() && self.events.is_empty()
}
fn add(&mut self, id: u32, from: Option<Vec2<i32>>) {
self.bitset.add(id);
self.events.push(Event::Entered(id, from));
}
fn remove(&mut self, id: u32, to: Option<Vec2<i32>>) {
self.bitset.remove(id);
self.events.push(Event::Left(id, to));
}
pub fn events(&self) -> &[Event] {
&self.events
}
pub fn entities(&self) -> &BitSet {
&self.bitset
}
}
/// How far can an entity roam outside its region before it is switched over to the neighboring one
/// In units of blocks (i.e. world pos)
/// Used to prevent rapid switching of entities between regions
const TETHER_LENGTH: u32 = 16;
/// Region Size in chunks
const REGION_SIZE: u16 = 16;
const REGION_LOG2: u8 = 4;
pub const TETHER_LENGTH: u32 = 16;
/// Region Size in blocks
pub const REGION_SIZE: u32 = 16 * 32;
/// Shift between region to world pos
/// TODO: don't use this :P
const REGION_LOG2: u8 = 9;
/// Offsets to iterate though neighbors
/// Counter-clockwise order
const NEIGHBOR_OFFSETS: [Vec2<i32>; 8] = [
@ -49,11 +75,11 @@ const NEIGHBOR_OFFSETS: [Vec2<i32>; 8] = [
// TODO generic region size (16x16 for now)
// TODO compare to sweep and prune approach
/// A region system that tracks where entities are
pub struct RegionMap<S> {
pub struct RegionMap {
// Tree?
// Sorted Vec? (binary search lookup)
// Sort into multiple vecs (say 32) using lower bits of morton code, then binary search via upper bits? <-- sounds very promising to me (might not be super good though?)
regions: IndexMap<Vec2<i32>, Region<S>, DefaultHashBuilder>,
regions: IndexMap<Vec2<i32>, Region, DefaultHashBuilder>,
// If an entity isn't here it needs to be added to a region
tracked_entities: BitSet,
// Re-useable vecs
@ -61,24 +87,24 @@ pub struct RegionMap<S> {
entities_to_move: Vec<(usize, u32, Vec3<i32>)>,
// (region, entity)
entities_to_remove: Vec<(usize, u32)>,
// Track the current tick, used to enable not checking everything every tick
tick: u64,
}
impl<S> RegionMap<S> {
impl RegionMap {
pub fn new() -> Self {
Self {
regions: IndexMap::default(),
tracked_entities: BitSet::new(),
entities_to_move: Vec::new(),
entities_to_remove: Vec::new(),
// rate is depedent on the rate the caller calls region_manager.tick()
tick: 0,
}
}
// TODO maintain within a system
pub fn maintain(
&mut self,
pos: ReadStorage<Pos>,
vel: ReadStorage<Vel>,
entities: Entities,
tick: u64,
) {
// TODO maintain within a system?
// TODO special case large entities
pub fn tick(&mut self, pos: ReadStorage<Pos>, vel: ReadStorage<Vel>, entities: Entities) {
self.tick += 1;
// Add any untracked entites
for (pos, id) in (&pos, &entities, !&self.tracked_entities)
.join()
@ -86,14 +112,22 @@ impl<S> RegionMap<S> {
.collect::<Vec<_>>()
{
// Add entity
self.add_entity(id, pos.0.map(|e| e as i32));
self.tracked_entities.add(id);
self.add_entity(id, pos.0.map(|e| e as i32), None);
}
self.entities_to_move.clear();
self.entities_to_remove.clear();
let mut regions_to_remove = Vec::new();
for i in 0..self.regions.len() {
for (maybe_pos, maybe_vel, entity) in (
// Clear events within each region
self.regions
.get_index_mut(i)
.map(|(_, v)| v)
.unwrap()
.events
.clear();
for (maybe_pos, _maybe_vel, id) in (
pos.maybe(),
vel.maybe(),
&self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset,
@ -118,14 +152,14 @@ impl<S> RegionMap<S> {
> TETHER_LENGTH
{
// Switch
self.entities_to_move.push((i, entity, pos));
self.entities_to_move.push((i, id, pos));
}
}
// Remove any non-existant entities (or just ones that lost their position component)
// TODO: distribute this between ticks
None => {
// TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs?
self.entities_to_remove.push((i, entity));
self.entities_to_remove.push((i, id));
}
}
}
@ -137,63 +171,65 @@ impl<S> RegionMap<S> {
.get_index(i)
.map(|(_, v)| v)
.unwrap()
.bitset
.is_empty()
.removable()
{
self.remove_index(i);
regions_to_remove.push(i);
}
}
for index in regions_to_remove {
self.remove_index(index);
}
// Mutate
// Note entity moving is outside the whole loop so that the same entity is not checked twice (this may be fine though...)
while let Some((i, entity, pos)) = self.entities_to_move.pop() {
self.regions
.get_index_mut(i)
.map(|(_, v)| v)
.unwrap()
.bitset
.remove(entity);
self.add_entity_untracked(entity, pos);
}
for (i, entity) in self.entities_to_remove.drain(..) {
self.regions
.get_index_mut(i)
.map(|(_, v)| v)
.unwrap()
.bitset
.remove(entity);
}
while let Some((i, id, pos)) = self.entities_to_move.pop() {
let (prev_key, region) = self.regions.get_index_mut(i).map(|(k, v)| (*k, v)).unwrap();
region.remove(id, Some(Self::pos_key(pos)));
// Maintain subscriptions ???
self.add_entity(id, pos, Some(prev_key));
}
for (i, id) in self.entities_to_remove.drain(..) {
self.regions
.get_index_mut(i)
.map(|(_, v)| v)
.unwrap()
.remove(id, None);
self.tracked_entities.remove(id);
}
}
fn add_entity(&mut self, id: u32, pos: Vec3<i32>) {
self.tracked_entities.add(id);
self.add_entity_untracked(id, pos);
pub fn add(&mut self, entity: EcsEntity, pos: Vec3<f32>) {
self.add_entity(entity.id(), pos.map(|e| e as i32), None);
}
fn add_entity_untracked(&mut self, id: u32, pos: Vec3<i32>) {
fn add_entity(&mut self, id: u32, pos: Vec3<i32>, from: Option<Vec2<i32>>) {
let key = Self::pos_key(pos);
if let Some(region) = self.regions.get_mut(&key) {
region.bitset.add(id);
region.add(id, from);
return;
}
self.insert(key, id);
let index = self.insert(key);
self.regions
.get_index_mut(index)
.map(|(_, v)| v)
.unwrap()
.add(id, None);
}
fn pos_key<P: Into<Vec2<i32>>>(pos: P) -> Vec2<i32> {
pos.into().map(|e| e >> REGION_LOG2)
}
fn key_pos(key: Vec2<i32>) -> Vec2<i32> {
pub fn key_pos(key: Vec2<i32>) -> Vec2<i32> {
key.map(|e| e << REGION_LOG2)
}
fn key_index(&self, key: Vec2<i32>) -> Option<usize> {
self.regions.get_full(&key).map(|(i, _, _)| i)
}
//fn key_index(&self, key: Vec2<i32>) -> Option<usize> {
// self.regions.get_full(&key).map(|(i, _, _)| i)
//}
fn index_key(&self, index: usize) -> Option<Vec2<i32>> {
self.regions.get_index(index).map(|(k, _)| k).copied()
}
/// Adds a new region
fn insert(&mut self, key: Vec2<i32>, entity: u32) {
let (index, old_region) = self.regions.insert_full(key, Region::with_entity(entity));
/// Returns the index of the region in the index map
fn insert(&mut self, key: Vec2<i32>) -> usize {
let (index, old_region) = self.regions.insert_full(key, Region::new());
if old_region.is_some() {
panic!("Inserted a region that already exists!!!(this should never need to occur");
}
@ -213,29 +249,33 @@ impl<S> RegionMap<S> {
.map(|(_, v)| v)
.unwrap()
.neighbors = neighbors;
index
}
/// Remove a region using its key
fn remove(&mut self, key: Vec2<i32>) {
if let Some(index) = self.key_index(key) {
self.remove_index(index);
}
}
//fn remove(&mut self, key: Vec2<i32>) {
// if let Some(index) = self.key_index(key) {
// self.remove_index(index);
// }
//}
/// Add a region using its key
fn remove_index(&mut self, index: usize) {
// Remap neighbor indices for neighbors of the region that will be moved from the end of the index map
let moved_neighbors = self
.regions
.get_index(index)
.map(|(_, v)| v)
.unwrap()
.neighbors;
for i in 0..8 {
if let Some(idx) = moved_neighbors[i] {
self.regions
.get_index_mut(idx)
.map(|(_, v)| v)
.unwrap()
.neighbors[(i + 4) % 8] = Some(index);
if index != self.regions.len() - 1 {
let moved_neighbors = self
.regions
.get_index(self.regions.len() - 1)
.map(|(_, v)| v)
.unwrap()
.neighbors;
for i in 0..8 {
if let Some(idx) = moved_neighbors[i] {
self.regions
.get_index_mut(idx)
.map(|(_, v)| v)
.unwrap()
.neighbors[(i + 4) % 8] = Some(index);
}
}
}
if let Some(region) = self
@ -258,75 +298,58 @@ impl<S> RegionMap<S> {
}
}
}
}
/*pub struct RegionManager<S> {
region_map: RegionMap<S>
// If an entity isn't here it needs to be added to a region
tracked_entities: BitSet,
}
impl<S> RegionManager {
// TODO maintain within a system?
pub fn maintain(&mut self, pos: ReadStorage<Pos>, vel: ReadStorage<Vel>, entities: Entities, tick: u64) {
let Self {
ref mut region_map,
ref mut tracked_entities,
} =
// Add any untracked entites
for (pos, e, _) in (&pos, &entities, !&self.tracked_entities).join() {
let id = e.id();
// Add entity
self.add_entity(id, pos.0.map(|e| e as i32));
}
// Iterate through regions
for i in 0..self.regions.len() {
for (maybe_pos, maybe_vel, entity) in
(pos.maybe(), vel.maybe(), &self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset).join()
{
match maybe_pos {
// Switch regions for entities which need switching
// TODO don't check every tick (use velocity) (and use id to stagger)
// Starting parameters at v = 0 check every 100 ticks
// tether_length^2 / vel^2 (with a max of every tick)
Some(pos) => {
let pos = pos.0.map(|e| e as i32);
let current_region = self.index_key(i).unwrap();
let key = Self::pos_key(pos);
// Consider switching
// Caculate distance outside border
if key != current_region
&& (Vec2::<i32>::from(pos) - Self::key_pos(current_region))
.map(|e| e.abs() as u32)
.reduce_max()
> TETHER_LENGTH
{
// Switch
self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity);
self.add_entity_untracked(entity, pos);
}
}
// Remove any non-existant entities (or just ones that lost their position component)
// TODO: distribute this between ticks
None => {
// TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs?
self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity);
}
}
}
// Remove region if it is empty
// TODO: distribute this betweeen ticks
if self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset.is_empty() {
self.remove_index(i);
}
}
// Maintain subscriptions ???
// Returns a region given a key
pub fn get(&self, key: Vec2<i32>) -> Option<&Region> {
self.regions.get(&key)
}
}*/
// Returns an iterator of (Position, Region)
pub fn iter(&self) -> impl Iterator<Item = (Vec2<i32>, &Region)> {
self.regions.iter().map(|(key, r)| (*key, r))
}
}
// Note vd is in blocks in this case
pub fn region_in_vd(key: Vec2<i32>, pos: Vec3<f32>, vd: f32) -> bool {
let vd_extended = vd + TETHER_LENGTH as f32 * 2.0f32.sqrt();
let min_region_pos = RegionMap::key_pos(key).map(|e| e as f32);
// Should be diff to closest point on the square (which can be in the middle of an edge)
let diff = (min_region_pos - Vec2::from(pos)).map(|e| {
if e < 0.0 {
(e + REGION_SIZE as f32).min(0.0)
} else {
e
}
});
diff.magnitude_squared() < vd_extended.powi(2)
}
// Note vd is in blocks in this case
pub fn regions_in_vd(pos: Vec3<f32>, vd: f32) -> HashSet<Vec2<i32>> {
let mut set = HashSet::new();
let pos_xy = Vec2::<f32>::from(pos);
let vd_extended = vd + TETHER_LENGTH as f32 * 2.0f32.sqrt();
let max = RegionMap::pos_key(pos_xy.map(|e| (e + vd_extended) as i32));
let min = RegionMap::pos_key(pos_xy.map(|e| (e - vd_extended) as i32));
for x in min.x..=max.x {
for y in min.y..=max.y {
let key = Vec2::new(x, y);
if region_in_vd(key, pos, vd) {
set.insert(key);
}
}
}
set
}
// Iterator designed for use in collision systems
// Iterates through all regions yielding them along with half of their neighbors
// ..................
/*fn interleave_i32_with_zeros(mut x: i32) -> i64 {
x = (x ^ (x << 16)) & 0x0000ffff0000ffff;

View File

@ -5,6 +5,7 @@ use crate::{
comp,
event::{EventBus, LocalEvent, ServerEvent},
msg::{EcsCompPacket, EcsResPacket},
region::RegionMap,
sys,
terrain::{Block, TerrainChunk, TerrainGrid},
vol::WriteVol,
@ -172,6 +173,7 @@ impl State {
ecs.add_resource(TerrainChanges::default());
ecs.add_resource(EventBus::<ServerEvent>::default());
ecs.add_resource(EventBus::<LocalEvent>::default());
ecs.add_resource(RegionMap::new());
}
/// Register a component with the state's ECS.
@ -387,6 +389,13 @@ impl State {
self.ecs.maintain();
// Run RegionMap tick to update enitity region occupancy
self.ecs.write_resource::<RegionMap>().tick(
self.ecs.read_storage::<comp::Pos>(),
self.ecs.read_storage::<comp::Vel>(),
self.ecs.entities(),
);
// Apply terrain changes
let mut terrain = self.ecs.write_resource::<TerrainGrid>();
self.ecs

View File

@ -8,6 +8,8 @@ edition = "2018"
common = { package = "veloren-common", path = "../common" }
world = { package = "veloren-world", path = "../world" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
log = "0.4.8"
specs = "0.14.2"
vek = "0.9.9"
@ -25,3 +27,6 @@ prometheus = "0.7"
prometheus-static-metric = "0.2"
rouille = "3.0.0"
portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" }
indexmap = "1.2.0"
# TODO: remove when upgrading to specs 0.15
hibitset = "0.5.3"

View File

@ -2,8 +2,12 @@ use common::{
msg::{ClientMsg, ClientState, RequestStateError, ServerMsg},
net::PostBox,
};
use hashbrown::HashMap;
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
use indexmap::IndexMap;
use specs::Entity as EcsEntity;
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
use vek::*;
pub struct Client {
pub client_state: ClientState,
@ -31,13 +35,13 @@ impl Client {
}
pub struct Clients {
clients: HashMap<EcsEntity, Client>,
clients: IndexMap<EcsEntity, Client, DefaultHashBuilder>,
}
impl Clients {
pub fn empty() -> Self {
Self {
clients: HashMap::new(),
clients: IndexMap::default(),
}
}
@ -54,17 +58,40 @@ impl Clients {
}
pub fn get_mut<'a>(&'a mut self, entity: &EcsEntity) -> Option<&'a mut Client> {
self.clients.get_mut(entity)
self.clients.get_mut(entit:y)
}
pub fn remove<'a>(&'a mut self, entity: &EcsEntity) -> Option<Client> {
self.clients.remove(entity)
}
pub fn get_client_index_ingame<'a>(&'a mut self, entity: &EcsEntity) -> Option<usize> {
self.clients.get_full(entity).and_then(|(i, _, c)| {
if c.client_state == ClientState::Spectator
|| c.client_state == ClientState::Character
|| c.client_state == ClientState::Dead
{
Some(i)
} else {
None
}
})
}
//pub fn get_index_mut<'a>(&'a mut self, index: u32) -> Option<&'a mut Client> {
// self.clients.get_index_mut(index)
//}
pub fn remove_if<F: FnMut(EcsEntity, &mut Client) -> bool>(&mut self, mut f: F) {
self.clients.retain(|entity, client| !f(*entity, client));
}
pub fn notify_index(&mut self, index: usize, msg: ServerMsg) {
if let Some((_, client)) = self.clients.get_index_mut(index) {
client.notify(msg);
}
}
pub fn notify(&mut self, entity: EcsEntity, msg: ServerMsg) {
if let Some(client) = self.clients.get_mut(&entity) {
client.notify(msg);
@ -138,3 +165,18 @@ impl Clients {
}
}
}
// Distance from fuzzy_chunk before snapping to current chunk
pub const CHUNK_FUZZ: u32 = 2;
// Distance out of the range of a region before removing it from subscriptions
pub const REGION_FUZZ: u32 = 16;
#[derive(Clone, Debug)]
pub struct RegionSubscription {
pub fuzzy_chunk: Vec2<i32>,
pub regions: HashSet<Vec2<i32>>,
}
impl Component for RegionSubscription {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}

View File

@ -14,7 +14,7 @@ pub use crate::{error::Error, input::Input, settings::ServerSettings};
use crate::{
auth_provider::AuthProvider,
client::{Client, Clients},
client::{Client, Clients, RegionSubscription},
cmd::CHAT_COMMANDS,
};
use common::{
@ -86,6 +86,10 @@ pub struct Server {
server_info: ServerInfo,
metrics: ServerMetrics,
// Tick count used for throttling network updates
// Note this doesn't account for dt (so update rate changes with tick rate)
tick: u64,
// TODO: anything but this
accounts: AuthProvider,
}
@ -102,6 +106,7 @@ impl Server {
state
.ecs_mut()
.add_resource(EventBus::<ServerEvent>::default());
state.ecs_mut().register::<RegionSubscription>();
// Set starting time for the server.
state.ecs_mut().write_resource::<TimeOfDay>().0 = settings.start_time;
@ -128,6 +133,7 @@ impl Server {
},
metrics: ServerMetrics::new(settings.metrics_address)
.expect("Failed to initialize server metrics submodule."),
tick: 0,
accounts: AuthProvider::new(),
server_settings: settings.clone(),
};
@ -547,6 +553,7 @@ impl Server {
/// Execute a single server tick, handle input and update the game state by the given duration.
pub fn tick(&mut self, _input: Input, dt: Duration) -> Result<Vec<Event>, Error> {
self.tick += 1;
// This tick function is the centre of the Veloren universe. Most server-side things are
// managed from here, and as such it's important that it stays organised. Please consult
// the core developers before making significant changes to this code. Here is the
@ -792,6 +799,7 @@ impl Server {
}
// Remove NPCs that are outside the view distances of all players
// This is done by removing NPCs in unloaded chunks
let to_delete = {
let terrain = self.state.terrain();
(
@ -1116,6 +1124,7 @@ impl Server {
}),
&server_settings,
);
Self::initialize_region_subscription(state, client, entity);
}
ClientState::Character => {
client.error_state(RequestStateError::Already)
@ -1329,161 +1338,368 @@ impl Server {
// Save player metadata (for example the username).
state.write_component(entity, player);
// Sync physics of all entities
for (&uid, &pos, vel, ori, character_state) in (
&state.ecs().read_storage::<Uid>(),
&state.ecs().read_storage::<comp::Pos>(), // We assume all these entities have a position
state.ecs().read_storage::<comp::Vel>().maybe(),
state.ecs().read_storage::<comp::Ori>().maybe(),
state.ecs().read_storage::<comp::CharacterState>().maybe(),
)
.join()
{
client.notify(ServerMsg::EntityPos {
entity: uid.into(),
pos,
});
if let Some(vel) = vel.copied() {
client.notify(ServerMsg::EntityVel {
entity: uid.into(),
vel,
});
}
if let Some(ori) = ori.copied() {
client.notify(ServerMsg::EntityOri {
entity: uid.into(),
ori,
});
}
if let Some(character_state) = character_state.copied() {
client.notify(ServerMsg::EntityCharacterState {
entity: uid.into(),
character_state,
});
}
}
// Tell the client its request was successful.
client.allow_state(ClientState::Registered);
}
/// Initialize region subscription, entity should be the client's entity
fn initialize_region_subscription(
state: &mut State,
client: &mut Client,
entity: specs::Entity,
) {
let mut subscription = None;
if let (Some(client_pos), Some(client_vd)) = (
state.ecs().read_storage::<comp::Pos>().get(entity),
state
.ecs()
.read_storage::<comp::Player>()
.get(entity)
.map(|pl| pl.view_distance)
.and_then(|v| v),
) {
use common::region::RegionMap;
let fuzzy_chunk = (Vec2::<f32>::from(client_pos.0))
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
let regions = common::region::regions_in_vd(
client_pos.0,
(client_vd as f32 * chunk_size) as f32
+ (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
);
for (_, region) in state
.ecs()
.read_resource::<RegionMap>()
.iter()
.filter(|(key, _)| regions.contains(key))
{
// Sync physics of all entities in this region
for (&uid, &pos, vel, ori, character_state, _) in (
&state.ecs().read_storage::<Uid>(),
&state.ecs().read_storage::<comp::Pos>(), // We assume all these entities have a position
state.ecs().read_storage::<comp::Vel>().maybe(),
state.ecs().read_storage::<comp::Ori>().maybe(),
state.ecs().read_storage::<comp::CharacterState>().maybe(),
region.entities(),
)
.join()
{
client.notify(ServerMsg::EntityPos {
entity: uid.into(),
pos,
});
if let Some(vel) = vel.copied() {
client.notify(ServerMsg::EntityVel {
entity: uid.into(),
vel,
});
}
if let Some(ori) = ori.copied() {
client.notify(ServerMsg::EntityOri {
entity: uid.into(),
ori,
});
}
if let Some(character_state) = character_state.copied() {
client.notify(ServerMsg::EntityCharacterState {
entity: uid.into(),
character_state,
});
}
}
}
subscription = Some(RegionSubscription {
fuzzy_chunk,
regions,
});
}
if let Some(subscription) = subscription {
state.write_component(entity, subscription);
}
}
/// Sync client states with the most up to date information.
fn sync_clients(&mut self) {
// Sync 'logical' state using Sphynx.
self.clients
.notify_registered(ServerMsg::EcsSync(self.state.ecs_mut().next_sync_package()));
use common::region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap};
//use hibitset::BitSetLike;
let ecs = self.state.ecs_mut();
let clients = &mut self.clients;
// Sync physics
for (entity, &uid, &pos, force_update) in (
// Sync 'logical' state using Sphynx.
clients.notify_registered(ServerMsg::EcsSync(ecs.next_sync_package()));
// To update subscriptions
// 1. Iterate through clients
// 2. Calculate current chunk position
// 3. If chunk is the same return, otherwise continue (use fuzzyiness)
// 4. Iterate through subscribed regions
// 5. Check if region is still in range (use fuzzyiness)
// 6. If not in range
// - remove from hashset
// - inform client of which entities to remove
// 7. Determine list of regions that are in range and iterate through it
// - check if in hashset (hash calc) if not add it
let mut regions_to_remove = Vec::new();
for (entity, subscription, pos, vd) in (
&ecs.entities(),
&ecs.read_storage::<Uid>(),
&mut ecs.write_storage::<RegionSubscription>(),
&ecs.read_storage::<comp::Pos>(),
ecs.read_storage::<comp::ForceUpdate>().maybe(),
&ecs.read_storage::<comp::Player>(),
)
.join()
.filter_map(|(e, s, pos, player)| player.view_distance.map(|v| (e, s, pos, v)))
{
let clients = &mut self.clients;
let in_vd = |entity| {
if let (Some(client_pos), Some(client_vd)) = (
ecs.read_storage::<comp::Pos>().get(entity),
ecs.read_storage::<comp::Player>()
.get(entity)
.map(|pl| pl.view_distance)
.and_then(|v| v),
) {
{
// Check if the entity is in the client's range
Vec2::from(pos.0 - client_pos.0)
.map2(TerrainChunkSize::RECT_SIZE, |d: f32, sz| {
(d.abs() as u32 / sz).checked_sub(2).unwrap_or(0)
})
.magnitude_squared()
< client_vd.pow(2)
let chunk = (Vec2::<f32>::from(pos.0))
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
if chunk != subscription.fuzzy_chunk
&& (subscription
.fuzzy_chunk
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
(e as f32 + 0.5) * sz as f32
})
- Vec2::from(pos.0))
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
e.abs() > (sz / 2 + client::CHUNK_FUZZ) as f32
})
.reduce_or()
{
let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
for key in &subscription.regions {
if !region_in_vd(
*key,
pos.0,
(vd as f32 * chunk_size)
+ (client::CHUNK_FUZZ as f32 + client::REGION_FUZZ as f32 + chunk_size)
* 2.0f32.sqrt(),
) {
regions_to_remove.push(*key);
}
}
let mut client = clients.get_mut(&entity);
for key in regions_to_remove.drain(..) {
subscription.regions.remove(&key);
// Inform the client to delete these entities
if let (Some(ref mut client), Some(region)) =
(&mut client, ecs.read_resource::<RegionMap>().get(key))
{
// Process entity left events since they won't be processed below because this region is no longer subscribed to
for event in region.events() {
match event {
RegionEvent::Entered(_, _) => {} // These don't need to be processed because this region is being thrown out anyway
RegionEvent::Left(id, maybe_key) => {
// Lookup UID for entity
if let Some(&uid) =
ecs.read_storage::<Uid>().get(ecs.entities().entity(*id))
{
if !maybe_key
.as_ref()
.map(|key| subscription.regions.contains(key))
.unwrap_or(false)
{
client.notify(ServerMsg::DeleteEntity(uid.into()));
}
}
}
}
}
for (&uid, _) in (&ecs.read_storage::<Uid>(), region.entities()).join() {
client.notify(ServerMsg::DeleteEntity(uid.into()))
}
}
}
for key in regions_in_vd(
pos.0,
(vd as f32 * chunk_size)
+ (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
) {
if subscription.regions.insert(key) {
// TODO: send the client initial infromation for all the entities in this region
}
}
}
}
// To send entity updates
// 1. Iterate through regions
// 2. Iterate through region subscribers (ie clients)
// - Collect a list of entity ids for clients who are subscribed to this region (hash calc to check each)
// 3. Iterate through events from that region
// - For each entity left event, iterate through the client list and check if they are subscribed to the destination (hash calc per subscribed client per entity left event)
// - Do something with entity entered events when sphynx is removed??
// 4. Iterate through entities in that region
// 5. Inform clients of the component changes for that entity
// - Throttle update rate base on distance to each client
// Sync physics
// via iterating through regions
for (key, region) in ecs.read_resource::<RegionMap>().iter() {
let subscriptions = ecs.read_storage::<RegionSubscription>();
let subscribers = (
&ecs.entities(),
&subscriptions,
&ecs.read_storage::<comp::Pos>(),
)
.join()
.filter_map(|(entity, subscription, pos)| {
if subscription.regions.contains(&key) {
clients
.get_client_index_ingame(&entity)
.map(|index| (index, &subscription.regions, entity, *pos))
} else {
None
}
})
.collect::<Vec<_>>();
for event in region.events() {
match event {
RegionEvent::Entered(_, _) => {} // TODO use this
RegionEvent::Left(id, maybe_key) => {
// Lookup UID for entity
if let Some(&uid) =
ecs.read_storage::<Uid>().get(ecs.entities().entity(*id))
{
for (client_index, regions, _, _) in &subscribers {
if !maybe_key
.as_ref()
.map(|key| regions.contains(key))
.unwrap_or(false)
{
clients.notify_index(
*client_index,
ServerMsg::DeleteEntity(uid.into()),
);
}
}
}
}
}
}
let tick = self.tick;
let send_msg = |msg: ServerMsg,
entity: EcsEntity,
pos: comp::Pos,
force_update,
clients: &mut Clients| {
for (index, _, client_entity, client_pos) in &subscribers {
match force_update {
None if client_entity == &entity => {}
_ => {
let distance_sq = client_pos.0.distance_squared(pos.0);
// Throttle update rate based on distance to player
let update = if distance_sq < 100.0f32.powi(2) {
true // Closer than 100.0 blocks
} else if distance_sq < 150.0f32.powi(2) {
tick + entity.id() as u64 % 2 == 0
} else if distance_sq < 200.0f32.powi(2) {
tick + entity.id() as u64 % 4 == 0
} else if distance_sq < 250.0f32.powi(2) {
tick + entity.id() as u64 % 8 == 0
} else if distance_sq < 300.0f32.powi(2) {
tick + entity.id() as u64 % 8 == 0
} else {
tick + entity.id() as u64 % 16 == 0
};
if update {
clients.notify_index(*index, msg.clone());
}
}
}
} else {
false
}
};
let mut last_pos = ecs.write_storage::<comp::Last<comp::Pos>>();
let mut last_vel = ecs.write_storage::<comp::Last<comp::Vel>>();
let mut last_ori = ecs.write_storage::<comp::Last<comp::Ori>>();
let mut last_character_state = ecs.write_storage::<comp::Last<comp::CharacterState>>();
if let Some(client_pos) = ecs.read_storage::<comp::Pos>().get(entity) {
if last_pos
.get(entity)
.map(|&l| l.0 != *client_pos)
.unwrap_or(true)
{
let _ = last_pos.insert(entity, comp::Last(*client_pos));
let msg = ServerMsg::EntityPos {
entity: uid.into(),
pos: *client_pos,
};
match force_update {
Some(_) => clients.notify_ingame_if(msg, in_vd),
None => clients.notify_ingame_if_except(entity, msg, in_vd),
}
}
}
if let Some(client_vel) = ecs.read_storage::<comp::Vel>().get(entity) {
if last_vel
.get(entity)
.map(|&l| l.0 != *client_vel)
.unwrap_or(true)
{
let _ = last_vel.insert(entity, comp::Last(*client_vel));
let msg = ServerMsg::EntityVel {
entity: uid.into(),
vel: *client_vel,
};
match force_update {
Some(_) => clients.notify_ingame_if(msg, in_vd),
None => clients.notify_ingame_if_except(entity, msg, in_vd),
}
}
}
if let Some(client_ori) = ecs.read_storage::<comp::Ori>().get(entity) {
if last_ori
.get(entity)
.map(|&l| l.0 != *client_ori)
.unwrap_or(true)
{
let _ = last_ori.insert(entity, comp::Last(*client_ori));
let msg = ServerMsg::EntityOri {
entity: uid.into(),
ori: *client_ori,
};
match force_update {
Some(_) => clients.notify_ingame_if(msg, in_vd),
None => clients.notify_ingame_if_except(entity, msg, in_vd),
}
}
}
if let Some(client_character_state) =
ecs.read_storage::<comp::CharacterState>().get(entity)
for (_, entity, &uid, &pos, maybe_vel, maybe_ori, character_state, force_update) in (
region.entities(),
&ecs.entities(),
&ecs.read_storage::<Uid>(),
&ecs.read_storage::<comp::Pos>(),
ecs.read_storage::<comp::Vel>().maybe(),
ecs.read_storage::<comp::Ori>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
ecs.read_storage::<comp::ForceUpdate>().maybe(),
)
.join()
{
if last_character_state
.get(entity)
.map(|&l| !client_character_state.is_same_state(&l.0))
.unwrap_or(true)
{
let _ =
last_character_state.insert(entity, comp::Last(*client_character_state));
let msg = ServerMsg::EntityCharacterState {
entity: uid.into(),
character_state: *client_character_state,
};
match force_update {
Some(_) => clients.notify_ingame_if(msg, in_vd),
None => clients.notify_ingame_if_except(entity, msg, in_vd),
let mut last_pos = ecs.write_storage::<comp::Last<comp::Pos>>();
let mut last_vel = ecs.write_storage::<comp::Last<comp::Vel>>();
let mut last_ori = ecs.write_storage::<comp::Last<comp::Ori>>();
let mut last_character_state =
ecs.write_storage::<comp::Last<comp::CharacterState>>();
if last_pos.get(entity).map(|&l| l.0 != pos).unwrap_or(true) {
let _ = last_pos.insert(entity, comp::Last(pos));
send_msg(
ServerMsg::EntityPos {
entity: uid.into(),
pos,
},
entity,
pos,
force_update,
clients,
);
}
if let Some(&vel) = maybe_vel {
if last_vel.get(entity).map(|&l| l.0 != vel).unwrap_or(true) {
let _ = last_vel.insert(entity, comp::Last(vel));
send_msg(
ServerMsg::EntityVel {
entity: uid.into(),
vel,
},
entity,
pos,
force_update,
clients,
);
}
}
if let Some(&ori) = maybe_ori {
if last_ori.get(entity).map(|&l| l.0 != ori).unwrap_or(true) {
let _ = last_ori.insert(entity, comp::Last(ori));
send_msg(
ServerMsg::EntityOri {
entity: uid.into(),
ori,
},
entity,
pos,
force_update,
clients,
);
}
}
if let Some(&character_state) = character_state {
if last_character_state
.get(entity)
.map(|&l| !character_state.is_same_state(&l.0))
.unwrap_or(true)
{
let _ = last_character_state.insert(entity, comp::Last(character_state));
send_msg(
ServerMsg::EntityCharacterState {
entity: uid.into(),
character_state,
},
entity,
pos,
force_update,
clients,
);
}
}
}