mirror of
https://github.com/eliasstepanik/big_space_with_trim.git
synced 2026-01-11 09:58:28 +00:00
Partition Bounds (#39)
Compute partition bounds during updates, to avoid needing to iterate over all cells in a partition every time this information is needed.
This commit is contained in:
parent
0b01883132
commit
2f8f9101e5
@ -7,7 +7,16 @@ use bevy::{
|
|||||||
use bevy_ecs::entity::EntityHasher;
|
use bevy_ecs::entity::EntityHasher;
|
||||||
use bevy_math::DVec3;
|
use bevy_math::DVec3;
|
||||||
use big_space::prelude::*;
|
use big_space::prelude::*;
|
||||||
use noise::{NoiseFn, Perlin};
|
use noise::{NoiseFn, Simplex};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use turborand::prelude::*;
|
||||||
|
|
||||||
|
// Try bumping this up to really stress test. I'm able to push a million entities with an M3 Max.
|
||||||
|
const HALF_WIDTH: f32 = 50.0;
|
||||||
|
const CELL_WIDTH: f32 = 10.0;
|
||||||
|
// How fast the entities should move, causing them to move into neighboring cells.
|
||||||
|
const MOVEMENT_SPEED: f32 = 5.0;
|
||||||
|
const PERCENT_STATIC: f32 = 0.99;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -26,19 +35,11 @@ fn main() {
|
|||||||
draw_partitions.after(GridHashMapSystem::UpdatePartition),
|
draw_partitions.after(GridHashMapSystem::UpdatePartition),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.add_systems(Update, cursor_grab)
|
.add_systems(Update, (cursor_grab, spawn_spheres))
|
||||||
.init_resource::<MaterialPresets>()
|
.init_resource::<MaterialPresets>()
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try bumping this up to really stress test. I'm able to push a million entities with an M3 Max.
|
|
||||||
const N_ENTITIES: usize = 100_000;
|
|
||||||
const HALF_WIDTH: f32 = 40.0;
|
|
||||||
const CELL_WIDTH: f32 = 10.0;
|
|
||||||
// How fast the entities should move, causing them to move into neighboring cells.
|
|
||||||
const MOVEMENT_SPEED: f32 = 5.0;
|
|
||||||
const PERCENT_STATIC: f32 = 0.9;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct Player;
|
struct Player;
|
||||||
|
|
||||||
@ -50,25 +51,35 @@ struct MaterialPresets {
|
|||||||
default: Handle<StandardMaterial>,
|
default: Handle<StandardMaterial>,
|
||||||
highlight: Handle<StandardMaterial>,
|
highlight: Handle<StandardMaterial>,
|
||||||
flood: Handle<StandardMaterial>,
|
flood: Handle<StandardMaterial>,
|
||||||
|
sphere: Handle<Mesh>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromWorld for MaterialPresets {
|
impl FromWorld for MaterialPresets {
|
||||||
fn from_world(world: &mut World) -> Self {
|
fn from_world(world: &mut World) -> Self {
|
||||||
let mut materials = world.resource_mut::<Assets<StandardMaterial>>();
|
let mut materials = world.resource_mut::<Assets<StandardMaterial>>();
|
||||||
|
|
||||||
let d: StandardMaterial = StandardMaterial {
|
let default = materials.add(StandardMaterial {
|
||||||
base_color: Color::from(Srgba::new(0.5, 0.5, 0.5, 1.0)),
|
base_color: Color::from(Srgba::new(0.5, 0.5, 0.5, 1.0)),
|
||||||
perceptual_roughness: 0.2,
|
perceptual_roughness: 0.2,
|
||||||
metallic: 0.0,
|
metallic: 0.0,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
});
|
||||||
let h: StandardMaterial = Color::from(Srgba::new(2.0, 0.0, 8.0, 1.0)).into();
|
let highlight = materials.add(Color::from(Srgba::new(2.0, 0.0, 8.0, 1.0)));
|
||||||
let f: StandardMaterial = Color::from(Srgba::new(1.1, 0.1, 1.0, 1.0)).into();
|
let flood = materials.add(Color::from(Srgba::new(1.1, 0.1, 1.0, 1.0)));
|
||||||
|
|
||||||
|
let mut meshes = world.resource_mut::<Assets<Mesh>>();
|
||||||
|
let sphere = meshes.add(
|
||||||
|
Sphere::new(HALF_WIDTH / (1_000_000_f32).powf(0.33) * 0.5)
|
||||||
|
.mesh()
|
||||||
|
.ico(0)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
default: materials.add(d),
|
default,
|
||||||
highlight: materials.add(h),
|
highlight,
|
||||||
flood: materials.add(f),
|
flood,
|
||||||
|
sphere,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,7 +90,7 @@ fn draw_partitions(
|
|||||||
grids: Query<(&GlobalTransform, &Grid<i32>)>,
|
grids: Query<(&GlobalTransform, &Grid<i32>)>,
|
||||||
camera: Query<&GridHash<i32>, With<Camera>>,
|
camera: Query<&GridHash<i32>, With<Camera>>,
|
||||||
) {
|
) {
|
||||||
for (id, p) in partitions.iter() {
|
for (id, p) in partitions.iter().take(10_000) {
|
||||||
let Ok((transform, grid)) = grids.get(p.grid()) else {
|
let Ok((transform, grid)) = grids.get(p.grid()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -92,35 +103,19 @@ fn draw_partitions(
|
|||||||
|
|
||||||
p.iter()
|
p.iter()
|
||||||
.filter(|hash| *hash != camera.single())
|
.filter(|hash| *hash != camera.single())
|
||||||
|
.take(1_000)
|
||||||
.for_each(|h| {
|
.for_each(|h| {
|
||||||
let center = [h.cell().x, h.cell().y, h.cell().z];
|
let center = [h.cell().x, h.cell().y, h.cell().z];
|
||||||
let local_trans = Transform::from_translation(IVec3::from(center).as_vec3() * l)
|
let local_trans = Transform::from_translation(IVec3::from(center).as_vec3() * l)
|
||||||
.with_scale(Vec3::splat(l));
|
.with_scale(Vec3::splat(l));
|
||||||
gizmos.cuboid(
|
gizmos.cuboid(
|
||||||
transform.mul_transform(local_trans),
|
transform.mul_transform(local_trans),
|
||||||
Hsla::new(hue, 1.0, 0.5, 0.2),
|
Hsla::new(hue, 1.0, 0.5, 0.05),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let Some(min) = p
|
let min = IVec3::from([p.min().x, p.min().y, p.min().z]).as_vec3() * l;
|
||||||
.iter()
|
let max = IVec3::from([p.max().x, p.max().y, p.max().z]).as_vec3() * l;
|
||||||
.filter(|hash| *hash != camera.single())
|
|
||||||
.map(|h| [h.cell().x, h.cell().y, h.cell().z])
|
|
||||||
.reduce(|[ax, ay, az], [ix, iy, iz]| [ax.min(ix), ay.min(iy), az.min(iz)])
|
|
||||||
.map(|v| IVec3::from(v).as_vec3() * l)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(max) = p
|
|
||||||
.iter()
|
|
||||||
.filter(|hash| *hash != camera.single())
|
|
||||||
.map(|h| [h.cell().x, h.cell().y, h.cell().z])
|
|
||||||
.reduce(|[ax, ay, az], [ix, iy, iz]| [ax.max(ix), ay.max(iy), az.max(iz)])
|
|
||||||
.map(|v| IVec3::from(v).as_vec3() * l)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let size = max - min;
|
let size = max - min;
|
||||||
let center = min + (size) * 0.5;
|
let center = min + (size) * 0.5;
|
||||||
@ -152,6 +147,7 @@ fn move_player(
|
|||||||
hash_stats: Res<big_space::timing::SmoothedStat<big_space::timing::GridHashStats>>,
|
hash_stats: Res<big_space::timing::SmoothedStat<big_space::timing::GridHashStats>>,
|
||||||
prop_stats: Res<big_space::timing::SmoothedStat<big_space::timing::PropagationStats>>,
|
prop_stats: Res<big_space::timing::SmoothedStat<big_space::timing::PropagationStats>>,
|
||||||
) {
|
) {
|
||||||
|
let n_entities = non_player.iter().len();
|
||||||
for neighbor in neighbors.iter() {
|
for neighbor in neighbors.iter() {
|
||||||
if let Ok(mut material) = materials.get_mut(*neighbor) {
|
if let Ok(mut material) = materials.get_mut(*neighbor) {
|
||||||
**material = material_presets.default.clone_weak();
|
**material = material_presets.default.clone_weak();
|
||||||
@ -163,10 +159,12 @@ fn move_player(
|
|||||||
if scale.abs() > 0.0 {
|
if scale.abs() > 0.0 {
|
||||||
// Avoid change detection
|
// Avoid change detection
|
||||||
for (i, (mut transform, _, _)) in non_player.iter_mut().enumerate() {
|
for (i, (mut transform, _, _)) in non_player.iter_mut().enumerate() {
|
||||||
if i > (PERCENT_STATIC * N_ENTITIES as f32) as usize {
|
if i < ((1.0 - PERCENT_STATIC) * n_entities as f32) as usize {
|
||||||
transform.translation.x += t.sin() * scale;
|
transform.translation.x += t.sin() * scale;
|
||||||
transform.translation.y += t.cos() * scale;
|
transform.translation.y += t.cos() * scale;
|
||||||
transform.translation.z += (t * 2.3).sin() * scale;
|
transform.translation.z += (t * 2.3).sin() * scale;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,6 +211,10 @@ fn move_player(
|
|||||||
let mut text = text.single_mut();
|
let mut text = text.single_mut();
|
||||||
text.0 = format!(
|
text.0 = format!(
|
||||||
"\
|
"\
|
||||||
|
Controls:
|
||||||
|
WASD to move, QE to roll
|
||||||
|
F to spawn 1,000, G to double
|
||||||
|
|
||||||
Population: {: >8} Entities
|
Population: {: >8} Entities
|
||||||
|
|
||||||
Transform Propagation
|
Transform Propagation
|
||||||
@ -229,7 +231,15 @@ Update Maps: {: >16.1?}
|
|||||||
Update Partitions: {: >10.1?}
|
Update Partitions: {: >10.1?}
|
||||||
|
|
||||||
Total: {: >22.1?}",
|
Total: {: >22.1?}",
|
||||||
N_ENTITIES,
|
n_entities
|
||||||
|
.to_string()
|
||||||
|
.as_bytes()
|
||||||
|
.rchunks(3)
|
||||||
|
.rev()
|
||||||
|
.map(std::str::from_utf8)
|
||||||
|
.collect::<Result<Vec<&str>, _>>()
|
||||||
|
.unwrap()
|
||||||
|
.join(","),
|
||||||
//
|
//
|
||||||
prop_stats.avg().grid_recentering(),
|
prop_stats.avg().grid_recentering(),
|
||||||
prop_stats.avg().low_precision_root_tagging(),
|
prop_stats.avg().low_precision_root_tagging(),
|
||||||
@ -246,36 +256,7 @@ Total: {: >22.1?}",
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn(
|
fn spawn(mut commands: Commands) {
|
||||||
mut commands: Commands,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
material_presets: Res<MaterialPresets>,
|
|
||||||
) {
|
|
||||||
use turborand::prelude::*;
|
|
||||||
let rng = Rng::with_seed(342525);
|
|
||||||
let noise = Perlin::new(345612);
|
|
||||||
|
|
||||||
let rng = || loop {
|
|
||||||
let noise_scale = 5.0;
|
|
||||||
let threshold = 0.70;
|
|
||||||
let rng_val = || rng.f64_normalized() * noise_scale;
|
|
||||||
let coord = [rng_val(), rng_val(), rng_val()];
|
|
||||||
if noise.get(coord) > threshold {
|
|
||||||
return DVec3::from_array(coord).as_vec3() * HALF_WIDTH * CELL_WIDTH
|
|
||||||
/ noise_scale as f32;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let values: Vec<_> = std::iter::repeat_with(rng).take(N_ENTITIES).collect();
|
|
||||||
|
|
||||||
let sphere_mesh_lq = meshes.add(
|
|
||||||
Sphere::new(HALF_WIDTH / (N_ENTITIES as f32).powf(0.33) * 0.2)
|
|
||||||
.mesh()
|
|
||||||
.ico(0)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
commands.spawn_big_space::<i32>(Grid::new(CELL_WIDTH, 0.0), |root| {
|
commands.spawn_big_space::<i32>(Grid::new(CELL_WIDTH, 0.0), |root| {
|
||||||
root.spawn_spatial((
|
root.spawn_spatial((
|
||||||
FloatingOrigin,
|
FloatingOrigin,
|
||||||
@ -298,32 +279,84 @@ fn spawn(
|
|||||||
b.spawn(DirectionalLight::default());
|
b.spawn(DirectionalLight::default());
|
||||||
});
|
});
|
||||||
|
|
||||||
for (i, value) in values.iter().enumerate() {
|
root.spawn_spatial(Player);
|
||||||
let mut sphere_builder = root.spawn((BigSpatialBundle::<i32> {
|
});
|
||||||
transform: Transform::from_xyz(value.x, value.y, value.z),
|
}
|
||||||
..default()
|
|
||||||
},));
|
fn spawn_spheres(
|
||||||
if i == 0 {
|
mut commands: Commands,
|
||||||
sphere_builder.insert((
|
input: Res<ButtonInput<KeyCode>>,
|
||||||
Player,
|
material_presets: Res<MaterialPresets>,
|
||||||
Mesh3d(meshes.add(Sphere::new(1.0))),
|
mut grid: Query<(Entity, &Grid<i32>, &mut Children)>,
|
||||||
MeshMaterial3d(materials.add(Color::from(Srgba::new(20.0, 20.0, 0.0, 1.0)))),
|
non_players: Query<(), With<NonPlayer>>,
|
||||||
Transform::from_scale(Vec3::splat(2.0)),
|
) {
|
||||||
));
|
let n_entities = non_players.iter().len().max(1);
|
||||||
} else {
|
let n_spawn = if input.pressed(KeyCode::KeyG) {
|
||||||
sphere_builder.insert((
|
n_entities
|
||||||
|
} else if input.pressed(KeyCode::KeyF) {
|
||||||
|
1_000
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (entity, _grid, mut children) = grid.single_mut();
|
||||||
|
let mut dyn_parent = bevy_reflect::DynamicTupleStruct::default();
|
||||||
|
dyn_parent.insert(entity);
|
||||||
|
let dyn_parent = dyn_parent.as_partial_reflect();
|
||||||
|
|
||||||
|
let new_children = sample_noise(n_spawn, &Simplex::new(345612), &Rng::new())
|
||||||
|
.map(|value| {
|
||||||
|
let hash = GridHash::<i32>::__new_manual(entity, &GridCell::default());
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
Transform::from_xyz(value.x, value.y, value.z),
|
||||||
|
GlobalTransform::default(),
|
||||||
|
GridCell::<i32>::default(),
|
||||||
|
FastGridHash::from(hash),
|
||||||
|
hash,
|
||||||
NonPlayer,
|
NonPlayer,
|
||||||
Mesh3d(sphere_mesh_lq.clone()),
|
Parent::from_reflect(dyn_parent).unwrap(),
|
||||||
|
Mesh3d(material_presets.sphere.clone_weak()),
|
||||||
MeshMaterial3d(material_presets.default.clone_weak()),
|
MeshMaterial3d(material_presets.default.clone_weak()),
|
||||||
bevy_render::view::VisibilityRange {
|
bevy_render::view::VisibilityRange {
|
||||||
start_margin: 1.0..5.0,
|
start_margin: 1.0..5.0,
|
||||||
end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8,
|
end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8,
|
||||||
use_aabb: false,
|
use_aabb: false,
|
||||||
},
|
},
|
||||||
));
|
bevy_render::view::NoFrustumCulling,
|
||||||
|
))
|
||||||
|
.id()
|
||||||
|
})
|
||||||
|
.chain(children.iter().copied())
|
||||||
|
.collect::<SmallVec<[Entity; 8]>>();
|
||||||
|
|
||||||
|
let mut dyn_children = bevy_reflect::DynamicTupleStruct::default();
|
||||||
|
dyn_children.insert(new_children);
|
||||||
|
let dyn_children = dyn_children.as_partial_reflect();
|
||||||
|
|
||||||
|
*children = Children::from_reflect(dyn_children).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sample_noise<'a, T: NoiseFn<f64, 3>>(
|
||||||
|
n_entities: usize,
|
||||||
|
noise: &'a T,
|
||||||
|
rng: &'a Rng,
|
||||||
|
) -> impl Iterator<Item = Vec3> + use<'a, T> {
|
||||||
|
std::iter::repeat_with(
|
||||||
|
|| loop {
|
||||||
|
let noise_scale = 0.05 * HALF_WIDTH as f64;
|
||||||
|
let threshold = 0.50;
|
||||||
|
let rng_val = || rng.f64_normalized() * noise_scale;
|
||||||
|
let coord = [rng_val(), rng_val(), rng_val()];
|
||||||
|
if noise.get(coord) > threshold {
|
||||||
|
return DVec3::from_array(coord).as_vec3() * HALF_WIDTH * CELL_WIDTH
|
||||||
|
/ noise_scale as f32;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
// Vec3::ONE
|
||||||
|
)
|
||||||
|
.take(n_entities)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
|
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
|||||||
@ -21,6 +21,10 @@ pub trait BigSpaceCommands {
|
|||||||
&mut self,
|
&mut self,
|
||||||
child_builder: impl FnOnce(&mut GridCommands<P>),
|
child_builder: impl FnOnce(&mut GridCommands<P>),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Access the [`GridCommands`] of an entity by passing in the [`Entity`] and [`Grid`]. Note
|
||||||
|
/// that the value of `grid` will be inserted in this entity when the command is applied.
|
||||||
|
fn grid<P: GridPrecision>(&mut self, entity: Entity, grid: Grid<P>) -> GridCommands<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BigSpaceCommands for Commands<'_, '_> {
|
impl BigSpaceCommands for Commands<'_, '_> {
|
||||||
@ -45,6 +49,15 @@ impl BigSpaceCommands for Commands<'_, '_> {
|
|||||||
) {
|
) {
|
||||||
self.spawn_big_space(Grid::default(), child_builder);
|
self.spawn_big_space(Grid::default(), child_builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grid<P: GridPrecision>(&mut self, entity: Entity, grid: Grid<P>) -> GridCommands<P> {
|
||||||
|
GridCommands {
|
||||||
|
entity,
|
||||||
|
commands: self.reborrow(),
|
||||||
|
grid,
|
||||||
|
children: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build [`big_space`](crate) hierarchies more easily, with access to grids.
|
/// Build [`big_space`](crate) hierarchies more easily, with access to grids.
|
||||||
@ -68,6 +81,7 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn an entity in this grid.
|
/// Spawn an entity in this grid.
|
||||||
|
#[inline]
|
||||||
pub fn spawn(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
|
pub fn spawn(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
|
||||||
let entity = self.commands.spawn(bundle).id();
|
let entity = self.commands.spawn(bundle).id();
|
||||||
self.children.push(entity);
|
self.children.push(entity);
|
||||||
@ -80,9 +94,9 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
|
|
||||||
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and insert the provided
|
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and insert the provided
|
||||||
/// bundle.
|
/// bundle.
|
||||||
|
#[inline]
|
||||||
pub fn spawn_spatial(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
|
pub fn spawn_spatial(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
|
||||||
let entity = self
|
let entity = self
|
||||||
.commands
|
|
||||||
.spawn((
|
.spawn((
|
||||||
#[cfg(feature = "bevy_render")]
|
#[cfg(feature = "bevy_render")]
|
||||||
bevy_render::view::Visibility::default(),
|
bevy_render::view::Visibility::default(),
|
||||||
@ -92,8 +106,6 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
.insert(bundle)
|
.insert(bundle)
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
self.children.push(entity);
|
|
||||||
|
|
||||||
SpatialEntityCommands {
|
SpatialEntityCommands {
|
||||||
entity,
|
entity,
|
||||||
commands: self.commands.reborrow(),
|
commands: self.commands.reborrow(),
|
||||||
@ -101,7 +113,8 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`Entity``] id of the entity.
|
/// Returns the [`Entity`] id of the entity.
|
||||||
|
#[inline]
|
||||||
pub fn id(&self) -> Entity {
|
pub fn id(&self) -> Entity {
|
||||||
self.entity
|
self.entity
|
||||||
}
|
}
|
||||||
@ -109,6 +122,7 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and apply entity commands
|
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and apply entity commands
|
||||||
/// to it via the closure. This allows you to insert bundles on this new spatial entities, and
|
/// to it via the closure. This allows you to insert bundles on this new spatial entities, and
|
||||||
/// add more children to it.
|
/// add more children to it.
|
||||||
|
#[inline]
|
||||||
pub fn with_spatial(
|
pub fn with_spatial(
|
||||||
&mut self,
|
&mut self,
|
||||||
spatial: impl FnOnce(&mut SpatialEntityCommands<P>),
|
spatial: impl FnOnce(&mut SpatialEntityCommands<P>),
|
||||||
@ -120,6 +134,7 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and apply entity commands
|
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and apply entity commands
|
||||||
/// to it via the closure. This allows you to insert bundles on this new spatial entities, and
|
/// to it via the closure. This allows you to insert bundles on this new spatial entities, and
|
||||||
/// add more children to it.
|
/// add more children to it.
|
||||||
|
#[inline]
|
||||||
pub fn with_grid(
|
pub fn with_grid(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_grid: Grid<P>,
|
new_grid: Grid<P>,
|
||||||
@ -130,16 +145,15 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Same as [`Self::with_grid`], but using the default [`Grid`] value.
|
/// Same as [`Self::with_grid`], but using the default [`Grid`] value.
|
||||||
|
#[inline]
|
||||||
pub fn with_grid_default(&mut self, builder: impl FnOnce(&mut GridCommands<P>)) -> &mut Self {
|
pub fn with_grid_default(&mut self, builder: impl FnOnce(&mut GridCommands<P>)) -> &mut Self {
|
||||||
self.with_grid(Grid::default(), builder)
|
self.with_grid(Grid::default(), builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a grid as a child of the current grid.
|
/// Spawn a grid as a child of the current grid.
|
||||||
|
#[inline]
|
||||||
pub fn spawn_grid(&mut self, new_grid: Grid<P>, bundle: impl Bundle) -> GridCommands<P> {
|
pub fn spawn_grid(&mut self, new_grid: Grid<P>, bundle: impl Bundle) -> GridCommands<P> {
|
||||||
let mut entity_commands = self.commands.entity(self.entity);
|
let entity = self
|
||||||
let mut commands = entity_commands.commands();
|
|
||||||
|
|
||||||
let entity = commands
|
|
||||||
.spawn((
|
.spawn((
|
||||||
#[cfg(feature = "bevy_render")]
|
#[cfg(feature = "bevy_render")]
|
||||||
bevy_render::view::Visibility::default(),
|
bevy_render::view::Visibility::default(),
|
||||||
@ -150,8 +164,6 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
.insert(bundle)
|
.insert(bundle)
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
self.children.push(entity);
|
|
||||||
|
|
||||||
GridCommands {
|
GridCommands {
|
||||||
entity,
|
entity,
|
||||||
commands: self.commands.reborrow(),
|
commands: self.commands.reborrow(),
|
||||||
@ -166,11 +178,13 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Access the underlying commands.
|
/// Access the underlying commands.
|
||||||
|
#[inline]
|
||||||
pub fn commands(&mut self) -> &mut Commands<'a, 'a> {
|
pub fn commands(&mut self) -> &mut Commands<'a, 'a> {
|
||||||
&mut self.commands
|
&mut self.commands
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the passed bundle which provides this grid, and adds it to this entity as a child.
|
/// Spawns the passed bundle which provides this grid, and adds it to this entity as a child.
|
||||||
|
#[inline]
|
||||||
pub fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
|
pub fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
|
||||||
self.commands.entity(self.entity).with_child(bundle);
|
self.commands.entity(self.entity).with_child(bundle);
|
||||||
self
|
self
|
||||||
|
|||||||
@ -36,7 +36,7 @@ pub struct GridCellAny;
|
|||||||
///
|
///
|
||||||
/// [`BigSpace`]s are only allowed to have a single type of `GridCell`, you cannot mix
|
/// [`BigSpace`]s are only allowed to have a single type of `GridCell`, you cannot mix
|
||||||
/// [`GridPrecision`]s.
|
/// [`GridPrecision`]s.
|
||||||
#[derive(Component, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Reflect)]
|
#[derive(Component, Default, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)]
|
||||||
#[reflect(Component, Default, PartialEq)]
|
#[reflect(Component, Default, PartialEq)]
|
||||||
#[require(Transform, GlobalTransform)]
|
#[require(Transform, GlobalTransform)]
|
||||||
#[component(storage = "Table", on_add = Self::on_add, on_remove = Self::on_remove)]
|
#[component(storage = "Table", on_add = Self::on_add, on_remove = Self::on_remove)]
|
||||||
@ -87,6 +87,28 @@ impl<P: GridPrecision> GridCell<P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a cell containing the minimum values for each element of self and rhs.
|
||||||
|
///
|
||||||
|
/// In other words this computes [self.x.min(rhs.x), self.y.min(rhs.y), ..].
|
||||||
|
pub fn min(&self, rhs: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.min(rhs.x),
|
||||||
|
y: self.y.min(rhs.y),
|
||||||
|
z: self.z.min(rhs.z),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a cell containing the maximum values for each element of self and rhs.
|
||||||
|
///
|
||||||
|
/// In other words this computes [self.x.max(rhs.x), self.y.max(rhs.y), ..].
|
||||||
|
pub fn max(&self, rhs: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.max(rhs.x),
|
||||||
|
y: self.y.max(rhs.y),
|
||||||
|
z: self.z.max(rhs.z),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// If an entity's transform translation becomes larger than the limit specified in its
|
/// If an entity's transform translation becomes larger than the limit specified in its
|
||||||
/// [`Grid`], it will be relocated to the nearest grid cell to reduce the size of the transform.
|
/// [`Grid`], it will be relocated to the nearest grid cell to reduce the size of the transform.
|
||||||
pub fn recenter_large_transforms(
|
pub fn recenter_large_transforms(
|
||||||
|
|||||||
@ -34,6 +34,12 @@ impl<P: GridPrecision> PartialEq<GridHash<P>> for FastGridHash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<P: GridPrecision> From<GridHash<P>> for FastGridHash {
|
||||||
|
fn from(value: GridHash<P>) -> Self {
|
||||||
|
Self(value.pre_hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A unique spatial hash shared by all entities in the same [`GridCell`] within the same [`Grid`].
|
/// A unique spatial hash shared by all entities in the same [`GridCell`] within the same [`Grid`].
|
||||||
///
|
///
|
||||||
/// Once computed, a spatial hash can be used to rapidly check if any two entities are in the same
|
/// Once computed, a spatial hash can be used to rapidly check if any two entities are in the same
|
||||||
@ -153,52 +159,48 @@ impl<P: GridPrecision> GridHash<P> {
|
|||||||
pub(super) fn update<F: GridHashMapFilter>(
|
pub(super) fn update<F: GridHashMapFilter>(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut changed_hashes: ResMut<ChangedGridHashes<P, F>>,
|
mut changed_hashes: ResMut<ChangedGridHashes<P, F>>,
|
||||||
mut spatial_entities: ParamSet<(
|
mut spatial_entities: Query<
|
||||||
Query<
|
(
|
||||||
(
|
Entity,
|
||||||
Entity,
|
&Parent,
|
||||||
&Parent,
|
&GridCell<P>,
|
||||||
&GridCell<P>,
|
&mut GridHash<P>,
|
||||||
&mut GridHash<P>,
|
&mut FastGridHash,
|
||||||
&mut FastGridHash,
|
),
|
||||||
),
|
(F, Or<(Changed<Parent>, Changed<GridCell<P>>)>),
|
||||||
(F, Or<(Changed<Parent>, Changed<GridCell<P>>)>),
|
>,
|
||||||
>,
|
added_entities: Query<(Entity, &Parent, &GridCell<P>), (F, Without<GridHash<P>>)>,
|
||||||
Query<(Entity, &Parent, &GridCell<P>), (F, Without<GridHash<P>>)>,
|
|
||||||
)>,
|
|
||||||
mut stats: Option<ResMut<crate::timing::GridHashStats>>,
|
mut stats: Option<ResMut<crate::timing::GridHashStats>>,
|
||||||
mut thread_changed_hashes: Local<Parallel<Vec<Entity>>>,
|
mut thread_updated_hashes: Local<Parallel<Vec<Entity>>>,
|
||||||
mut thread_commands: Local<Parallel<Vec<(Entity, GridHash<P>, FastGridHash)>>>,
|
mut thread_commands: Local<Parallel<Vec<(Entity, GridHash<P>, FastGridHash)>>>,
|
||||||
) {
|
) {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Create new
|
// Create new
|
||||||
spatial_entities
|
added_entities
|
||||||
.p1()
|
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.for_each(|(entity, parent, cell)| {
|
.for_each(|(entity, parent, cell)| {
|
||||||
let spatial_hash = GridHash::new(parent, cell);
|
let spatial_hash = GridHash::new(parent, cell);
|
||||||
let fast_hash = FastGridHash(spatial_hash.pre_hash);
|
let fast_hash = spatial_hash.into();
|
||||||
thread_commands.scope(|tl| tl.push((entity, spatial_hash, fast_hash)));
|
thread_commands.scope(|tl| tl.push((entity, spatial_hash, fast_hash)));
|
||||||
thread_changed_hashes.scope(|tl| tl.push(entity));
|
thread_updated_hashes.scope(|tl| tl.push(entity));
|
||||||
});
|
});
|
||||||
for (entity, spatial_hash, fast_hash) in thread_commands.drain() {
|
for (entity, spatial_hash, fast_hash) in thread_commands.drain() {
|
||||||
commands.entity(entity).insert((spatial_hash, fast_hash));
|
commands.entity(entity).insert((spatial_hash, fast_hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update existing
|
// Update existing
|
||||||
spatial_entities.p0().par_iter_mut().for_each(
|
spatial_entities.par_iter_mut().for_each(
|
||||||
|(entity, parent, cell, mut hash, mut fast_hash)| {
|
|(entity, parent, cell, mut hash, mut fast_hash)| {
|
||||||
let new_hash = GridHash::new(parent, cell);
|
let new_hash = GridHash::new(parent, cell);
|
||||||
let new_fast_hash = new_hash.pre_hash;
|
let new_fast_hash = new_hash.pre_hash;
|
||||||
if hash.replace_if_neq(new_hash).is_some() {
|
if hash.replace_if_neq(new_hash).is_some() {
|
||||||
thread_changed_hashes.scope(|tl| tl.push(entity));
|
thread_updated_hashes.scope(|tl| tl.push(entity));
|
||||||
}
|
}
|
||||||
fast_hash.0 = new_fast_hash;
|
fast_hash.0 = new_fast_hash;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
thread_updated_hashes.drain_into(&mut changed_hashes.updated);
|
||||||
changed_hashes.list.extend(thread_changed_hashes.drain());
|
|
||||||
|
|
||||||
if let Some(ref mut stats) = stats {
|
if let Some(ref mut stats) = stats {
|
||||||
stats.hash_update_duration += start.elapsed();
|
stats.hash_update_duration += start.elapsed();
|
||||||
|
|||||||
@ -116,7 +116,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
|
impl<P, F> GridHashMap<P, F>
|
||||||
|
where
|
||||||
|
P: GridPrecision,
|
||||||
|
F: GridHashMapFilter,
|
||||||
|
{
|
||||||
/// Get information about all entities located at this [`GridHash`], as well as its
|
/// Get information about all entities located at this [`GridHash`], as well as its
|
||||||
/// neighbors.
|
/// neighbors.
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -243,7 +247,11 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Private Systems
|
/// Private Systems
|
||||||
impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
|
impl<P, F> GridHashMap<P, F>
|
||||||
|
where
|
||||||
|
P: GridPrecision,
|
||||||
|
F: GridHashMapFilter,
|
||||||
|
{
|
||||||
/// Update the [`GridHashMap`] with entities that have changed [`GridHash`]es, and meet the
|
/// Update the [`GridHashMap`] with entities that have changed [`GridHash`]es, and meet the
|
||||||
/// optional [`GridHashMapFilter`].
|
/// optional [`GridHashMapFilter`].
|
||||||
pub(super) fn update(
|
pub(super) fn update(
|
||||||
@ -263,12 +271,12 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut stats) = stats {
|
if let Some(ref mut stats) = stats {
|
||||||
stats.moved_entities = changed_hashes.list.len();
|
stats.moved_entities = changed_hashes.updated.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
// See the docs on ChangedGridHash understand why we don't use query change detection.
|
// See the docs on ChangedGridHash understand why we don't use query change detection.
|
||||||
for (entity, spatial_hash) in changed_hashes
|
for (entity, spatial_hash) in changed_hashes
|
||||||
.list
|
.updated
|
||||||
.drain(..)
|
.drain(..)
|
||||||
.filter_map(|entity| all_hashes.get(entity).ok())
|
.filter_map(|entity| all_hashes.get(entity).ok())
|
||||||
{
|
{
|
||||||
@ -282,7 +290,11 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Private Methods
|
/// Private Methods
|
||||||
impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
|
impl<P, F> GridHashMap<P, F>
|
||||||
|
where
|
||||||
|
P: GridPrecision,
|
||||||
|
F: GridHashMapFilter,
|
||||||
|
{
|
||||||
/// Insert an entity into the [`GridHashMap`], updating any existing entries.
|
/// Insert an entity into the [`GridHashMap`], updating any existing entries.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn insert(&mut self, entity: Entity, hash: GridHash<P>) {
|
fn insert(&mut self, entity: Entity, hash: GridHash<P>) {
|
||||||
@ -361,31 +373,35 @@ impl<P: GridPrecision> InnerGridHashMap<P> {
|
|||||||
} else {
|
} else {
|
||||||
let mut entities = self.hash_set_pool.pop().unwrap_or_default();
|
let mut entities = self.hash_set_pool.pop().unwrap_or_default();
|
||||||
entities.insert(entity);
|
entities.insert(entity);
|
||||||
|
self.insert_entry(hash, entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut occupied_neighbors = self.neighbor_pool.pop().unwrap_or_default();
|
#[inline]
|
||||||
occupied_neighbors.extend(hash.adjacent(1).filter(|neighbor| {
|
fn insert_entry(&mut self, hash: GridHash<P>, entities: HashSet<Entity, EntityHash>) {
|
||||||
self.inner
|
let mut occupied_neighbors = self.neighbor_pool.pop().unwrap_or_default();
|
||||||
.get_mut(neighbor)
|
occupied_neighbors.extend(hash.adjacent(1).filter(|neighbor| {
|
||||||
.map(|entry| {
|
self.inner
|
||||||
entry.occupied_neighbors.push(hash);
|
.get_mut(neighbor)
|
||||||
true
|
.map(|entry| {
|
||||||
})
|
entry.occupied_neighbors.push(hash);
|
||||||
.unwrap_or_default()
|
true
|
||||||
}));
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}));
|
||||||
|
|
||||||
self.inner.insert(
|
self.inner.insert(
|
||||||
hash,
|
hash,
|
||||||
GridHashEntry {
|
GridHashEntry {
|
||||||
entities,
|
entities,
|
||||||
occupied_neighbors,
|
occupied_neighbors,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if !self.just_removed.remove(&hash) {
|
if !self.just_removed.remove(&hash) {
|
||||||
// If a cell is removed then added within the same update, it can't be considered
|
// If a cell is removed then added within the same update, it can't be considered
|
||||||
// "just added" because it *already existed* at the start of the update.
|
// "just added" because it *already existed* at the start of the update.
|
||||||
self.just_inserted.insert(hash);
|
self.just_inserted.insert(hash);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,14 +92,14 @@ impl<T: QueryFilter + Send + Sync + 'static> GridHashMapFilter for T {}
|
|||||||
/// react to a component being mutated. For now, this performs well enough.
|
/// react to a component being mutated. For now, this performs well enough.
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct ChangedGridHashes<P: GridPrecision, F: GridHashMapFilter> {
|
struct ChangedGridHashes<P: GridPrecision, F: GridHashMapFilter> {
|
||||||
list: Vec<Entity>,
|
updated: Vec<Entity>,
|
||||||
spooky: PhantomData<(P, F)>,
|
spooky: PhantomData<(P, F)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: GridPrecision, F: GridHashMapFilter> Default for ChangedGridHashes<P, F> {
|
impl<P: GridPrecision, F: GridHashMapFilter> Default for ChangedGridHashes<P, F> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
list: Vec::new(),
|
updated: Vec::new(),
|
||||||
spooky: PhantomData,
|
spooky: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
//! Detect and update groups of nearby occupied cells.
|
//! Detect and update groups of nearby occupied cells.
|
||||||
|
|
||||||
use std::{hash::Hash, marker::PhantomData, ops::Deref, time::Instant};
|
use std::{hash::Hash, marker::PhantomData, ops::Deref};
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_tasks::{ComputeTaskPool, ParallelSliceMut};
|
use bevy_tasks::{ComputeTaskPool, ParallelSliceMut};
|
||||||
use bevy_utils::{
|
use bevy_utils::{
|
||||||
hashbrown::{HashMap, HashSet},
|
hashbrown::{HashMap, HashSet},
|
||||||
PassHash,
|
Instant, PassHash,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{GridHash, GridHashMap, GridHashMapFilter, GridHashMapSystem, GridPrecision};
|
use super::{GridCell, GridHash, GridHashMap, GridHashMapFilter, GridHashMapSystem, GridPrecision};
|
||||||
|
|
||||||
|
pub use private::GridPartition;
|
||||||
|
|
||||||
/// Adds support for spatial partitioning. Requires [`GridHashPlugin`](super::GridHashPlugin).
|
/// Adds support for spatial partitioning. Requires [`GridHashPlugin`](super::GridHashPlugin).
|
||||||
pub struct GridPartitionPlugin<P, F = ()>(PhantomData<(P, F)>)
|
pub struct GridPartitionPlugin<P, F = ()>(PhantomData<(P, F)>)
|
||||||
@ -61,7 +63,7 @@ impl Hash for GridPartitionId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Groups connected [`GridCell`](crate::GridCell)s into [`GridPartition`]s.
|
/// Groups connected [`GridCell`]s into [`GridPartition`]s.
|
||||||
///
|
///
|
||||||
/// Partitions divide space into independent groups of cells.
|
/// Partitions divide space into independent groups of cells.
|
||||||
///
|
///
|
||||||
@ -135,15 +137,16 @@ where
|
|||||||
let Some(hash) = set.iter().next() else {
|
let Some(hash) = set.iter().next() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let mut min = hash.cell();
|
||||||
|
let mut max = hash.cell();
|
||||||
for hash in set.iter() {
|
for hash in set.iter() {
|
||||||
self.reverse_map.insert(*hash, partition);
|
self.reverse_map.insert(*hash, partition);
|
||||||
|
min = min.min(hash.cell());
|
||||||
|
max = max.max(hash.cell());
|
||||||
}
|
}
|
||||||
self.partitions.insert(
|
self.partitions.insert(
|
||||||
partition,
|
partition,
|
||||||
GridPartition {
|
GridPartition::new(hash.grid(), vec![set], min, max),
|
||||||
grid: hash.grid(),
|
|
||||||
tables: vec![set],
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,8 +165,14 @@ where
|
|||||||
let Some(old_id) = self.reverse_map.remove(hash) else {
|
let Some(old_id) = self.reverse_map.remove(hash) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let mut empty = false;
|
||||||
if let Some(partition) = self.partitions.get_mut(&old_id) {
|
if let Some(partition) = self.partitions.get_mut(&old_id) {
|
||||||
partition.tables.iter_mut().any(|table| table.remove(hash));
|
if partition.remove(hash) && partition.is_empty() {
|
||||||
|
empty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if empty {
|
||||||
|
self.partitions.remove(&old_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,12 +261,6 @@ where
|
|||||||
partition_map.remove(removed_cell);
|
partition_map.remove(removed_cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up empty tables and partitions
|
|
||||||
partition_map.partitions.retain(|_id, partition| {
|
|
||||||
partition.tables.retain(|table| !table.is_empty());
|
|
||||||
!partition.tables.is_empty()
|
|
||||||
});
|
|
||||||
|
|
||||||
for removed_cell in hash_grid.just_removed().iter() {
|
for removed_cell in hash_grid.just_removed().iter() {
|
||||||
// Group occupied neighbor cells by partition, so we can check if they are still
|
// Group occupied neighbor cells by partition, so we can check if they are still
|
||||||
// connected to each other after this removal.
|
// connected to each other after this removal.
|
||||||
@ -293,7 +296,7 @@ where
|
|||||||
let _task_span = tracing::info_span!("parallel partition split").entered();
|
let _task_span = tracing::info_span!("parallel partition split").entered();
|
||||||
affected_cells
|
affected_cells
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.filter_map(|(original_partition, adjacent_hashes)| {
|
.filter_map(|(id, adjacent_hashes)| {
|
||||||
let mut new_partitions = Vec::with_capacity(0);
|
let mut new_partitions = Vec::with_capacity(0);
|
||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
while let Some(this_cell) = adjacent_hashes.iter().next().copied() {
|
while let Some(this_cell) = adjacent_hashes.iter().next().copied() {
|
||||||
@ -309,7 +312,7 @@ where
|
|||||||
if adjacent_hashes.is_empty() && counter == 0 {
|
if adjacent_hashes.is_empty() && counter == 0 {
|
||||||
// If it only took a single iteration to connect all affected cells,
|
// If it only took a single iteration to connect all affected cells,
|
||||||
// it means the partition has not been split, and we can continue to
|
// it means the partition has not been split, and we can continue to
|
||||||
// the next // partition.
|
// the next partition.
|
||||||
return None;
|
return None;
|
||||||
} else {
|
} else {
|
||||||
new_partitions
|
new_partitions
|
||||||
@ -319,7 +322,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(SplitResult {
|
Some(SplitResult {
|
||||||
original_partition: *original_partition,
|
original_partition_id: *id,
|
||||||
new_partitions,
|
new_partitions,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -328,27 +331,15 @@ where
|
|||||||
);
|
);
|
||||||
|
|
||||||
for SplitResult {
|
for SplitResult {
|
||||||
original_partition,
|
original_partition_id,
|
||||||
ref mut new_partitions,
|
ref mut new_partitions,
|
||||||
} in split_results.iter_mut().flatten()
|
} in split_results.iter_mut().flatten()
|
||||||
{
|
{
|
||||||
// We want the original partition to retain the most cells to ensure that the smaller
|
// We want the original partition to retain the most cells to ensure that the smaller
|
||||||
// sets are the ones that are assigned a new partition ID.
|
// sets are the ones that are assigned a new partition ID.
|
||||||
new_partitions.sort_unstable_by_key(|v| v.len());
|
new_partitions.sort_unstable_by_key(|set| set.len());
|
||||||
if let Some(partition) = new_partitions.pop() {
|
if let Some(largest_partition) = new_partitions.pop() {
|
||||||
if let Some(tables) = partition_map
|
partition_map.insert(*original_partition_id, largest_partition);
|
||||||
.partitions
|
|
||||||
.get_mut(original_partition)
|
|
||||||
.map(|p| &mut p.tables)
|
|
||||||
{
|
|
||||||
// TODO: keep these in an object pool to reuse allocs
|
|
||||||
tables.drain(1..);
|
|
||||||
if let Some(table) = tables.get_mut(0) {
|
|
||||||
*table = partition;
|
|
||||||
} else {
|
|
||||||
tables.push(partition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point the reverse map will be out of date. However, `partitions.insert()`
|
// At this point the reverse map will be out of date. However, `partitions.insert()`
|
||||||
@ -363,86 +354,193 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SplitResult<P: GridPrecision> {
|
struct SplitResult<P: GridPrecision> {
|
||||||
original_partition: GridPartitionId,
|
original_partition_id: GridPartitionId,
|
||||||
new_partitions: Vec<HashSet<GridHash<P>, PassHash>>,
|
new_partitions: Vec<HashSet<GridHash<P>, PassHash>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
|
/// A private module to ensure the internal fields of the partition are not accessed directly.
|
||||||
/// [`GridCell`](crate::GridCell)s.
|
/// Needed to ensure invariants are upheld.
|
||||||
#[derive(Debug)]
|
mod private {
|
||||||
pub struct GridPartition<P: GridPrecision> {
|
use super::{GridCell, GridHash, GridPrecision};
|
||||||
grid: Entity,
|
use bevy_ecs::prelude::*;
|
||||||
tables: Vec<HashSet<GridHash<P>, PassHash>>,
|
use bevy_utils::{hashbrown::HashSet, PassHash};
|
||||||
}
|
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
|
||||||
impl<P: GridPrecision> GridPartition<P> {
|
/// [`GridCell`](crate::GridCell)s.
|
||||||
/// Tables smaller than this will be drained into other tables when merging. Tables larger than
|
#[derive(Debug)]
|
||||||
/// this limit will instead be added to a list of tables. This prevents partitions ending up
|
pub struct GridPartition<P: GridPrecision> {
|
||||||
/// with many tables containing a few entries.
|
grid: Entity,
|
||||||
///
|
tables: Vec<HashSet<GridHash<P>, PassHash>>,
|
||||||
/// Draining and extending a hash set is much slower than moving the entire hash set into a
|
min: GridCell<P>,
|
||||||
/// list. The tradeoff is that the more tables added, the more there are that need to be
|
max: GridCell<P>,
|
||||||
/// iterated over when searching for a cell.
|
|
||||||
const MIN_TABLE_SIZE: usize = 128;
|
|
||||||
|
|
||||||
/// Returns `true` if the `hash` is in this partition.
|
|
||||||
#[inline]
|
|
||||||
pub fn contains(&self, hash: &GridHash<P>) -> bool {
|
|
||||||
self.tables.iter().any(|table| table.contains(hash))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over all [`GridHash`]s in this partition.
|
impl<P: GridPrecision> GridPartition<P> {
|
||||||
#[inline]
|
/// Returns `true` if the `hash` is in this partition.
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &GridHash<P>> {
|
#[inline]
|
||||||
self.tables.iter().flat_map(|table| table.iter())
|
pub fn contains(&self, hash: &GridHash<P>) -> bool {
|
||||||
}
|
self.tables.iter().any(|table| table.contains(hash))
|
||||||
|
|
||||||
/// Returns the total number of cells in this partition.
|
|
||||||
#[inline]
|
|
||||||
pub fn num_cells(&self) -> usize {
|
|
||||||
self.tables.iter().map(|t| t.len()).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn insert(&mut self, cell: GridHash<P>) {
|
|
||||||
if self.contains(&cell) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if let Some(i) = self.smallest_table() {
|
|
||||||
self.tables[i].insert(cell);
|
/// Iterates over all [`GridHash`]s in this partition.
|
||||||
} else {
|
#[inline]
|
||||||
let mut table = HashSet::default();
|
pub fn iter(&self) -> impl Iterator<Item = &GridHash<P>> {
|
||||||
table.insert(cell);
|
self.tables.iter().flat_map(|table| table.iter())
|
||||||
self.tables.push(table);
|
}
|
||||||
|
|
||||||
|
/// Returns the total number of cells in this partition.
|
||||||
|
#[inline]
|
||||||
|
pub fn num_cells(&self) -> usize {
|
||||||
|
self.tables.iter().map(|t| t.len()).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The grid this partition resides in.
|
||||||
|
#[inline]
|
||||||
|
pub fn grid(&self) -> Entity {
|
||||||
|
self.grid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum grid cell extent of the partition.
|
||||||
|
pub fn max(&self) -> GridCell<P> {
|
||||||
|
self.max
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The minimum grid cell extent of the partition.
|
||||||
|
pub fn min(&self) -> GridCell<P> {
|
||||||
|
self.min
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees up any unused memory. Returns `false` if the partition is completely empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.tables.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
/// Private internal methods
|
||||||
fn smallest_table(&self) -> Option<usize> {
|
impl<P: GridPrecision> GridPartition<P> {
|
||||||
self.tables
|
pub(crate) fn new(
|
||||||
.iter()
|
grid: Entity,
|
||||||
.enumerate()
|
tables: Vec<HashSet<GridHash<P>, PassHash>>,
|
||||||
.map(|(i, t)| (i, t.len()))
|
min: GridCell<P>,
|
||||||
.min_by_key(|(_, len)| *len)
|
max: GridCell<P>,
|
||||||
.map(|(i, _len)| i)
|
) -> Self {
|
||||||
}
|
Self {
|
||||||
|
grid,
|
||||||
|
tables,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
/// Tables smaller than this will be drained into other tables when merging. Tables larger than
|
||||||
fn extend(&mut self, mut partition: GridPartition<P>) {
|
/// this limit will instead be added to a list of tables. This prevents partitions ending up
|
||||||
for mut table in partition.tables.drain(..) {
|
/// with many tables containing a few entries.
|
||||||
if table.len() < Self::MIN_TABLE_SIZE {
|
///
|
||||||
if let Some(i) = self.smallest_table() {
|
/// Draining and extending a hash set is much slower than moving the entire hash set into a
|
||||||
self.tables[i].extend(table.drain());
|
/// list. The tradeoff is that the more tables added, the more there are that need to be
|
||||||
|
/// iterated over when searching for a cell.
|
||||||
|
const MIN_TABLE_SIZE: usize = 20_000;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn insert(&mut self, cell: GridHash<P>) {
|
||||||
|
if self.contains(&cell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(i) = self.smallest_table() {
|
||||||
|
self.tables[i].insert(cell);
|
||||||
|
} else {
|
||||||
|
let mut table = HashSet::default();
|
||||||
|
table.insert(cell);
|
||||||
|
self.tables.push(table);
|
||||||
|
}
|
||||||
|
self.min = self.min.min(cell.cell());
|
||||||
|
self.max = self.max.max(cell.cell());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn smallest_table(&self) -> Option<usize> {
|
||||||
|
self.tables
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, t)| (i, t.len()))
|
||||||
|
.min_by_key(|(_, len)| *len)
|
||||||
|
.map(|(i, _len)| i)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn extend(&mut self, mut partition: GridPartition<P>) {
|
||||||
|
for mut table in partition.tables.drain(..) {
|
||||||
|
if table.len() < Self::MIN_TABLE_SIZE {
|
||||||
|
if let Some(i) = self.smallest_table() {
|
||||||
|
for hash in table.drain() {
|
||||||
|
self.tables[i].insert_unique_unchecked(hash);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.tables.push(table);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.tables.push(table);
|
self.tables.push(table);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
self.min = self.min.min(partition.min);
|
||||||
|
self.max = self.max.max(partition.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a grid hash from the partition. Returns whether the value was present.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn remove(&mut self, hash: &GridHash<P>) -> bool {
|
||||||
|
let Some(i_table) = self
|
||||||
|
.tables
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(i, table)| table.remove(hash).then_some(i))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if self.tables[i_table].is_empty() {
|
||||||
|
self.tables.swap_remove(i_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (cell, min, max) = (hash.cell(), self.min, self.max);
|
||||||
|
// Only need to recompute the bounds if the removed cell was touching the boundary.
|
||||||
|
if min.x == cell.x || min.y == cell.y || min.z == cell.z {
|
||||||
|
self.compute_min();
|
||||||
|
}
|
||||||
|
// Note this is not an `else if`. The cell might be on the max bound in one axis, and the
|
||||||
|
// min bound in another.
|
||||||
|
if max.x == cell.x || max.y == cell.y || max.z == cell.z {
|
||||||
|
self.compute_max();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the minimum bounding coordinate. Requires linearly scanning over entries in the
|
||||||
|
/// partition.
|
||||||
|
#[inline]
|
||||||
|
fn compute_min(&mut self) {
|
||||||
|
if let Some(min) = self
|
||||||
|
.iter()
|
||||||
|
.map(|hash| hash.cell())
|
||||||
|
.reduce(|acc, e| acc.min(e))
|
||||||
|
{
|
||||||
|
self.min = min
|
||||||
} else {
|
} else {
|
||||||
self.tables.push(table);
|
self.min = GridCell::ONE * P::from_f64(1e10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the maximum bounding coordinate. Requires linearly scanning over entries in the
|
||||||
|
/// partition.
|
||||||
|
#[inline]
|
||||||
|
fn compute_max(&mut self) {
|
||||||
|
if let Some(max) = self
|
||||||
|
.iter()
|
||||||
|
.map(|hash| hash.cell())
|
||||||
|
.reduce(|acc, e| acc.max(e))
|
||||||
|
{
|
||||||
|
self.max = max
|
||||||
|
} else {
|
||||||
|
self.min = GridCell::ONE * P::from_f64(-1e10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The grid this partition resides in.
|
|
||||||
pub fn grid(&self) -> Entity {
|
|
||||||
self.grid
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user