Updated to match constraints better.

This commit is contained in:
2026-04-08 20:47:12 -05:00
parent 9c7a5dfcd4
commit cac4178600
+33 -23
View File
@@ -10,6 +10,7 @@ use std::time::{Duration, Instant};
pub trait Positions: Sync { pub trait Positions: Sync {
fn len(&self) -> usize; fn len(&self) -> usize;
fn get(&self, index: u32) -> (f32, f32); fn get(&self, index: u32) -> (f32, f32);
fn radius(&self, index: u32) -> f32;
} }
/// Read-write access to entity kinematics. The collision system never /// Read-write access to entity kinematics. The collision system never
@@ -35,11 +36,10 @@ pub struct SpatialGrid {
inv_cell_size: f32, inv_cell_size: f32,
grid_dim: usize, grid_dim: usize,
total_cells: usize, total_cells: usize,
// Counting-sort buffers, reused across frames
cell_counts: Vec<u32>, cell_counts: Vec<u32>,
cell_offsets: Vec<u32>, cell_offsets: Vec<u32>,
sorted: Vec<u32>, // entity indices sorted by cell sorted: Vec<u32>,
pairs: Vec<(u32, u32)>, // (cell_index, entity_index) staging pairs: Vec<(u32, u32)>,
} }
impl SpatialGrid { impl SpatialGrid {
@@ -57,11 +57,9 @@ impl SpatialGrid {
} }
} }
/// Rebuild the grid from scratch. Call once per tick.
pub fn rebuild(&mut self, positions: &impl Positions) { pub fn rebuild(&mut self, positions: &impl Positions) {
let n = positions.len(); let n = positions.len();
// Ensure buffers are big enough
if self.sorted.len() < n { if self.sorted.len() < n {
self.sorted.resize(n, 0); self.sorted.resize(n, 0);
} }
@@ -69,7 +67,6 @@ impl SpatialGrid {
self.cell_counts.iter_mut().for_each(|c| *c = 0); self.cell_counts.iter_mut().for_each(|c| *c = 0);
self.pairs.clear(); self.pairs.clear();
// Pass 1: bin every entity
for i in 0..n { for i in 0..n {
let (x, y) = positions.get(i as u32); let (x, y) = positions.get(i as u32);
if let Some(ci) = self.cell_of(x, y) { if let Some(ci) = self.cell_of(x, y) {
@@ -78,14 +75,12 @@ impl SpatialGrid {
} }
} }
// Pass 2: prefix sum
let mut running = 0u32; let mut running = 0u32;
for i in 0..self.total_cells { for i in 0..self.total_cells {
self.cell_offsets[i] = running; self.cell_offsets[i] = running;
running += self.cell_counts[i]; running += self.cell_counts[i];
} }
// Pass 3: scatter into sorted order
let mut cursors = self.cell_offsets.clone(); let mut cursors = self.cell_offsets.clone();
for &(ci, ei) in &self.pairs { for &(ci, ei) in &self.pairs {
let slot = cursors[ci as usize] as usize; let slot = cursors[ci as usize] as usize;
@@ -110,12 +105,11 @@ impl SpatialGrid {
// Collision detection — free function, parallel, read-only // Collision detection — free function, parallel, read-only
// ============================================================ // ============================================================
/// Detect all overlapping pairs and report them through the handler. /// Detect all overlapping pairs using per-entity radii.
/// Returns total number of collision pairs found. /// Returns total number of collision pairs found.
pub fn detect_collisions( pub fn detect_collisions(
grid: &SpatialGrid, grid: &SpatialGrid,
positions: &impl Positions, positions: &impl Positions,
collision_dist_sq: f32,
) -> u64 { ) -> u64 {
const NEIGHBORS: [(i32, i32); 4] = [(1, 0), (0, 1), (-1, 1), (1, 1)]; const NEIGHBORS: [(i32, i32); 4] = [(1, 0), (0, 1), (-1, 1), (1, 1)];
@@ -133,12 +127,17 @@ pub fn detect_collisions(
// Intra-cell // Intra-cell
for i in 0..a_count { for i in 0..a_count {
let (px, py) = positions.get(grid.sorted[a_start + i]); let ai = grid.sorted[a_start + i];
let (px, py) = positions.get(ai);
let ra = positions.radius(ai);
for j in (i + 1)..a_count { for j in (i + 1)..a_count {
let (qx, qy) = positions.get(grid.sorted[a_start + j]); let bj = grid.sorted[a_start + j];
let (qx, qy) = positions.get(bj);
let rb = positions.radius(bj);
let dx = px - qx; let dx = px - qx;
let dy = py - qy; let dy = py - qy;
if dx * dx + dy * dy <= collision_dist_sq { let dist = ra + rb;
if dx * dx + dy * dy <= dist * dist {
row_hits += 1; row_hits += 1;
} }
} }
@@ -158,12 +157,17 @@ pub fn detect_collisions(
continue; continue;
} }
for i in 0..a_count { for i in 0..a_count {
let (px, py) = positions.get(grid.sorted[a_start + i]); let ai = grid.sorted[a_start + i];
let (px, py) = positions.get(ai);
let ra = positions.radius(ai);
for j in 0..b_count { for j in 0..b_count {
let (qx, qy) = positions.get(grid.sorted[b_start + j]); let bj = grid.sorted[b_start + j];
let (qx, qy) = positions.get(bj);
let rb = positions.radius(bj);
let dx = px - qx; let dx = px - qx;
let dy = py - qy; let dy = py - qy;
if dx * dx + dy * dy <= collision_dist_sq { let dist = ra + rb;
if dx * dx + dy * dy <= dist * dist {
row_hits += 1; row_hits += 1;
} }
} }
@@ -237,6 +241,7 @@ struct ProjectileStore {
y: Vec<f32>, y: Vec<f32>,
vx: Vec<f32>, vx: Vec<f32>,
vy: Vec<f32>, vy: Vec<f32>,
radius: Vec<f32>,
} }
impl Positions for ProjectileStore { impl Positions for ProjectileStore {
@@ -246,6 +251,10 @@ impl Positions for ProjectileStore {
let i = index as usize; let i = index as usize;
(self.x[i], self.y[i]) (self.x[i], self.y[i])
} }
#[inline(always)]
fn radius(&self, index: u32) -> f32 {
self.radius[index as usize]
}
} }
impl Kinematics for ProjectileStore { impl Kinematics for ProjectileStore {
@@ -274,10 +283,11 @@ impl Rng {
fn next_signed(&mut self) -> f32 { self.next_f32() * 2.0 - 1.0 } fn next_signed(&mut self) -> f32 { self.next_f32() * 2.0 - 1.0 }
} }
const NUM_PROJECTILES: usize = 200_000; const NUM_PROJECTILES: usize = 500_000;
const WORLD_SIZE: f32 = 4000.0; const WORLD_SIZE: f32 = 14000.0;
const PROJECTILE_RADIUS: f32 = 2.0; const MIN_RADIUS: f32 = 1.0; // small bullets
const CELL_SIZE: f32 = 8.0; const MAX_RADIUS: f32 = 8.0; // big ships / missiles
const CELL_SIZE: f32 = 20.0; // >= 2 * MAX_RADIUS so neighbors always suffice
const GRID_DIM: usize = (WORLD_SIZE / CELL_SIZE) as usize; const GRID_DIM: usize = (WORLD_SIZE / CELL_SIZE) as usize;
const TICK_DT: f32 = 1.0 / 60.0; const TICK_DT: f32 = 1.0 / 60.0;
const MAX_SPEED: f32 = 200.0; const MAX_SPEED: f32 = 200.0;
@@ -292,10 +302,10 @@ fn main() {
y: (0..NUM_PROJECTILES).map(|_| rng.next_f32() * WORLD_SIZE).collect(), y: (0..NUM_PROJECTILES).map(|_| rng.next_f32() * WORLD_SIZE).collect(),
vx: (0..NUM_PROJECTILES).map(|_| rng.next_signed() * MAX_SPEED).collect(), vx: (0..NUM_PROJECTILES).map(|_| rng.next_signed() * MAX_SPEED).collect(),
vy: (0..NUM_PROJECTILES).map(|_| rng.next_signed() * MAX_SPEED).collect(), vy: (0..NUM_PROJECTILES).map(|_| rng.next_signed() * MAX_SPEED).collect(),
radius: (0..NUM_PROJECTILES).map(|_| MIN_RADIUS + rng.next_f32() * (MAX_RADIUS - MIN_RADIUS)).collect(),
}; };
let mut grid = SpatialGrid::new(CELL_SIZE, GRID_DIM); let mut grid = SpatialGrid::new(CELL_SIZE, GRID_DIM);
let collision_dist_sq = (PROJECTILE_RADIUS * 2.0) * (PROJECTILE_RADIUS * 2.0);
let mut total_collisions: u64 = 0; let mut total_collisions: u64 = 0;
let mut total_physics_time = Duration::ZERO; let mut total_physics_time = Duration::ZERO;
@@ -307,7 +317,7 @@ fn main() {
Projectiles: {NUM_PROJECTILES}\n\ Projectiles: {NUM_PROJECTILES}\n\
World: {WORLD_SIZE}x{WORLD_SIZE}\n\ World: {WORLD_SIZE}x{WORLD_SIZE}\n\
Cell size: {CELL_SIZE} ({GRID_DIM}x{GRID_DIM} = {} cells)\n\ Cell size: {CELL_SIZE} ({GRID_DIM}x{GRID_DIM} = {} cells)\n\
Projectile radius: {PROJECTILE_RADIUS}\n\ Radii: {MIN_RADIUS} - {MAX_RADIUS}\n\
Ticks: {NUM_TICKS} ({} seconds at 60hz)\n\ Ticks: {NUM_TICKS} ({} seconds at 60hz)\n\
Rayon threads: {}\n", Rayon threads: {}\n",
GRID_DIM * GRID_DIM, GRID_DIM * GRID_DIM,
@@ -338,7 +348,7 @@ fn main() {
// --- Collision (knows nothing about movement or storage) --- // --- Collision (knows nothing about movement or storage) ---
let t_coll = Instant::now(); let t_coll = Instant::now();
let frame_collisions = detect_collisions(&grid, &store, collision_dist_sq); let frame_collisions = detect_collisions(&grid, &store);
let coll_elapsed = t_coll.elapsed(); let coll_elapsed = t_coll.elapsed();
total_collision_time += coll_elapsed; total_collision_time += coll_elapsed;
total_collisions += frame_collisions; total_collisions += frame_collisions;