21 Commits

Author SHA1 Message Date
df86f05876 Mark v0.2.0, update lockfile 2025-09-03 14:46:25 -05:00
68d6de9a83 Add a WASM build configuration
The config.toml lets me run `cargo run --target wasm32-unknown-unknown`
and host a dev server.

I've adjusted the Cargo.toml to use the kdtree_rayon feature by default,
but disable the Rayon feature for WASM builds (because it doesn't work).
2025-09-03 14:41:53 -05:00
ccd8a12b79 Autoformat 2025-09-03 14:27:28 -05:00
f2a71e712a Put the boid separation range back to 1/4 not 1/8
With the 1.0 radius boids (instead of whatever `Circle::default()`
uses), a separation activation of 1/4 the range looks more visually
appealing (imo).
2025-09-03 14:25:16 -05:00
dca8bcdaa7 Add speed_controller to maintain system energy
The starting velocities going outwards in a circle means that the sum of
energy in the system is 0. As the boids come back together, they will
slow down to fall into a lattice. This is interesting, but not the
flocking behavior Boids are supposed to have.

I've replaced the `space_brakes` function with this new speed controller
to keep boids moving between two speeds. Anything too slow will speed up
along it's current vector, and anything too fast will slow down.
2025-09-03 14:20:17 -05:00
e6e56506f8 Fix: alignment should align to vel, not impulse
I need to apply an impulse to match the velocities, but the previous
version was trying to match the forces.
2025-09-03 14:11:18 -05:00
bc0630b4ae Fix: off-by-one error in alignment averaging
The "length" is actually the `enumerate()` index, which is one less than
the item count. The previous version was not using the average, and may
have been deviding by 0 for boids with exactly one neighbor.
2025-09-03 14:09:40 -05:00
76a6b4f999 New cohesion rule function 2025-09-03 14:07:50 -05:00
451311126d New separation rule function 2025-09-03 14:07:34 -05:00
7b380196a5 Filter-map the entities to avoid self-alignment
The boids shouldn't try to align with themselves. That doesn't really
make any sense.
2025-09-03 12:23:53 -05:00
8b61d38636 Make keyboard control forces, not velocities
The keyboard input should apply a force, not modify the velocity. Right
now, there is no Mass component, but in the future there might be. I've
just fixed a broken physics integrator made by bad assumptions, too, so
I'm goig to do this for consistency if nothing else.
2025-09-03 12:23:53 -05:00
b3cf47e684 Remove BoidBundle, use required-components instead 2025-09-03 12:23:53 -05:00
69403bc6ca Fix the physics integrator
The velocity component never got updated. Instead, the system was only
calculating the current frame's deltaV. That instantaneous dV and any
velocity that *did* get assigned (bundle insertion, keyboard control)
would be added to the position.

Now forces are integrated into velocity, and velocity is integrated into
position. You know... like a real integration function.
2025-09-03 12:23:53 -05:00
a6240c844a Make another new alignment system 2025-09-03 12:23:53 -05:00
3eb23fb4bf Move physics parts to a physics module 2025-09-03 12:23:53 -05:00
532025b42f Bump Rust edition to 2024 2025-08-30 15:45:20 -05:00
4150f85ccc Rename & restructure birdoids module
The module isn't the plugin, so it's going to be called simply
"birdoids" going forward.

I've turned it into a folder and a `mod.rs` so I can slap down a small,
custom physics system.
2025-08-30 15:39:41 -05:00
214da65db5 Switch off deprecated functions, fix clippy lints 2025-08-30 15:39:41 -05:00
325e515a7b Autoformat 2025-08-30 15:39:41 -05:00
e85114c4c8 Adjust code for Bevy 0.14 -> 0.16 upgrade
Now the program builds again.
2025-08-30 15:39:41 -05:00
feeeb15d22 Begin v0.2.0-dev and upgrade Bevy version
Resuming work on this project. New crate version, new Bevy version.
2025-08-30 15:39:41 -05:00
7 changed files with 2043 additions and 1118 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[target.wasm32-unknown-unknown]
runner = "wasm-server-runner"

2838
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
[package] [package]
name = "another-boids-in-rust" name = "another-boids-in-rust"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
bevy = "0.14.0" bevy = "0.16.0"
bevy_spatial = "0.9.0"
# Grand-dependency pins # Grand-dependency pins
ab_glyph = "0.2.16" ab_glyph = "0.2.16"
@@ -17,6 +16,16 @@ miniz-sys = "0.1.10"
nonmax = "0.5.1" nonmax = "0.5.1"
rand = "0.8.0" rand = "0.8.0"
# Use regular bevy_spatial on non-WASM builds
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bevy_spatial = "0.11.0"
# Use bevy_spatial *without* the kdtree_rayon feature when building for WASM.
[target.'cfg(target_arch = "wasm32")'.dependencies.bevy_spatial]
version = "0.11.0"
default-features = false
features = ["kdtree"]
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

View File

@@ -1,16 +1,22 @@
use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; pub mod physics;
use bevy::prelude::*;
use bevy_spatial::{ use bevy_spatial::{
kdtree::KDTree2, AutomaticUpdate, SpatialAccess, SpatialStructure, TransformMode, AutomaticUpdate, SpatialAccess, SpatialStructure, TransformMode, kdtree::KDTree2,
}; };
use crate::birdoids::physics::{Force, Velocity, apply_velocity};
const BACKGROUND_COLOR: Color = Color::srgb(0.4, 0.4, 0.4); const BACKGROUND_COLOR: Color = Color::srgb(0.4, 0.4, 0.4);
const PLAYERBOID_COLOR: Color = Color::srgb(1.0, 0.0, 0.0); const PLAYERBOID_COLOR: Color = Color::srgb(1.0, 0.0, 0.0);
const TURN_FACTOR: f32 = 1.0; const TURN_FACTOR: f32 = 1.0;
const BOID_VIEW_RANGE: f32 = 50.0; const BOID_VIEW_RANGE: f32 = 15.0;
const COHESION_FACTOR: f32 = 1.0; const COHESION_FACTOR: f32 = 1.0;
const SEPARATION_FACTOR: f32 = 1.0; const SEPARATION_FACTOR: f32 = 10.0;
const ALIGNMENT_FACTOR: f32 = 1.0; const ALIGNMENT_FACTOR: f32 = 1.0;
const SPACEBRAKES_COEFFICIENT: f32 = 0.01; const SPACEBRAKES_COEFFICIENT: f32 = 0.5;
const LOW_SPEED_THRESHOLD: f32 = 50.0;
const HIGH_SPEED_THRESHOLD: f32 = 200.0;
pub struct BoidsPlugin; pub struct BoidsPlugin;
@@ -33,13 +39,14 @@ impl Plugin for BoidsPlugin {
cohesion, cohesion,
separation, separation,
alignment, alignment,
// space_brakes, speed_controller,
), ),
); );
} }
} }
#[derive(Component)] #[derive(Component)]
#[require(Velocity, Force, TrackedByKdTree)]
pub(crate) struct Boid; pub(crate) struct Boid;
// It's a Boid, but with an extra component so the player // It's a Boid, but with an extra component so the player
@@ -47,36 +54,11 @@ pub(crate) struct Boid;
#[derive(Component)] #[derive(Component)]
struct PlayerBoid; struct PlayerBoid;
#[derive(Component, Deref, DerefMut)] #[derive(Component, Default)]
pub(crate) struct Velocity(Vec3); pub struct TrackedByKdTree;
#[derive(Component, Default, Deref, DerefMut, PartialEq, Debug)]
pub(crate) struct Force(Vec3);
#[derive(Component)]
pub(crate) struct TrackedByKdTree;
#[derive(Bundle)]
struct BoidBundle {
boid: Boid,
velocity: Velocity,
accel: Force,
spatial: TrackedByKdTree,
}
impl BoidBundle {
fn new(vel: Vec3) -> Self {
Self {
boid: Boid,
velocity: Velocity(vel),
accel: Force(Vec3::ZERO),
spatial: TrackedByKdTree,
}
}
}
fn spawn_camera(mut commands: Commands) { fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2d);
} }
fn spawn_boids( fn spawn_boids(
@@ -89,34 +71,32 @@ fn spawn_boids(
let frac = 2.0 * std::f32::consts::PI / (num_boids as f32) * (i as f32); let frac = 2.0 * std::f32::consts::PI / (num_boids as f32) * (i as f32);
let vel = Vec3::new(frac.cos() * 1.0, frac.sin() * 1.0, 0.0) * 10.0; let vel = Vec3::new(frac.cos() * 1.0, frac.sin() * 1.0, 0.0) * 10.0;
commands.spawn(( commands.spawn((
BoidBundle::new(vel), Boid,
MaterialMesh2dBundle { Velocity(vel),
mesh: meshes.add(Circle::default()).into(), Mesh2d(meshes.add(Circle::new(1.0))),
material: materials.add(Color::srgb(1.0, 1.0, 1.0)), MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 1.0))),
transform: Transform { Transform::from_translation(vel * 20.0),
translation: vel * 20.0,
..default()
},
..default()
},
)); ));
} }
commands.spawn(( commands.spawn((
BoidBundle::new(Vec3::new(0.0, 0.0, 0.0)), Boid,
PlayerBoid, PlayerBoid,
MaterialMesh2dBundle { Mesh2d(meshes.add(Triangle2d::default())),
mesh: meshes.add(Triangle2d::default()).into(), MeshMaterial2d(materials.add(PLAYERBOID_COLOR)),
material: materials.add(PLAYERBOID_COLOR),
..default()
},
)); ));
} }
fn space_brakes(mut mobs: Query<&mut Force, With<Boid>>) { /// Controls the boid's minimum and maximum speed according to a low- and
for mut accel in &mut mobs { /// high-threshold. Boids moving too slow are sped up, and boids moving too
let braking_dir = -accel.0 * SPACEBRAKES_COEFFICIENT; /// fast are slowed down.
accel.0 += braking_dir; fn speed_controller(mut mobs: Query<(&Velocity, &mut Force), With<Boid>>) {
for (vel, mut impulse) in &mut mobs {
if vel.0.length() < LOW_SPEED_THRESHOLD {
impulse.0 += vel.0 * SPACEBRAKES_COEFFICIENT;
} else if vel.0.length() > HIGH_SPEED_THRESHOLD {
impulse.0 += -vel.0 * SPACEBRAKES_COEFFICIENT;
}
} }
} }
@@ -124,7 +104,7 @@ fn turn_if_edge(
mut query: Query<(&mut Transform, &mut Velocity), With<Boid>>, mut query: Query<(&mut Transform, &mut Velocity), With<Boid>>,
window: Query<&Window>, window: Query<&Window>,
) { ) {
if let Ok(window) = window.get_single() { if let Ok(window) = window.single() {
let (width, height) = (window.resolution.width(), window.resolution.height()); let (width, height) = (window.resolution.width(), window.resolution.height());
for (transform, mut velocity) in &mut query { for (transform, mut velocity) in &mut query {
let boid_pos = transform.translation.xy(); let boid_pos = transform.translation.xy();
@@ -145,25 +125,18 @@ fn turn_if_edge(
} }
} }
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity, &mut Force)>, time: Res<Time>) {
for (mut transform, velocity, mut acceleration) in &mut query {
let delta_v = **acceleration * time.delta_seconds();
**acceleration = Vec3::ZERO;
let delta_position = (**velocity + delta_v) * time.delta_seconds();
transform.translation += delta_position;
}
}
fn check_keyboard( fn check_keyboard(
keyboard_input: Res<ButtonInput<KeyCode>>, keyboard_input: Res<ButtonInput<KeyCode>>,
mut app_exit_events: ResMut<Events<bevy::app::AppExit>>, mut app_exit_events: ResMut<Events<bevy::app::AppExit>>,
mut query: Query<&mut Velocity, With<PlayerBoid>>, mut query: Query<&mut Force, With<PlayerBoid>>,
) { ) {
if keyboard_input.just_pressed(KeyCode::KeyQ) { if keyboard_input.just_pressed(KeyCode::KeyQ) {
app_exit_events.send(bevy::app::AppExit::Success); app_exit_events.send(bevy::app::AppExit::Success);
} }
let mut pvelocity = query.single_mut(); let mut impulse = query
.single_mut()
.expect("[birdoids_plugin::check_keyboard()] ->> There seems to be more than one player... How did that happen?");
let mut dir = Vec2::ZERO; let mut dir = Vec2::ZERO;
if keyboard_input.pressed(KeyCode::ArrowLeft) { if keyboard_input.pressed(KeyCode::ArrowLeft) {
dir.x -= 1.0; dir.x -= 1.0;
@@ -178,53 +151,78 @@ fn check_keyboard(
dir.y += 1.0; dir.y += 1.0;
} }
**pvelocity = **pvelocity + dir.extend(0.0); **impulse += dir.extend(0.0) * 50.0;
} }
fn cohesion( fn cohesion(
spatial_tree: Res<KDTree2<TrackedByKdTree>>, spatial_tree: Res<KDTree2<TrackedByKdTree>>,
mut boids: Query<(&Transform, &mut Force), With<Boid>>, mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
) { ) {
// for each boid // for each boid
// find neighbors // find neighbors
// find center-of-mass of neighbors // find center-of-mass of neighbors
// find vector from boid to flock CoM // find vector from boid to flock CoM
// apply force // apply force
for (transform, mut acceleration) in &mut boids { for (this_entt, transform, mut force) in &mut boids {
let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE); let (len, sum) = spatial_tree
if let Some(center_mass) = center_of_boids(neighbors.iter().map(|boid| boid.0)) { .within_distance(transform.translation.xy(), BOID_VIEW_RANGE)
let force = cohesive_force(center_mass, transform.translation.xy()).unwrap_or_default(); .iter()
acceleration.0 += *force * COHESION_FACTOR; .filter_map(|(pos, entt)| {
} // Skip self-comparison. A boid should not try to separate from itself.
let entt = entt
.expect("within_distance gave me an entity... with no entity ID... somehow");
if this_entt == entt { None } else { Some(pos) }
})
.enumerate()
.fold((0, Vec2::ZERO), |(_len, com), (idx, pos)| (idx, com + pos));
// Skip to next boid if the current one has no neighbors.
let center_of_mass = if len > 0 {
sum / ((len + 1) as f32)
} else {
continue;
};
let impulse = cohesive_force(center_of_mass, transform.translation.xy()).expect("damn");
force.0 -= impulse.0 * COHESION_FACTOR;
} }
} }
fn separation( fn separation(
spatial_tree: Res<KDTree2<TrackedByKdTree>>, spatial_tree: Res<KDTree2<TrackedByKdTree>>,
mut boids: Query<(&Transform, &mut Force), With<Boid>>, mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
) { ) {
// for each boid // for each boid
// find neighbors // find neighbors
// sum force from neighbors // sum force from neighbors
// apply force // apply force
for (boid_transform, mut boid_acceleration) in &mut boids { for (this_entt, tsfm, mut force) in &mut boids {
let neighbors = let impulse = spatial_tree
spatial_tree.within_distance(boid_transform.translation.xy(), BOID_VIEW_RANGE / 4.0); .within_distance(tsfm.translation.xy(), BOID_VIEW_RANGE / 4.0)
let accel = neighbors.iter().map(|(pos, _)| pos.extend(0.0)).fold( .iter()
Vec3::ZERO, .filter_map(|(pos, entt)| {
|accumulator, neighbor| { // Skip self-comparison. A boid should not try to separate from itself.
let force = separation_force(boid_transform.translation.xy(), neighbor.xy()) let entt = entt
.unwrap_or_default(); .expect("within_distance gave me an entity... with no entity ID... somehow");
accumulator + *force * SEPARATION_FACTOR if this_entt == entt {
}, None
); } else {
boid_acceleration.0 += accel; Some(pos.extend(0.0))
}
})
.fold(Vec3::ZERO, |acc, other| {
// let force = tsfm.translation - other;
let force = separation_force(tsfm.translation.xy(), other.xy()).expect("angy");
acc + force.0
});
force.0 += impulse * SEPARATION_FACTOR;
} }
} }
fn alignment( fn alignment(
spatial_tree: Res<KDTree2<TrackedByKdTree>>, spatial_tree: Res<KDTree2<TrackedByKdTree>>,
mut boids: Query<(&Transform, &Velocity, &mut Force), With<Boid>>, mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
boid_velocities: Query<&Velocity, With<Boid>>, boid_velocities: Query<&Velocity, With<Boid>>,
) { ) {
// for each boid // for each boid
@@ -234,27 +232,36 @@ fn alignment(
// perpendicular so that magnitude is constant // perpendicular so that magnitude is constant
// apply steering force // apply steering force
for (transform, velocity, mut acceleration) in &mut boids { for (this_entt, transform, mut force) in &mut boids {
let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE); let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE);
// averaging divides by length. Guard against an empty set of neighbors // averaging divides by length. Guard against an empty set of neighbors
// so that we don't divide by zero. let (len, sum) = neighbors
if neighbors.len() > 0 { .iter()
if let Some(avg_velocity) = // Extract the velocities by `get()`ing from another query param.
velocity_of_boids(neighbors.iter().map(|(vel, opt_entity)| { .filter_map(|(_pos, maybe_entt)| {
// I've observed no panics in the old version, nor the debug_plugins version let entt = maybe_entt
// I'm not clear on the conditions that cause a None option, but I want to .expect("Neighbor boid has no Entity ID. I don't know what this means");
// crash when I find one. if this_entt == entt {
let entity_id = opt_entity.unwrap_or_else(|| panic!("Boid has no Entity ID!")); None
let vel = boid_velocities } else {
.get(entity_id) let vel = boid_velocities.get(entt).expect("Boid has no velocity!");
.unwrap_or_else(|_| panic!("Boid has no velocity!")); Some(vel.xy())
(*vel).xy()
}))
{
let deviation = -velocity.0 + avg_velocity.extend(0.0);
acceleration.0 += deviation * ALIGNMENT_FACTOR;
}
} }
})
.enumerate()
.fold((0, Vec2::ZERO), |(_len, vel_acc), (idx, vel)| {
(idx, vel_acc + vel)
});
// Skip to next boid if the current one has no neighbors.
let avg = if len > 0 {
sum / ((len + 1) as f32)
} else {
continue;
};
let boid_vel = boid_velocities.get(this_entt).unwrap();
force.0 += (avg.extend(0.0) - boid_vel.0) * ALIGNMENT_FACTOR;
} }
} }
@@ -267,18 +274,9 @@ pub(crate) fn velocity_of_boids(points: impl Iterator<Item = Vec2>) -> Option<Ve
} }
fn average_of_vec2s(points: impl Iterator<Item = Vec2>) -> Option<Vec2> { fn average_of_vec2s(points: impl Iterator<Item = Vec2>) -> Option<Vec2> {
// Average the points by summing them all together, and dividing by let (len, sum) = points
// the total count. .enumerate()
// Passing the points as an iterator means we lose the length of the .fold((0, Vec2::ZERO), |(_len, sum), (idx, point)| {
// list. The `.enumerate()` iterator reintroduces that count.
let mut points = points.enumerate();
// Empty iterators have no points and so no center of mass.
// Try to get the first one, but exit with None if it doesn't yield.
let init = points.next()?;
// if we get one, fold all the remaining values into it.
let (len, sum) = points.fold(init, |(len, sum), (idx, point)| {
// replace length with most recent index // replace length with most recent index
// add running sum & new point for new running sum // add running sum & new point for new running sum
(idx, sum + point) (idx, sum + point)
@@ -329,9 +327,9 @@ fn separation_force(boid: Vec2, target: Vec2) -> Option<Force> {
mod tests { mod tests {
use bevy::prelude::*; use bevy::prelude::*;
use crate::birdoids_plugin::{cohesive_force, separation_force}; use crate::birdoids::{cohesive_force, separation_force};
use super::{Force, BOID_VIEW_RANGE}; use super::{BOID_VIEW_RANGE, physics::Force};
// forces are relative to the boid's view range, so all // forces are relative to the boid's view range, so all
// distances need to be fractions of that // distances need to be fractions of that

23
src/birdoids/physics.rs Normal file
View File

@@ -0,0 +1,23 @@
use bevy::prelude::*;
#[derive(Component, Default, Deref, DerefMut)]
pub struct Velocity(pub Vec3);
#[derive(Component, Default, Deref, DerefMut, PartialEq, Debug)]
pub struct Force(pub Vec3);
pub fn apply_velocity(
mut query: Query<(&mut Transform, &mut Velocity, &mut Force)>,
time: Res<Time>,
) {
for (mut transform, mut velocity, mut force) in &mut query {
// integrate forces into new velocity (assume mass of 1kg)
let delta_v = **force * time.delta_secs();
velocity.0 += delta_v;
force.0 = Vec3::ZERO; // clear force value
// integrate velocity into position
let delta_p = **velocity * time.delta_secs();
transform.translation += delta_p;
}
}

View File

@@ -1,8 +1,8 @@
use bevy::{prelude::*, sprite::MaterialMesh2dBundle, window::PrimaryWindow}; use bevy::{prelude::*, window::PrimaryWindow};
use bevy_spatial::{kdtree::KDTree2, SpatialAccess}; use bevy_spatial::{SpatialAccess, kdtree::KDTree2};
use crate::birdoids_plugin::{ use crate::birdoids::{
center_of_boids, velocity_of_boids, Boid, Force, TrackedByKdTree, Velocity, Boid, TrackedByKdTree, center_of_boids, physics::Force, physics::Velocity, velocity_of_boids,
}; };
const SCANRADIUS: f32 = 50.0; const SCANRADIUS: f32 = 50.0;
@@ -22,13 +22,8 @@ fn setup(
) { ) {
commands.spawn(( commands.spawn((
ScannerWidget::default(), ScannerWidget::default(),
MaterialMesh2dBundle { Mesh2d(meshes.add(Annulus::new(SCANRADIUS - 1.0, SCANRADIUS))),
mesh: meshes MeshMaterial2d(materials.add(Color::srgb(0.0, 0.0, 0.0))),
.add(Annulus::new(SCANRADIUS - 1.0, SCANRADIUS))
.into(),
material: materials.add(Color::srgb(0.0, 0.0, 0.0)),
..default()
},
)); ));
} }
@@ -71,8 +66,8 @@ fn update_cursor(
) { ) {
// I'm trusting that only one thing has the `Cursor` component // I'm trusting that only one thing has the `Cursor` component
// It's defined here in this module, so that *should* be the case... // It's defined here in this module, so that *should* be the case...
let win = window.get_single().unwrap(); let win = window.single().unwrap();
let (cam, cam_transform) = camera.get_single().unwrap(); let (cam, cam_transform) = camera.single().unwrap();
// the cursor might not be on the window. Only adjust position when it is. // the cursor might not be on the window. Only adjust position when it is.
if let Some(cursor_pos_in_window) = win.cursor_position() { if let Some(cursor_pos_in_window) = win.cursor_position() {
// transform the window position into world space // transform the window position into world space
@@ -81,7 +76,7 @@ fn update_cursor(
let cursor_in_world = cam let cursor_in_world = cam
.viewport_to_world_2d(cam_transform, cursor_pos_in_window) .viewport_to_world_2d(cam_transform, cursor_pos_in_window)
.unwrap(); .unwrap();
let mut cursor = cursor_query.get_single_mut().unwrap(); let mut cursor = cursor_query.single_mut().unwrap();
cursor.translation = cursor_in_world.extend(0.0); cursor.translation = cursor_in_world.extend(0.0);
} }
} }
@@ -94,7 +89,7 @@ fn update_scanner_mode(
mut scanner_query: Query<(&mut SelectionMode, &mut ScannerMode), With<Cursor>>, mut scanner_query: Query<(&mut SelectionMode, &mut ScannerMode), With<Cursor>>,
) { ) {
// I'm making another assertion that there is exactly one scanner. // I'm making another assertion that there is exactly one scanner.
let (mut select_mode, mut scan_mode) = scanner_query.get_single_mut().unwrap(); let (mut select_mode, mut scan_mode) = scanner_query.single_mut().unwrap();
// Assign selection mode // Assign selection mode
if keycodes.just_pressed(KeyCode::Digit1) { if keycodes.just_pressed(KeyCode::Digit1) {
@@ -112,7 +107,7 @@ fn update_scanner_mode(
} }
fn print_gizmo_config(query: Query<(&SelectionMode, &ScannerMode), With<Cursor>>) { fn print_gizmo_config(query: Query<(&SelectionMode, &ScannerMode), With<Cursor>>) {
let (select, scan) = query.get_single().unwrap(); let (select, scan) = query.single().unwrap();
println!("Selection: {select:?}, Scanning: {scan:?}"); println!("Selection: {select:?}, Scanning: {scan:?}");
} }
@@ -123,7 +118,7 @@ fn do_scan(
/* Push info to summary somewhere */ /* Push info to summary somewhere */
mut gizmos: Gizmos, mut gizmos: Gizmos,
) { ) {
let (cursor_pos, select_mode, scan_mode) = scanner_query.get_single().unwrap(); let (cursor_pos, select_mode, scan_mode) = scanner_query.single().unwrap();
match select_mode { match select_mode {
SelectionMode::NearestSingle => todo!(), SelectionMode::NearestSingle => todo!(),
SelectionMode::CircularArea => { SelectionMode::CircularArea => {

View File

@@ -1,9 +1,9 @@
use bevy::prelude::*; use bevy::prelude::*;
mod birdoids_plugin; mod birdoids;
mod debug_plugin; mod debug_plugin;
use birdoids_plugin::BoidsPlugin; use birdoids::BoidsPlugin;
use debug_plugin::BoidsDebugPlugin; use debug_plugin::BoidsDebugPlugin;
fn main() { fn main() {