diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..897cc1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +*.pdb +*.log +.DS_Store +.vscode/ diff --git a/Cargo.lock b/Cargo.lock index 5398662..3f63213 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -194,6 +194,8 @@ dependencies = [ "bevy_pancam", "kd-tree", "rand", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 70bb79d..9ccb67c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,5 @@ rand = "0.8.5" bevy_pancam = "0.9.0" bevy_egui = "0.21.0" kd-tree = "0.5.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/README.md b/README.md index f1f2a3c..7a67181 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,81 @@ -# Ant Colony Simulation -This is an ant colony simulation, it internally uses kdtree and query caching, it's able to handle about 5k ants on the cpu. +# Rust Ant Colony Simulation +A high-performance ant colony simulation written in Rust using the [Bevy](https://bevyengine.org/) engine. This simulation demonstrates emergent behavior where ants find optimal paths between their nest and food sources using pheromone trails. -Built with [Rust](https://www.rust-lang.org/) and [Bevy](https://bevyengine.org/) game engine +![screenshot](assets/demo.png) -![screenshot](/screenshot.png) +## Features -# Demo -Here's the entire timelapse of the AI learning to drive +- **High Performance**: Capable of simulating thousands of ants efficiently using spatial hashing and KD-Trees. +- **Emergent Behavior**: Ants follow simple local rules to form complex global pathfinding networks. +- **Smart Navigation**: Ants utilize a multi-sensor array including a rear sensor to climb pheromone gradients accurately to find the source. +- **Real-time Tuning**: Adjust simulation parameters on the fly without restarting. +- **Interactive UI**: Control visuals and simulation settings via a GUI. -[![youtube](https://img.youtube.com/vi/98pUSZAM_7M/0.jpg)](https://youtu.be/98pUSZAM_7M) +## How to Run (Windows) -# Timelapses with Approach 1 -[![youtube](https://img.youtube.com/vi/5xdfTJBMnwI/0.jpg)](https://youtu.be/5xdfTJBMnwI) +**Recommended Method:** +Use the provided PowerShell script to run the project. this avoids common "file in use" / locking errors during compilation on Windows by building in a temporary directory. - -## Usage -- Clone the repo -```bash -git clone git@github.com:bones-ai/rust-ants-colony-simulation.git -cd rust-ants-colony-simulation +```powershell +.\run_safe.ps1 ``` -- Run the simulation + +**Standard Method:** ```bash cargo run --release ``` +*Note: If you encounter linking errors or "file used by another process" errors, please use the `run_safe.ps1` script.* + +## Controls & Shortcuts + +### Keyboard Shortcuts +| Key | Action | +| --- | --- | +| **Space** | Pause/Resume Simulation | +| **TAB** | Toggle Settings Menu (Open/Close UI) | +| **H** | Toggle Home Pheromone Visibility | +| **F** | Toggle Food Pheromone Visibility | +| **P** | Toggle Debug Paths (Sensor lines & Radius) | +| **A** | Toggle Ant Visibility | +| **-** | Reduce Speed (Limit FPS: 60 -> 30) | +| **=** | Increase Speed (Unlimited FPS) | +| **ESC**| Exit Simulation | + +### Mouse Controls +- **Left Click**: Use selected Editor Tool (Draw Wall, Place Food, etc.) +- **Right / Middle Click + Drag**: Pan Camera + +### UI Parameters (Press TAB) +You can tweak these values in real-time to see how they affect the colony's behavior: + +- **Env Ph Decay**: How fast pheromones on the ground evaporate. +- **Ant Ph Decay**: How fast the pheromone strength carried by an ant decays. +- **Sensor Dist**: How far ahead an ant looks for pheromones. +- **Sensor Angle**: The width of the ant's sensing field. +- **Randomness**: The amount of random jitter in ant movement. +- **Update Interval**: How often ants make steering decisions. +- **Max Pheromone**: Cap for pheromone accumulation. Determines when trails turn "Deep Red/Blue". + +### Map Editor (Toolbar at Bottom) +The simulation includes a built-in map editor to interact with the environment: +- **Draw Wall**: Paint obstacles that ants must navigate around. Supports continuous drawing. +- **Eraser**: Remove walls. Supports continuous erasing. +- **Place Food**: Click to spawn a food source. Each food source contains 1000 units of food. +- **Remove Food**: Click near a food source to remove it. + +**Note:** The simulation starts with **No Food**. You must place food using the editor to start the foraging process. + +### Reset +- **Reset Simulation**: Clears the map, respawns ants, and resets pheromones. Note that placed food and walls persist (or cleared? Check reset logic). + +## Configuration +The initial static configuration constants are located in `src/configs.rs`. However, many of these can now be overridden at runtime via the UI. +Settings can be saved to `user_config.json` via the "Save Config" button and are automatically loaded on startup. + +## Assets +Original assets located in `assets/`. +processed sprites sheets are in `assets/processed/`. -## Configurations -- The project config file is located at `src/configs.rs` -- If all ants aren't forming a single trail even after a long time, try increasing `ANT_INITIAL_PH_STRENGTH` in the configs to a greater value (exmaple: `40.0`) +## License +MIT diff --git a/assets/ant.png b/assets/ant.png deleted file mode 100644 index 1673e51..0000000 Binary files a/assets/ant.png and /dev/null differ diff --git a/assets/ant_with_food.png b/assets/ant_with_food.png deleted file mode 100644 index d8e9bce..0000000 Binary files a/assets/ant_with_food.png and /dev/null differ diff --git a/assets/demo.png b/assets/demo.png new file mode 100644 index 0000000..bc34954 Binary files /dev/null and b/assets/demo.png differ diff --git a/assets/food.png b/assets/food.png index 56e3cfd..1ec2867 100644 Binary files a/assets/food.png and b/assets/food.png differ diff --git a/assets/nest.png b/assets/nest.png index d5ab8d0..e43c664 100644 Binary files a/assets/nest.png and b/assets/nest.png differ diff --git a/assets/processed/ant_sheet_centered.png b/assets/processed/ant_sheet_centered.png new file mode 100644 index 0000000..ca87b5b Binary files /dev/null and b/assets/processed/ant_sheet_centered.png differ diff --git a/assets/processed/ant_with_food_sheet_centered.png b/assets/processed/ant_with_food_sheet_centered.png new file mode 100644 index 0000000..421070c Binary files /dev/null and b/assets/processed/ant_with_food_sheet_centered.png differ diff --git a/run_safe.ps1 b/run_safe.ps1 new file mode 100644 index 0000000..2fa657d --- /dev/null +++ b/run_safe.ps1 @@ -0,0 +1,3 @@ +$env:CARGO_TARGET_DIR = "$env:TEMP\rust-ants-colony-target" +Write-Host "Building in temporary directory: $env:CARGO_TARGET_DIR" +cargo run --release diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 8c6f2e9..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/src/ant.rs b/src/ant.rs index b2d123c..e5be2bb 100644 --- a/src/ant.rs +++ b/src/ant.rs @@ -1,5 +1,5 @@ use crate::{ - gui::SimStatistics, + gui::{ResetSimEvent, SimConfig, SimStatistics}, pheromone::Pheromones, utils::{calc_rotation_angle, get_rand_unit_vec2}, *, @@ -35,6 +35,23 @@ struct AntScanRadius(f32); #[derive(Resource)] pub struct AntFollowCameraPos(pub Vec2); +#[derive(Component)] +struct AnimationTimer(Timer); + +#[derive(Resource)] +pub struct AntAnimations { + pub walk: Handle, + pub walk_food: Handle, +} + +#[derive(Component)] +pub struct DecisionTimer(pub f32); + +#[derive(Component)] +pub struct Food { + pub storage: i32, +} + impl Plugin for AntPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup) @@ -42,23 +59,18 @@ impl Plugin for AntPlugin { .insert_resource(AntFollowCameraPos(Vec2::ZERO)) .add_systems( Update, - drop_pheromone.run_if(on_timer(Duration::from_secs_f32(ANT_PH_DROP_INTERVAL))), - ) - .add_systems( - Update, - check_wall_collision.run_if(on_timer(Duration::from_secs_f32(0.1))), - ) - .add_systems( - Update, - check_home_food_collisions.run_if(on_timer(Duration::from_secs_f32(0.1))), + ( + drop_pheromone.run_if(on_timer(Duration::from_secs_f32(ANT_PH_DROP_INTERVAL))), + avoid_obstacles, + check_wall_collision.after(avoid_obstacles), + check_home_food_collisions.run_if(on_timer(Duration::from_secs_f32(0.1))), + periodic_direction_update, + decay_ph_strength.run_if(on_timer(Duration::from_secs_f32(ANT_PH_STRENGTH_DECAY_INTERVAL))), + update_position.after(check_wall_collision), + animate_ant, + ).run_if(run_if_not_paused) ) .add_systems(Update, update_camera_follow_pos) - .add_systems( - Update, - periodic_direction_update.run_if(on_timer(Duration::from_secs_f32( - ANT_DIRECTION_UPDATE_INTERVAL, - ))), - ) .add_systems( Update, update_stats.run_if(on_timer(Duration::from_secs_f32(3.0))), @@ -67,49 +79,131 @@ impl Plugin for AntPlugin { Update, update_scan_radius.run_if(on_timer(Duration::from_secs_f32(1.0))), ) - .add_systems( - Update, - decay_ph_strength.run_if(on_timer(Duration::from_secs_f32( - ANT_PH_STRENGTH_DECAY_INTERVAL, - ))), - ) - .add_systems(Update, update_position.after(check_wall_collision)); + .add_systems(Update, debug_sensors) + .add_systems(Update, update_ant_count) + .add_systems(Update, reset_ants); } } -fn setup(mut commands: Commands, asset_server: Res) { +fn run_if_not_paused(settings: Res) -> bool { + !settings.is_paused +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut texture_atlases: ResMut>, +) { + let texture_handle = asset_server.load(SPRITE_ANT_SHEET); + let texture_atlas = TextureAtlas::from_grid( + texture_handle, + Vec2::new(512.0, 512.0), + ANT_SHEET_COLS, + ANT_SHEET_ROWS, + None, + None, + ); + let walk_handle = texture_atlases.add(texture_atlas); + + let texture_handle_food = asset_server.load(SPRITE_ANT_WITH_FOOD_SHEET); + let texture_atlas_food = TextureAtlas::from_grid( + texture_handle_food, + Vec2::new(512.0, 512.0), + ANT_SHEET_COLS, + ANT_SHEET_ROWS, + None, + None, + ); + let walk_food_handle = texture_atlases.add(texture_atlas_food); + + commands.insert_resource(AntAnimations { + walk: walk_handle.clone(), + walk_food: walk_food_handle, + }); + for _ in 0..NUM_ANTS { - commands.spawn(( - SpriteBundle { - texture: asset_server.load(SPRITE_ANT), - sprite: Sprite { - color: Color::rgb(1.1, 1.1, 1.0), - ..default() - }, - transform: Transform::from_xyz(HOME_LOCATION.0, HOME_LOCATION.1, ANT_Z_INDEX) - .with_scale(Vec3::splat(ANT_SPRITE_SCALE)), - ..Default::default() - }, - Ant, - CurrentTask(AntTask::FindFood), - Velocity(get_rand_unit_vec2()), - Acceleration(Vec2::ZERO), - PhStrength(ANT_INITIAL_PH_STRENGTH), - )); + spawn_ant(&mut commands, &walk_handle); + } +} + +fn spawn_ant(commands: &mut Commands, texture: &Handle) { + commands.spawn(( + SpriteSheetBundle { + texture_atlas: texture.clone(), + sprite: TextureAtlasSprite::new(0), + transform: Transform::from_xyz(HOME_LOCATION.0, HOME_LOCATION.1, ANT_Z_INDEX) + .with_scale(Vec3::splat(ANT_SPRITE_SCALE)), + ..Default::default() + }, + Ant, + CurrentTask(AntTask::FindFood), + Velocity(get_rand_unit_vec2()), + Acceleration(Vec2::ZERO), + PhStrength(ANT_INITIAL_PH_STRENGTH), + AnimationTimer(Timer::from_seconds(ANT_ANIMATION_SPEED, TimerMode::Repeating)), + DecisionTimer(thread_rng().gen_range(0.0..0.1)), + )); +} + +fn reset_ants( + mut commands: Commands, + mut events: EventReader, + ant_query: Query>, + ant_animations: Res, + config: Res, +) { + for _ in events.iter() { + // Despawn all ants + for entity in ant_query.iter() { + commands.entity(entity).despawn(); + } + + // Spawn new ants + for _ in 0..config.ants_count { + spawn_ant(&mut commands, &ant_animations.walk); + } + } +} + +fn update_ant_count( + mut commands: Commands, + ant_query: Query>, + config: Res, + ant_animations: Res, +) { + if !config.is_changed() { + return; + } + + let current_count = ant_query.iter().len(); + let target_count = config.ants_count; + + if current_count < target_count { + let diff = target_count - current_count; + for _ in 0..diff { + spawn_ant(&mut commands, &ant_animations.walk); + } + } else if current_count > target_count { + let diff = current_count - target_count; + let ants_to_despawn = ant_query.iter().take(diff); + for entity in ants_to_despawn { + commands.entity(entity).despawn(); + } } } fn drop_pheromone( mut ant_query: Query<(&Transform, &CurrentTask, &PhStrength), With>, mut pheromones: ResMut, + config: Res, ) { for (transform, ant_task, ph_strength) in ant_query.iter_mut() { let x = transform.translation.x as i32; let y = transform.translation.y as i32; match ant_task.0 { - AntTask::FindFood => pheromones.to_home.emit_signal(&(x, y), ph_strength.0), - AntTask::FindHome => pheromones.to_food.emit_signal(&(x, y), ph_strength.0), + AntTask::FindFood => pheromones.to_home.emit_signal(&(x, y), ph_strength.0, config.max_pheromone_strength), + AntTask::FindHome => pheromones.to_food.emit_signal(&(x, y), ph_strength.0, config.max_pheromone_strength), } } } @@ -140,72 +234,122 @@ fn update_stats( stats.num_ants = ant_query.iter().len(); } -fn decay_ph_strength(mut ant_query: Query<&mut PhStrength, With>) { +fn decay_ph_strength(mut ant_query: Query<&mut PhStrength, With>, config: Res) { for mut ph_strength in ant_query.iter_mut() { - ph_strength.0 = f32::max(ph_strength.0 - ANT_PH_STRENGTH_DECAY_RATE, 0.0); + ph_strength.0 = f32::max(ph_strength.0 - config.ant_ph_strength_decay_rate, 0.0); } } fn get_steering_force(target: Vec2, current: Vec2, velocity: Vec2) -> Vec2 { let desired = target - current; let steering = desired - velocity; - steering * 0.05 + steering * 0.2 } fn periodic_direction_update( - mut ant_query: Query<(&mut Acceleration, &Transform, &CurrentTask, &Velocity), With>, + mut ant_query: Query<(&mut Acceleration, &Transform, &CurrentTask, &Velocity, &mut DecisionTimer), With>, mut pheromones: ResMut, mut stats: ResMut, - scan_radius: Res, + _scan_radius: Res, + config: Res, + time: Res