Add initial region system implementation

This commit is contained in:
Imbris 2019-10-03 20:41:42 -04:00 committed by Imbris
parent 0f55533430
commit 24d1f6d970
4 changed files with 354 additions and 0 deletions

8
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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;

342
common/src/region.rs Normal file
View File

@ -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<S> {
// Use specs bitset for simplicity (and joinability)
bitset: BitSet,
// Indices of neighboring regions
neighbors: [Option<usize>; 8],
// Keep track of subscribers
subscribers: Vec<S>,
}
impl<S> Region<S> {
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<i32>; 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<S> {
// 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>,
// 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<i32>)>,
// (region, entity)
entities_to_remove: Vec<(usize, u32)>,
}
impl<S> RegionMap<S> {
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<Pos>,
vel: ReadStorage<Vel>,
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::<Vec<_>>()
{
// 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::<i32>::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<i32>) {
self.tracked_entities.add(id);
self.add_entity_untracked(id, pos);
}
fn add_entity_untracked(&mut self, id: u32, pos: Vec3<i32>) {
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<P: Into<Vec2<i32>>>(pos: P) -> Vec2<i32> {
pos.into().map(|e| e >> REGION_LOG2)
}
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 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));
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<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 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<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 ???
}
}*/
// 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<i32>) -> i64 {
interleave_i32_with_zeros(pos.x) | (interleave_i32_with_zeros(pos.y) << 1)
}*/