Skip to main content

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}