Skip to content

Spatial Grid Plugin, Forest Fire and Spatial Pandemic Examples#1

Merged
haath merged 23 commits into
haath:mainfrom
rozgo:feature/spatial-grid
Jul 7, 2025
Merged

Spatial Grid Plugin, Forest Fire and Spatial Pandemic Examples#1
haath merged 23 commits into
haath:mainfrom
rozgo:feature/spatial-grid

Conversation

@rozgo

@rozgo rozgo commented Jul 2, 2025

Copy link
Copy Markdown
Contributor

Adds spatial indexing capabilities for grid-based Monte Carlo simulations.

New Features

SpatialGridPlugin

  • HashMap-based spatial indexing for O(1) neighbor lookups
  • Distance queries within Manhattan distance radius
  • Automatic entity tracking with ECS integration
  • Optional grid boundaries with bounds validation

SimulationBuilder Integration

let simulation = SimulationBuilder::new()
    .add_spatial_grid(bounds)
    .add_systems(movement_system)
    .build();

Bevy Math Integration

  • GridPosition wraps IVec2
  • GridBounds wraps IRect with grid cell semantics
  • Deref/DerefMut access to underlying Bevy types

Examples

Forest Fire Simulation

  • Cellular automaton with probabilistic fire spread
  • Moore/Von Neumann neighborhood calculations
  • Fire statistics time series collection

Pandemic Simulation

  • Spatial infection modeling with configurable radius
  • Contact tracing and quarantine systems
  • Social distancing behavior modeling

Implementation Details

  • O(1) spatial queries using HashMap indexing
  • No allocations for neighbor calculations
  • Built on Bevy's types for ecosystem compatibility
  • 10 test cases covering core functionality

@haath haath left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff, thanks a lot!

Only one point I'd like to discuss: what do you think about making the dimensionality generic so that it works both for 2D and 3D grids?
It seems most of the code would be the same, and we would get the extra dimension practically for free.

The way I'm thinking it SpatialGrid would be generic over a sealed trait which we would implement only for IVec2 and IVec3.

Then we would provide shorthand types for the user like this:

type SpatialGrid2D = SpatialGrid<IVec2>;
type SpatialGrid3D = SpatialGrid<IVec3>;

Let me know if you'd like to take on this effort yourself.
No worries if you'd rather not spend more time on this, I could also do it in a follow-up change.

Comment thread README.md Outdated
Comment thread examples/README.md Outdated
Comment thread examples/forest_fire.rs Outdated
Comment thread examples/forest_fire.rs
Comment thread examples/forest_fire.rs Outdated
Comment thread src/plugins/spatial_grid.rs Outdated
Comment thread src/plugins/spatial_grid.rs Outdated
Comment thread src/plugins/spatial_grid.rs Outdated
Comment thread src/plugins/spatial_grid.rs Outdated
Comment thread src/plugins/spatial_grid.rs Outdated
Comment thread src/plugins/spatial_grid.rs Outdated
@rozgo rozgo marked this pull request as draft July 2, 2025 18:40
@rozgo

rozgo commented Jul 3, 2025

Copy link
Copy Markdown
Contributor Author

Ok, lots of feedback, appreciate you taking the time to go through this. I think I covered all of them. Was trying this repo out of curiosity, but really liking how you structured this and contained the bevy complexities while making good use of bevys strength.

Let me know if I missed anything. There are many projects I want to do with a system like this, so its in my best interest to help out with anything I can.

@rozgo rozgo marked this pull request as ready for review July 3, 2025 03:35
@rozgo

rozgo commented Jul 3, 2025

Copy link
Copy Markdown
Contributor Author

Added a multi grid example:

    let mut simulation = SimulationBuilder::new()
        // Create separate spatial grids for each entity type
        .add_spatial_grid::<IVec2, Prey>(bounds)
        .add_spatial_grid::<IVec2, Predator>(bounds)
        .add_spatial_grid::<IVec2, Vegetation>(bounds)
        .add_entity_spawner(spawn_ecosystem)
        .add_systems((
            prey_behavior,
            predator_hunting.after(prey_behavior),
            vegetation_dynamics.after(predator_hunting),
            prey_feeding.after(vegetation_dynamics),
            lifecycle_system.after(prey_feeding),
            display_ecosystem_stats.after(lifecycle_system),
        ))
        .build();

@rozgo

rozgo commented Jul 4, 2025

Copy link
Copy Markdown
Contributor Author

Tests and examples are updated and clippy fixed. Ecosystem removed.

@rozgo rozgo force-pushed the feature/spatial-grid branch from 5f8d2ac to 8fe1a67 Compare July 4, 2025 21:05
@rozgo

rozgo commented Jul 4, 2025

Copy link
Copy Markdown
Contributor Author

rebased on main

@rozgo

rozgo commented Jul 4, 2025

Copy link
Copy Markdown
Contributor Author

Also, do not feel pressured to accept this PR. Already getting a good learning experience out of the process.

@haath haath left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some last points and it should be good to go!

Comment thread src/lib.rs Outdated

mod error;
mod plugins;
pub mod plugins;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't expose the plugins module.
Instead we do pub use only for the types the user might need to reference (GridPosition, SpatialGrid etc).

Comment thread src/plugins/spatial_grid.rs Outdated
{
for (entity, position) in &query
{
spatial_grid.remove(entity);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry my bad, I didn't see that insert() calls remove() on its own.
Can remove this line.

Comment thread src/plugins/spatial_grid.rs Outdated
/// Returns the position where the entity was located, if it was found.
pub(crate) fn remove(&mut self, entity: Entity) -> Option<GridPosition<T>>
{
if let Some(position) = self.entity_to_position.remove(&entity)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's handle the error case as well.

let position = self.entity_to_position.remove(&entity)?;

let Some(entities_at_position) = self.position_to_entities.get_mut(&position)
else
{
    panic!("entity found in one hashmap but not the other?");
};

entities_at_position.remove(&entity);
if entities_at_position.is_empty()
{
    self.position_to_entities.remove(&position);
}
Some(position)

Comment thread src/plugins/spatial_grid.rs Outdated
}

/// Clear all entities from the spatial index.
pub fn clear(&mut self)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be private.
Same for insert and remove actually, I think pub(crate) is not necessary.

Comment thread src/plugins/time_series.rs Outdated
{
// only get new samples once every 'sample_interval' steps
if step_counter.is_multiple_of(time_series.sample_interval)
if (**step_counter).is_multiple_of(time_series.sample_interval)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicit deref with ** is not needed here.

@haath haath merged commit adcad19 into haath:main Jul 7, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants