goud_engine/ecs/broad_phase/spatial_hash.rs
1//! Spatial hash data structure and mutation operations.
2
3use crate::core::math::Rect;
4use crate::ecs::Entity;
5use std::collections::{HashMap, HashSet};
6
7use super::grid::CellCoord;
8use super::stats::SpatialHashStats;
9
10// =============================================================================
11// SpatialHash - Broad Phase Collision Detection
12// =============================================================================
13
14/// Spatial hash for broad phase collision detection.
15///
16/// Divides 2D space into a uniform grid of cells. Entities are mapped to cells
17/// based on their AABBs. This enables O(1) insertion/removal and efficient
18/// querying of potential collision pairs.
19///
20/// # Performance
21///
22/// - Insertion: O(k) where k = number of cells occupied by AABB
23/// - Removal: O(k)
24/// - Query pairs: O(n^2) where n = average entities per cell
25/// - Total query: O(m * n^2) where m = number of occupied cells
26///
27/// # Cell Size Selection
28///
29/// The cell size should be roughly equal to the average object size:
30/// - Too small: Objects span many cells, increasing overhead
31/// - Too large: Too many objects per cell, increasing pair checks
32///
33/// For games with mixed object sizes, use the size of the most common objects.
34///
35/// # Examples
36///
37/// ```
38/// use goud_engine::ecs::broad_phase::SpatialHash;
39/// use goud_engine::ecs::Entity;
40/// use goud_engine::core::math::Rect;
41///
42/// let mut hash = SpatialHash::new(64.0);
43///
44/// // Insert entities
45/// let player = Entity::new(0, 0);
46/// let enemy = Entity::new(1, 0);
47///
48/// hash.insert(player, Rect::new(0.0, 0.0, 32.0, 32.0));
49/// hash.insert(enemy, Rect::new(50.0, 0.0, 32.0, 32.0));
50///
51/// // Query potential collisions
52/// let pairs = hash.query_pairs();
53/// assert_eq!(pairs.len(), 1); // Player and enemy are close
54/// ```
55#[derive(Clone, Debug)]
56pub struct SpatialHash {
57 /// Size of each grid cell (world units).
58 pub(super) cell_size: f32,
59
60 /// Inverse of cell size for faster division (reserved for future use).
61 pub(super) _cell_size_inv: f32,
62
63 /// Grid cells: each cell contains a list of entities.
64 /// Key is the cell coordinate, value is the set of entities in that cell.
65 pub(super) grid: HashMap<CellCoord, HashSet<Entity>>,
66
67 /// Entity to AABB mapping for updates.
68 /// Stores the AABB for each entity to know which cells it occupies.
69 pub(super) entity_bounds: HashMap<Entity, Rect>,
70
71 /// Entity to cell list mapping.
72 /// Stores which cells each entity occupies for efficient removal.
73 pub(super) entity_cells: HashMap<Entity, Vec<CellCoord>>,
74
75 /// Statistics for debugging and profiling.
76 pub(super) stats: SpatialHashStats,
77}
78
79impl SpatialHash {
80 // =========================================================================
81 // Construction
82 // =========================================================================
83
84 /// Creates a new spatial hash with the specified cell size.
85 ///
86 /// # Arguments
87 ///
88 /// * `cell_size` - Size of each grid cell in world units (e.g., pixels)
89 ///
90 /// # Panics
91 ///
92 /// Panics if `cell_size` is not positive and finite.
93 ///
94 /// # Examples
95 ///
96 /// ```
97 /// use goud_engine::ecs::broad_phase::SpatialHash;
98 ///
99 /// // 64-pixel cells (good for medium-sized objects)
100 /// let hash = SpatialHash::new(64.0);
101 ///
102 /// // 128-pixel cells (larger objects)
103 /// let hash = SpatialHash::new(128.0);
104 /// ```
105 pub fn new(cell_size: f32) -> Self {
106 assert!(
107 cell_size > 0.0 && cell_size.is_finite(),
108 "Cell size must be positive and finite"
109 );
110
111 Self {
112 cell_size,
113 _cell_size_inv: 1.0 / cell_size,
114 grid: HashMap::new(),
115 entity_bounds: HashMap::new(),
116 entity_cells: HashMap::new(),
117 stats: SpatialHashStats::default(),
118 }
119 }
120
121 /// Creates a spatial hash with the specified cell size and initial capacity.
122 ///
123 /// Pre-allocates memory for the specified number of entities, reducing
124 /// allocations during insertion.
125 ///
126 /// # Arguments
127 ///
128 /// * `cell_size` - Size of each grid cell
129 /// * `capacity` - Expected number of entities
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// use goud_engine::ecs::broad_phase::SpatialHash;
135 ///
136 /// // Pre-allocate for 1000 entities
137 /// let hash = SpatialHash::with_capacity(64.0, 1000);
138 /// ```
139 pub fn with_capacity(cell_size: f32, capacity: usize) -> Self {
140 assert!(
141 cell_size > 0.0 && cell_size.is_finite(),
142 "Cell size must be positive and finite"
143 );
144
145 Self {
146 cell_size,
147 _cell_size_inv: 1.0 / cell_size,
148 grid: HashMap::with_capacity(capacity),
149 entity_bounds: HashMap::with_capacity(capacity),
150 entity_cells: HashMap::with_capacity(capacity),
151 stats: SpatialHashStats::default(),
152 }
153 }
154
155 // =========================================================================
156 // Insertion and Removal
157 // =========================================================================
158
159 /// Inserts an entity with its AABB into the spatial hash.
160 ///
161 /// The entity is added to all cells that its AABB overlaps. If the entity
162 /// was already in the hash, it is first removed before re-insertion.
163 ///
164 /// # Arguments
165 ///
166 /// * `entity` - The entity to insert
167 /// * `aabb` - The entity's axis-aligned bounding box
168 ///
169 /// # Examples
170 ///
171 /// ```
172 /// use goud_engine::ecs::broad_phase::SpatialHash;
173 /// use goud_engine::ecs::Entity;
174 /// use goud_engine::core::math::Rect;
175 ///
176 /// let mut hash = SpatialHash::new(64.0);
177 /// let entity = Entity::new(0, 0);
178 /// let aabb = Rect::new(0.0, 0.0, 32.0, 32.0);
179 ///
180 /// hash.insert(entity, aabb);
181 /// assert!(hash.contains(entity));
182 /// ```
183 pub fn insert(&mut self, entity: Entity, aabb: Rect) {
184 // Remove if already present
185 if self.entity_bounds.contains_key(&entity) {
186 self.remove(entity);
187 }
188
189 // Calculate which cells this AABB overlaps
190 let cells = self.get_cells_for_aabb(aabb);
191
192 // Insert entity into each cell
193 for cell in &cells {
194 self.grid.entry(*cell).or_default().insert(entity);
195 }
196
197 // Store mappings
198 self.entity_bounds.insert(entity, aabb);
199 self.entity_cells.insert(entity, cells);
200
201 // Update stats
202 self.update_stats();
203 }
204
205 /// Removes an entity from the spatial hash.
206 ///
207 /// # Arguments
208 ///
209 /// * `entity` - The entity to remove
210 ///
211 /// # Returns
212 ///
213 /// `true` if the entity was present and removed, `false` otherwise.
214 ///
215 /// # Examples
216 ///
217 /// ```
218 /// use goud_engine::ecs::broad_phase::SpatialHash;
219 /// use goud_engine::ecs::Entity;
220 /// use goud_engine::core::math::Rect;
221 ///
222 /// let mut hash = SpatialHash::new(64.0);
223 /// let entity = Entity::new(0, 0);
224 ///
225 /// hash.insert(entity, Rect::new(0.0, 0.0, 32.0, 32.0));
226 /// assert!(hash.remove(entity));
227 /// assert!(!hash.contains(entity));
228 /// ```
229 pub fn remove(&mut self, entity: Entity) -> bool {
230 // Get the cells this entity occupies
231 let cells = match self.entity_cells.remove(&entity) {
232 Some(cells) => cells,
233 None => return false, // Entity not in hash
234 };
235
236 // Remove from each cell
237 for cell in cells {
238 if let Some(cell_entities) = self.grid.get_mut(&cell) {
239 cell_entities.remove(&entity);
240
241 // Remove empty cells to save memory
242 if cell_entities.is_empty() {
243 self.grid.remove(&cell);
244 }
245 }
246 }
247
248 // Remove entity bounds
249 self.entity_bounds.remove(&entity);
250
251 // Update stats
252 self.update_stats();
253
254 true
255 }
256
257 /// Updates an entity's AABB in the spatial hash.
258 ///
259 /// This is more efficient than remove + insert when the entity hasn't
260 /// moved much, as it can avoid cell changes.
261 ///
262 /// # Arguments
263 ///
264 /// * `entity` - The entity to update
265 /// * `new_aabb` - The entity's new AABB
266 ///
267 /// # Returns
268 ///
269 /// `true` if the entity was present and updated, `false` otherwise.
270 ///
271 /// # Examples
272 ///
273 /// ```
274 /// use goud_engine::ecs::broad_phase::SpatialHash;
275 /// use goud_engine::ecs::Entity;
276 /// use goud_engine::core::math::Rect;
277 ///
278 /// let mut hash = SpatialHash::new(64.0);
279 /// let entity = Entity::new(0, 0);
280 ///
281 /// hash.insert(entity, Rect::new(0.0, 0.0, 32.0, 32.0));
282 /// hash.update(entity, Rect::new(10.0, 10.0, 32.0, 32.0));
283 /// ```
284 pub fn update(&mut self, entity: Entity, new_aabb: Rect) -> bool {
285 if !self.entity_bounds.contains_key(&entity) {
286 return false;
287 }
288
289 // Calculate new cells
290 let new_cells = self.get_cells_for_aabb(new_aabb);
291 let old_cells = self.entity_cells.get(&entity).unwrap();
292
293 // Optimization: if cells haven't changed, just update the AABB
294 if new_cells.len() == old_cells.len() && new_cells.iter().all(|c| old_cells.contains(c)) {
295 self.entity_bounds.insert(entity, new_aabb);
296 return true;
297 }
298
299 // Cells changed, do full update
300 self.remove(entity);
301 self.insert(entity, new_aabb);
302
303 true
304 }
305
306 /// Clears all entities from the spatial hash.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use goud_engine::ecs::broad_phase::SpatialHash;
312 /// use goud_engine::ecs::Entity;
313 /// use goud_engine::core::math::Rect;
314 ///
315 /// let mut hash = SpatialHash::new(64.0);
316 /// hash.insert(Entity::new(0, 0), Rect::new(0.0, 0.0, 32.0, 32.0));
317 ///
318 /// hash.clear();
319 /// assert_eq!(hash.entity_count(), 0);
320 /// ```
321 pub fn clear(&mut self) {
322 self.grid.clear();
323 self.entity_bounds.clear();
324 self.entity_cells.clear();
325 self.stats = SpatialHashStats::default();
326 }
327
328 // =========================================================================
329 // Internal Helpers (shared with queries module)
330 // =========================================================================
331
332 /// Gets all cell coordinates that an AABB overlaps.
333 pub(super) fn get_cells_for_aabb(&self, aabb: Rect) -> Vec<CellCoord> {
334 let mut cells = Vec::new();
335
336 // Calculate cell range
337 let min = aabb.min();
338 let max = aabb.max();
339
340 let min_cell = CellCoord::from_world(min, self.cell_size);
341 let max_cell = CellCoord::from_world(max, self.cell_size);
342
343 // Iterate over cell range
344 for y in min_cell.y..=max_cell.y {
345 for x in min_cell.x..=max_cell.x {
346 cells.push(CellCoord::new(x, y));
347 }
348 }
349
350 cells
351 }
352
353 /// Updates statistics after modification.
354 pub(super) fn update_stats(&mut self) {
355 self.stats.entity_count = self.entity_bounds.len();
356 self.stats.cell_count = self.grid.len();
357
358 // Count total cell entries and find max
359 let mut total = 0;
360 let mut max = 0;
361
362 for cell_entities in self.grid.values() {
363 let count = cell_entities.len();
364 total += count;
365 max = max.max(count);
366 }
367
368 self.stats.total_cell_entries = total;
369 self.stats.max_entities_per_cell = max;
370 self.stats.avg_entities_per_cell = if self.stats.cell_count > 0 {
371 total as f32 / self.stats.cell_count as f32
372 } else {
373 0.0
374 };
375 }
376}
377
378impl std::fmt::Display for SpatialHash {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 write!(
381 f,
382 "SpatialHash(cell_size: {:.1}, entities: {}, cells: {})",
383 self.cell_size,
384 self.entity_count(),
385 self.cell_count()
386 )
387 }
388}