diff --git a/Cargo.lock b/Cargo.lock index 7fd91c26e2..c84f74e7c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1577,6 +1577,11 @@ dependencies = [ "tiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "indexmap" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "inflate" version = "0.3.4" @@ -3592,7 +3597,9 @@ dependencies = [ "dot_vox 4.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "find_folder 0.3.0 (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)", "image 0.22.2 (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)", "lz4-compress 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4144,6 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "545f000e8aa4e569e93f49c446987133452e0091c2494ac3efd3606aa3d309f2" "checksum image 0.22.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ee0665404aa0f2ad154021777b785878b0e5b1c1da030455abc3d9ed257c2c67" +"checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" "checksum inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" "checksum inotify 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8" diff --git a/common/Cargo.toml b/common/Cargo.toml index f24e52d3d5..d1825f29fd 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -28,6 +28,9 @@ find_folder = "0.3.0" parking_lot = "0.9.0" crossbeam = "0.7.2" notify = "5.0.0-pre.1" +indexmap = "1.2.0" +# TODO: remove when upgrading to specs 0.15 +hibitset = "0.5.3" [dev-dependencies] criterion = "0.3" diff --git a/common/src/lib.rs b/common/src/lib.rs index 34848ad1c8..06a775c88d 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -19,6 +19,7 @@ pub mod figure; pub mod msg; pub mod npc; pub mod ray; +pub mod region; pub mod state; pub mod sys; pub mod terrain; diff --git a/common/src/region.rs b/common/src/region.rs new file mode 100644 index 0000000000..0f29f5d1e8 --- /dev/null +++ b/common/src/region.rs @@ -0,0 +1,342 @@ +use crate::comp::{Pos, Vel}; +use hashbrown::hash_map::DefaultHashBuilder; +use hibitset::BitSetLike; +use indexmap::IndexMap; +use specs::{BitSet, Entities, Join, ReadStorage}; +use vek::*; + +/// Region consisting of a bitset of entities within it +struct Region { + // Use specs bitset for simplicity (and joinability) + bitset: BitSet, + // Indices of neighboring regions + neighbors: [Option; 8], + // Keep track of subscribers + subscribers: Vec, +} +impl Region { + fn with_entity(entity: u32) -> Self { + let mut bitset = BitSet::new(); + bitset.add(entity); + Self { + bitset, + neighbors: [None; 8], + subscribers: Vec::new(), + } + } +} + +/// 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; +/// Offsets to iterate though neighbors +/// Counter-clockwise order +const NEIGHBOR_OFFSETS: [Vec2; 8] = [ + Vec2::new(0, 1), + Vec2::new(-1, 1), + Vec2::new(-1, 0), + Vec2::new(-1, -1), + Vec2::new(0, -1), + Vec2::new(1, -1), + Vec2::new(1, 0), + Vec2::new(1, 1), +]; + +// 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 { + // 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, Region, DefaultHashBuilder>, + // If an entity isn't here it needs to be added to a region + tracked_entities: BitSet, + // Re-useable vecs + // (src, entity, pos) + entities_to_move: Vec<(usize, u32, Vec3)>, + // (region, entity) + entities_to_remove: Vec<(usize, u32)>, +} +impl RegionMap { + pub fn new() -> Self { + Self { + regions: IndexMap::default(), + tracked_entities: BitSet::new(), + entities_to_move: Vec::new(), + entities_to_remove: Vec::new(), + } + } + // TODO maintain within a system + pub fn maintain( + &mut self, + pos: ReadStorage, + vel: ReadStorage, + entities: Entities, + tick: u64, + ) { + // Add any untracked entites + for (pos, id) in (&pos, &entities, !&self.tracked_entities) + .join() + .map(|(pos, e, _)| (pos, e.id())) + .collect::>() + { + // Add entity + self.add_entity(id, pos.0.map(|e| e as i32)); + } + + self.entities_to_move.clear(); + self.entities_to_remove.clear(); + + 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::::from(pos) - Self::key_pos(current_region)) + .map(|e| e.abs() as u32) + .reduce_max() + > TETHER_LENGTH + { + // Switch + self.entities_to_move.push((i, 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.entities_to_remove.push((i, 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); + } + } + + // 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); + } + + // Maintain subscriptions ??? + } + fn add_entity(&mut self, id: u32, pos: Vec3) { + self.tracked_entities.add(id); + self.add_entity_untracked(id, pos); + } + fn add_entity_untracked(&mut self, id: u32, pos: Vec3) { + let key = Self::pos_key(pos); + if let Some(region) = self.regions.get_mut(&key) { + region.bitset.add(id); + return; + } + + self.insert(key, id); + } + fn pos_key>>(pos: P) -> Vec2 { + pos.into().map(|e| e >> REGION_LOG2) + } + fn key_pos(key: Vec2) -> Vec2 { + key.map(|e| e << REGION_LOG2) + } + fn key_index(&self, key: Vec2) -> Option { + self.regions.get_full(&key).map(|(i, _, _)| i) + } + fn index_key(&self, index: usize) -> Option> { + self.regions.get_index(index).map(|(k, _)| k).copied() + } + /// Adds a new region + fn insert(&mut self, key: Vec2, entity: u32) { + let (index, old_region) = self.regions.insert_full(key, Region::with_entity(entity)); + if old_region.is_some() { + panic!("Inserted a region that already exists!!!(this should never need to occur"); + } + // Add neighbors and add to neighbors + let mut neighbors = [None; 8]; + for i in 0..8 { + if let Some((idx, _, region)) = self.regions.get_full_mut(&(key + NEIGHBOR_OFFSETS[i])) + { + // Add neighbor to the new region + neighbors[i] = Some(idx); + // Add new region to neighbor + region.neighbors[(i + 4) % 8] = Some(index); + } + } + self.regions + .get_index_mut(index) + .map(|(_, v)| v) + .unwrap() + .neighbors = neighbors; + } + /// Remove a region using its key + fn remove(&mut self, key: Vec2) { + 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 let Some(region) = self + .regions + .swap_remove_index(index) + .map(|(_, region)| region) + { + if !region.bitset.is_empty() { + panic!("Removed region containing entities"); + } + // Remove from neighbors + for i in 0..8 { + if let Some(idx) = region.neighbors[i] { + self.regions + .get_index_mut(idx) + .map(|(_, v)| v) + .unwrap() + .neighbors[(i + 4) % 8] = None; + } + } + } + } +} + +/*pub struct RegionManager { + region_map: RegionMap + // If an entity isn't here it needs to be added to a region + tracked_entities: BitSet, +} +impl RegionManager { + // TODO maintain within a system? + pub fn maintain(&mut self, pos: ReadStorage, vel: ReadStorage, 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::::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 ??? + + } +}*/ +// 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; + x = (x ^ (x << 8)) & 0x00ff00ff00ff00ff; + x = (x ^ (x << 4)) & 0x0f0f0f0f0f0f0f0f; + x = (x ^ (x << 2)) & 0x3333333333333333; + x = (x ^ (x << 1)) & 0x5555555555555555; + x +} + +fn morton_code(pos: Vec2) -> i64 { + interleave_i32_with_zeros(pos.x) | (interleave_i32_with_zeros(pos.y) << 1) +}*/