20 Commits

Author SHA1 Message Date
ac60482688 Release v0.9.0
Some checks failed
check / nightly / doc (push) Has been cancelled
check / ubuntu / stable / features (push) Has been cancelled
check / ubuntu / 1.88.0 (push) Has been cancelled
check / stable / fmt (push) Has been cancelled
Web builds now get their version text automatically from the Cargo.toml
2025-12-26 10:21:25 -06:00
75a675977a Fully remove old transitive dep pins 2025-12-26 10:18:21 -06:00
8f2ab97cf0 Improve comments on Makefile config vars
Updated comments explaining the purpose of the variables (to go with the
new "automatic vars" section).
2025-12-26 10:05:51 -06:00
e494fc6c35 Auto fill crate version in web page
The webpage has a placeholder string instead of a hard-coded version.
This gets replaced by the Makefile with the help of a pair of `sed`
calls.

And with that, I've upgraded my ad-hoc web bundler with an ad-hoc HTML
templating engine.
2025-12-26 10:01:39 -06:00
03c7d44aad Release v0.8.0
Some checks failed
check / nightly / doc (push) Has been cancelled
check / ubuntu / stable / features (push) Has been cancelled
check / ubuntu / 1.88.0 (push) Has been cancelled
check / stable / fmt (push) Has been cancelled
- Replaced "bevy_spatial" with "avian2d"
- Upgraded Bevy to 0.17

The bevy_spatial crate is still stuck on Bevy 0.16 and I'd like to move
forward and start playing with some of the new UI stuff. I've replaced
it with Avian physics, although I'm only using the spatial querying
facilities not the rest of the physics engine.
2025-12-25 23:47:14 -06:00
975f2a0b92 Install libwayland-dev in CI workflow
Some checks failed
check / nightly / doc (push) Has been cancelled
check / ubuntu / stable / features (push) Has been cancelled
check / ubuntu / 1.88.0 (push) Has been cancelled
check / stable / fmt (push) Has been cancelled
Bevy 0.17 enables Wayland by default so the CI workflow needs to have it
installed.
2025-12-25 23:27:33 -06:00
3a3b8181f9 Upgrade to Bevy 0.17
Avian physics has been updated for Bevy 0.17 while bevy_spatial has not.
Now that the program is switched over, I can finally pull in the new
versions of everything.
2025-12-25 23:17:14 -06:00
62feb6f313 Fix: separation() system needs smaller view range
I figured out why the new flocking behavior is different...

The previous version used 1/4th the view range for boid separation and I
didn't preserve that.
2025-12-25 22:58:03 -06:00
9797b90415 Remove dependency on crate "bevy_spatial" 2025-12-24 16:04:25 -06:00
1846b7065e Convert flocking systems to use Avian2d 2025-12-24 16:02:46 -06:00
e252e3385c (autoformat) 2025-12-24 15:44:54 -06:00
5f0b428811 Debug scanner uses shape_intersections
That was really easy. I feel dumb for making a checkpoint commit before.
Oh well, onwards to the next step: Swap over the flocking systems.
2025-12-24 15:01:03 -06:00
119d7acf09 WIP: Debug tool scans using Avian2d shape casting
Working demonstration, but I'm not sure shape casting is the action I
want to be doing. I've just found a "shape_intersections" method in the
docs, but I'm saving my progress before making further changes.
2025-12-24 14:54:49 -06:00
63f15ae6a7 Update MSRV to 1.88.0
One or more of the transitive dependencies needs Rust 1.88 or newer, so
this becomes our minimum version. It may be possible to build with an
older Rust by manually picking dependency versions but I'm not going to
do that.

Ideally, `-Zminimal-versions` would pick out those versions, but lots of
crates don't correctly specify minimum depdendency versions. As a
result, our transitive dependencies resolve to
matching-but-non-functional versions.
2025-12-24 13:05:05 -06:00
ba01d8137f Mark v0.7.0
Mark new version, update the lockfile, and autoformat the project.
2025-12-22 11:07:56 -06:00
b8c28529e6 Remove the Player Boid
The player-controlled boid was for testing the flocking behavior. It has
outlived this purpose and I never even bothered to make it larger like
the others. It's finally going away completely.
2025-12-22 10:56:47 -06:00
fd161dc26b Wire in the magic physics vars as a Resource
I don't know what to call these, so they're "misc. parameters" for now.
2025-12-22 10:45:02 -06:00
c2cf100c05 Flocking params as a resource & egui-inspector
The birdoid flocking parameters are now a resource named
`FlockingParameters`. Adjustments can be made using the Egui inspector
widget, although I plan to make a custom UI in the future.
2025-12-21 16:33:04 -06:00
0828518963 Begin 0.7.0-dev, small upgrade to behavior params
I'm not happy with the flocking behavior so I fiddled with the program
to find these new behavior parameters. It's annoying to constantly
rebuild the program, though, and I'd like for the end-user to be able to
fiddle with it at runtime. To that end, v0.7.0 shall be the UI update.
2025-12-21 15:31:08 -06:00
5ef91ec88c Remove unused print_gizmo_config() function 2025-12-20 09:42:14 -06:00
8 changed files with 2444 additions and 778 deletions

View File

@@ -45,7 +45,7 @@ jobs:
with:
submodules: true
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev libwayland-dev
- name: Install nightly
uses: dtolnay/rust-toolchain@nightly
- name: cargo doc
@@ -62,7 +62,7 @@ jobs:
with:
submodules: true
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev cmake
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev libwayland-dev cmake
- name: Install stable
uses: dtolnay/rust-toolchain@stable
- name: cargo install cargo-hack
@@ -78,14 +78,14 @@ jobs:
# https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
strategy:
matrix:
msrv: ["1.87.0"]
msrv: ["1.88.0"]
name: ubuntu / ${{ matrix.msrv }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev libwayland-dev
- name: Install ${{ matrix.msrv }}
uses: dtolnay/rust-toolchain@master
with:

2757
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,14 @@
[package]
name = "another-boids-in-rust"
version = "0.6.0"
version = "0.9.0"
edition = "2024"
license = "AGPL-3.0-only"
rust-version = "1.88.0"
[dependencies]
bevy = "0.16.0"
# Grand-dependency pins
# ab_glyph = "0.2.16"
# fnv = "1.0.6"
# gilrs = "0.10.5"
# lazy_static = "1.0.2"
# lock_api = "0.4.7"
# miniz-sys = "0.1.10"
# nonmax = "0.5.1"
# 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"]
avian2d = "0.4"
bevy = "0.17.0"
bevy-inspector-egui = "0.34"
[profile.dev]
opt-level = 1

View File

@@ -1,17 +1,28 @@
# This script produces a web build. If you aren't trying to do that, it is
# entirely useless to you.
# Patch these to select a different build profile or target
# The target shouldn't change any time soon. WASM64, I guess. Other targets
# aren't aimed at the web, so you shouldn't be using this makefile.
# # # Configuration Variables # # #
# (because I don't have a ./configure script)
#
# Patch these to select a different build profile or target.
# This Makefile will build a web bundle so using a non-wasm target is basically
# nonsense. Maybe WASM64, but I don't think that's useful (or stabilized yet).
CARGO_TARGET := wasm32-unknown-unknown
CARGO_PROFILE := wasm-release
# Override DESTDIR to set a custom install path (such as your web root)
# Override DESTDIR to set a custom install path
# E.g.: your web root
# I use it via the Debian package build process (dpkg needs it)
DESTDIR ?= .
# # # Automatic Variables # # #
# (meaning you shouldn't modify them yourself)
#
# These are for automatically finding information or files so that they can be
# used somewhere in the build process.
SRC_DIR = ./src
SRCS := $(wildcard $(SRC_DIR)/**)
CRATE_VERSION != sed -nre 's/^version = "(.*)"/\1/p' Cargo.toml
.PHONY: clean full-clean install tarball tarball-standalone web web-standalone
@@ -48,11 +59,13 @@ out/boids.js out/boids_bg.wasm.gz &: target/$(CARGO_TARGET)/$(CARGO_PROFILE)/ano
out/index.html: www/index.html
cp -a $< $@
rm -f out/boids.html
sed -i -e "s/#CRATE_VERSION_PLACEHOLDER#/$(CRATE_VERSION)/" $@
# Like `out/index.html`, but renames it for use in a larger site.
out/boids.html: www/index.html
cp -a $< $@
rm -f out/index.html
sed -i -e "s/#CRATE_VERSION_PLACEHOLDER#/$(CRATE_VERSION)/" $@
# Clean the web build, but not the Cargo cache. Cargo handles it's own caching
# and I don't want to obliterate it all the time.

View File

@@ -1,62 +1,81 @@
pub mod physics;
use avian2d::prelude::*;
use bevy::prelude::*;
use bevy_spatial::{
AutomaticUpdate, SpatialAccess, SpatialStructure, TransformMode, kdtree::KDTree2,
};
use crate::birdoids::physics::{Force, Velocity, apply_velocity};
use bevy_inspector_egui::{InspectorOptions, prelude::ReflectInspectorOptions};
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 TURN_FACTOR: f32 = 1.0;
const BOID_VIEW_RANGE: f32 = 15.0;
const COHESION_FACTOR: f32 = 1.0;
const SEPARATION_FACTOR: f32 = 10.0;
const ALIGNMENT_FACTOR: f32 = 1.0;
const SPACEBRAKES_COEFFICIENT: f32 = 0.5;
const LOW_SPEED_THRESHOLD: f32 = 50.0;
const HIGH_SPEED_THRESHOLD: f32 = 200.0;
pub struct BoidsPlugin;
impl Plugin for BoidsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(
AutomaticUpdate::<TrackedByKdTree>::new()
// .with_frequency(Duration::from_secs_f32(0.3))
.with_transform(TransformMode::GlobalTransform)
.with_spatial_ds(SpatialStructure::KDTree2),
)
.insert_resource(ClearColor(BACKGROUND_COLOR))
.add_systems(Startup, (spawn_camera, spawn_boids))
.add_systems(
FixedUpdate,
(
apply_velocity,
turn_if_edge,
check_keyboard,
cohesion,
separation,
alignment,
speed_controller,
),
);
app.insert_resource(ClearColor(BACKGROUND_COLOR))
.insert_resource(FlockingParameters::new())
.register_type::<FlockingParameters>()
.insert_resource(MiscParams::new())
.register_type::<MiscParams>()
.add_systems(Startup, (spawn_camera, spawn_boids))
.add_systems(
FixedUpdate,
(
apply_velocity,
turn_if_edge,
cohesion,
separation,
alignment,
speed_controller,
),
);
}
}
#[derive(InspectorOptions, Reflect, Resource, Debug, Clone, Copy)]
#[reflect(Resource, InspectorOptions)]
pub(crate) struct FlockingParameters {
view_range: f32,
cohesion: f32,
separation: f32,
alignment: f32,
}
impl FlockingParameters {
pub(crate) fn new() -> Self {
Self {
view_range: 15.0,
cohesion: 1.0,
separation: 5.0,
alignment: 2.0,
}
}
}
#[derive(InspectorOptions, Reflect, Resource, Debug, Clone, Copy)]
#[reflect(Resource, InspectorOptions)]
pub(crate) struct MiscParams {
turn_factor: f32,
spacebrakes: f32,
minimum_speed: f32,
maximum_speed: f32,
}
impl MiscParams {
pub(crate) fn new() -> Self {
Self {
turn_factor: 1.0,
spacebrakes: 0.5,
minimum_speed: 50.0,
maximum_speed: 200.0,
}
}
}
#[derive(Component)]
#[require(Velocity, Force, TrackedByKdTree)]
#[require(Velocity, Force)]
pub(crate) struct Boid;
// It's a Boid, but with an extra component so the player
// can control it from the keyboard
#[derive(Component)]
struct PlayerBoid;
#[derive(Component, Default)]
pub struct TrackedByKdTree;
fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
@@ -76,26 +95,21 @@ fn spawn_boids(
Mesh2d(meshes.add(Circle::new(1.0))),
MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 1.0))),
Transform::from_translation(vel * 20.0),
// RigidBody::Dynamic,
Collider::circle(1.0),
));
}
commands.spawn((
Boid,
PlayerBoid,
Mesh2d(meshes.add(Triangle2d::default())),
MeshMaterial2d(materials.add(PLAYERBOID_COLOR)),
));
}
/// Controls the boid's minimum and maximum speed according to a low- and
/// high-threshold. Boids moving too slow are sped up, and boids moving too
/// fast are slowed down.
fn speed_controller(mut mobs: Query<(&Velocity, &mut Force), With<Boid>>) {
fn speed_controller(mut mobs: Query<(&Velocity, &mut Force), With<Boid>>, params: Res<MiscParams>) {
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;
if vel.0.length() < params.minimum_speed {
impulse.0 += vel.0 * params.spacebrakes;
} else if vel.0.length() > params.maximum_speed {
impulse.0 += -vel.0 * params.spacebrakes;
}
}
}
@@ -103,21 +117,22 @@ fn speed_controller(mut mobs: Query<(&Velocity, &mut Force), With<Boid>>) {
fn turn_if_edge(
mut query: Query<(&mut Transform, &mut Velocity), With<Boid>>,
window: Query<&Window>,
params: Res<MiscParams>,
) {
if let Ok(window) = window.single() {
let (width, height) = (window.resolution.width(), window.resolution.height());
for (transform, mut velocity) in &mut query {
let boid_pos = transform.translation.xy();
if boid_pos.x <= -width / 2. + 50. {
velocity.x += TURN_FACTOR;
velocity.x += params.turn_factor;
} else if boid_pos.x >= width / 2. - 50. {
velocity.x -= TURN_FACTOR;
velocity.x -= params.turn_factor;
}
if boid_pos.y <= -height / 2. + 50. {
velocity.y += TURN_FACTOR;
velocity.y += params.turn_factor;
} else if boid_pos.y >= height / 2. - 50. {
velocity.y -= TURN_FACTOR;
velocity.y -= params.turn_factor;
}
}
} else {
@@ -125,38 +140,12 @@ fn turn_if_edge(
}
}
fn check_keyboard(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut app_exit_events: ResMut<Events<bevy::app::AppExit>>,
mut query: Query<&mut Force, With<PlayerBoid>>,
) {
if keyboard_input.just_pressed(KeyCode::KeyQ) {
app_exit_events.send(bevy::app::AppExit::Success);
}
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;
if keyboard_input.pressed(KeyCode::ArrowLeft) {
dir.x -= 1.0;
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
dir.x += 1.0;
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
dir.y -= 1.0;
}
if keyboard_input.pressed(KeyCode::ArrowUp) {
dir.y += 1.0;
}
**impulse += dir.extend(0.0) * 50.0;
}
fn cohesion(
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
spatial: SpatialQuery,
// TODO: Ensure this is logically sound. I think it will fail the "disjoint queries" requirement.
boid_locations: Query<&Transform, With<Boid>>,
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
props: Res<FlockingParameters>,
) {
// for each boid
// find neighbors
@@ -164,14 +153,23 @@ fn cohesion(
// find vector from boid to flock CoM
// apply force
for (this_entt, transform, mut force) in &mut boids {
let (len, sum) = spatial_tree
.within_distance(transform.translation.xy(), BOID_VIEW_RANGE)
let (len, sum) = spatial
.shape_intersections(
&Collider::circle(props.view_range),
transform.translation.xy(),
0.0,
&SpatialQueryFilter::default(),
)
.iter()
.filter_map(|(pos, entt)| {
.filter_map(|&entt| {
// extract neighbor's position
// 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) }
if this_entt == entt {
None
} else {
let tsfm = boid_locations.get(entt).unwrap();
Some(tsfm.translation.xy())
}
})
.enumerate()
.fold((0, Vec2::ZERO), |(_len, com), (idx, pos)| (idx, com + pos));
@@ -183,47 +181,56 @@ fn cohesion(
continue;
};
let impulse = cohesive_force(center_of_mass, transform.translation.xy()).expect("damn");
let impulse = cohesive_force(center_of_mass, transform.translation.xy(), props.view_range)
.expect("damn");
force.0 -= impulse.0 * COHESION_FACTOR;
force.0 -= impulse.0 * props.cohesion;
}
}
fn separation(
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
spatial: SpatialQuery,
boid_locations: Query<&Transform, With<Boid>>,
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
props: Res<FlockingParameters>,
) {
// for each boid
// find neighbors
// sum force from neighbors
// apply force
for (this_entt, tsfm, mut force) in &mut boids {
let impulse = spatial_tree
.within_distance(tsfm.translation.xy(), BOID_VIEW_RANGE / 4.0)
let impulse = spatial
.shape_intersections(
&Collider::circle(props.view_range / 4.0),
tsfm.translation.xy(),
0.0,
&SpatialQueryFilter::default(),
)
.iter()
.filter_map(|(pos, entt)| {
.filter_map(|&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 {
let pos = boid_locations.get(entt).unwrap().translation.xy();
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");
let force = separation_force(tsfm.translation.xy(), other.xy(), props.view_range)
.expect("angy");
acc + force.0
});
force.0 += impulse * SEPARATION_FACTOR;
force.0 += impulse * props.separation;
}
}
fn alignment(
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
spatial: SpatialQuery,
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
boid_velocities: Query<&Velocity, With<Boid>>,
props: Res<FlockingParameters>,
) {
// for each boid
// find neighbors
@@ -233,14 +240,16 @@ fn alignment(
// apply steering force
for (this_entt, transform, mut force) in &mut boids {
let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE);
let neighbors = spatial.shape_intersections(
&Collider::circle(props.view_range),
transform.translation.xy(),
0.0,
&SpatialQueryFilter::default(),
);
// averaging divides by length. Guard against an empty set of neighbors
let (len, sum) = neighbors
.iter()
// Extract the velocities by `get()`ing from another query param.
.filter_map(|(_pos, maybe_entt)| {
let entt = maybe_entt
.expect("Neighbor boid has no Entity ID. I don't know what this means");
.filter_map(|&entt| {
if this_entt == entt {
None
} else {
@@ -261,7 +270,7 @@ fn alignment(
};
let boid_vel = boid_velocities.get(this_entt).unwrap();
force.0 += (avg.extend(0.0) - boid_vel.0) * ALIGNMENT_FACTOR;
force.0 += (avg.extend(0.0) - boid_vel.0) * props.alignment;
}
}
@@ -287,13 +296,13 @@ fn average_of_vec2s(points: impl Iterator<Item = Vec2>) -> Option<Vec2> {
}
// f(x) = 4((x-0.5)^3 + 0.125)
fn cohesive_force(boid: Vec2, target: Vec2) -> Option<Force> {
fn cohesive_force(boid: Vec2, target: Vec2, view_range: f32) -> Option<Force> {
let deviation = target - boid;
/*
Scale deviation vector by the boid's view range. The curve is made to
operate on the range (0, 1), so that needs to be the viewing circle.
*/
let scaled = deviation / BOID_VIEW_RANGE;
let scaled = deviation / view_range;
let mag = scaled.length();
if mag > 0.0 {
let cube: f32 = (mag - 0.5).powf(3.0);
@@ -310,9 +319,9 @@ fn cohesive_force(boid: Vec2, target: Vec2) -> Option<Force> {
}
// f(x) = x^2 - 1
fn separation_force(boid: Vec2, target: Vec2) -> Option<Force> {
fn separation_force(boid: Vec2, target: Vec2, view_range: f32) -> Option<Force> {
// Scale from BOID_VIEW_RANGE to unit space
let distance_unit = (target - boid) / BOID_VIEW_RANGE;
let distance_unit = (target - boid) / view_range;
let mag = distance_unit.length();
if mag > 0.0 {
let force_mag = mag.powf(2.0) - 1.0;
@@ -329,14 +338,15 @@ mod tests {
use crate::birdoids::{cohesive_force, separation_force};
use super::{BOID_VIEW_RANGE, physics::Force};
use super::{FlockingParameters, physics::Force};
// forces are relative to the boid's view range, so all
// distances need to be fractions of that
#[test]
fn check_cohesion_zero_zero() {
let force = cohesive_force(Vec2::ZERO, Vec2::ZERO);
let props = FlockingParameters::new();
let force = cohesive_force(Vec2::ZERO, Vec2::ZERO, props.view_range);
assert!(force.is_none());
}
@@ -347,36 +357,56 @@ mod tests {
#[test]
fn check_cohesion_midpoint_x_positive() {
// Pull right 0.5 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.5, 0.0, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(0.5 * BOID_VIEW_RANGE, 0.0),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(0.5 * props.view_range, 0.0),
props.view_range
)
);
}
#[test]
fn check_cohesion_midpoint_x_negative() {
// Pull left 0.5 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(-0.5, 0.0, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(-0.5 * BOID_VIEW_RANGE, 0.0),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(-0.5 * props.view_range, 0.0),
props.view_range
)
);
}
#[test]
fn check_cohesion_edge_x_positive() {
// pull left 1.0 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(1.0, 0.0, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(1.0 * BOID_VIEW_RANGE, 0.0),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(1.0 * props.view_range, 0.0),
props.view_range
)
);
}
#[test]
fn check_cohesion_edge_x_negative() {
// pull left 1.0 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(-1.0, 0.0, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(-1.0 * BOID_VIEW_RANGE, 0.0),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(-1.0 * props.view_range, 0.0),
props.view_range
)
);
}
@@ -387,43 +417,64 @@ mod tests {
#[test]
fn check_cohesion_midpoint_y_positive() {
// Pull up 0.5 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.0, 0.5, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.5 * BOID_VIEW_RANGE),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(0.0, 0.5 * props.view_range),
props.view_range
)
);
}
#[test]
fn check_cohesion_midpoint_y_negative() {
// Pull down 0.5 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.0, -0.5, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(0.0, -0.5 * BOID_VIEW_RANGE),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(0.0, -0.5 * props.view_range),
props.view_range
)
);
}
#[test]
fn check_cohesion_edge_y_positive() {
// Pull up 1.0 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.0, 1.0, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(0.0, 1.0 * BOID_VIEW_RANGE))
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(0.0, 1.0 * props.view_range),
props.view_range
)
);
}
#[test]
fn check_cohesion_edge_y_negative() {
// pull down 0.2 units
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.0, -1.0, 0.0))),
cohesive_force(Vec2::new(0.0, 0.0), Vec2::new(0.0, -1.0 * BOID_VIEW_RANGE),)
cohesive_force(
Vec2::new(0.0, 0.0),
Vec2::new(0.0, -1.0 * props.view_range),
props.view_range
)
);
}
// Separation 0,0 test
#[test]
fn check_separation_zero_zero() {
let force = separation_force(Vec2::ZERO, Vec2::ZERO);
let props = FlockingParameters::new();
let force = separation_force(Vec2::ZERO, Vec2::ZERO, props.view_range);
assert!(force.is_none());
}
@@ -432,39 +483,53 @@ mod tests {
// *********************
#[test]
fn check_separation_midpoint_x_positive() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.75, 0.0, 0.0))), // expected force
separation_force(
Vec2::new(0.5 * BOID_VIEW_RANGE, 0.0), // boid position
Vec2::ZERO, // obstacle position
Vec2::new(0.5 * props.view_range, 0.0), // boid position
Vec2::ZERO, // obstacle position
props.view_range
)
);
}
#[test]
fn check_separation_midpoint_x_negative() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(-0.75, 0.0, 0.0))), // expected force
separation_force(
Vec2::new(-0.5 * BOID_VIEW_RANGE, 0.0), // boid position
Vec2::ZERO, // obstacle position
Vec2::new(-0.5 * props.view_range, 0.0), // boid position
Vec2::ZERO, // obstacle position
props.view_range
)
);
}
#[test]
fn check_separation_edge_x_positive() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::ZERO)),
separation_force(Vec2::new(1.0 * BOID_VIEW_RANGE, 0.0), Vec2::ZERO,),
separation_force(
Vec2::new(1.0 * props.view_range, 0.0),
Vec2::ZERO,
props.view_range
),
);
}
#[test]
fn check_separation_edge_x_negative() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::ZERO)),
separation_force(Vec2::new(-1.0 * BOID_VIEW_RANGE, 0.0), Vec2::ZERO,),
separation_force(
Vec2::new(-1.0 * props.view_range, 0.0),
Vec2::ZERO,
props.view_range
),
);
}
@@ -473,33 +538,53 @@ mod tests {
// *********************
#[test]
fn check_separation_midpoint_y_positive() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.0, 0.75, 0.0))),
separation_force(Vec2::new(0.0, 0.5 * BOID_VIEW_RANGE), Vec2::ZERO,)
separation_force(
Vec2::new(0.0, 0.5 * props.view_range),
Vec2::ZERO,
props.view_range
)
);
}
#[test]
fn check_separation_midpoint_y_negative() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::new(0.0, -0.75, 0.0))),
separation_force(Vec2::new(0.0, -0.5 * BOID_VIEW_RANGE), Vec2::ZERO,)
separation_force(
Vec2::new(0.0, -0.5 * props.view_range),
Vec2::ZERO,
props.view_range
)
);
}
#[test]
fn check_separation_edge_y_positive() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::ZERO)),
separation_force(Vec2::new(0.0, 1.0 * BOID_VIEW_RANGE), Vec2::ZERO,)
separation_force(
Vec2::new(0.0, 1.0 * props.view_range),
Vec2::ZERO,
props.view_range
)
)
}
#[test]
fn check_separation_edge_y_negative() {
let props = FlockingParameters::new();
assert_eq!(
Some(Force(Vec3::ZERO)),
separation_force(Vec2::new(0.0, -1.0 * BOID_VIEW_RANGE), Vec2::ZERO,)
separation_force(
Vec2::new(0.0, -1.0 * props.view_range),
Vec2::ZERO,
props.view_range
)
)
}
}

View File

@@ -1,8 +1,8 @@
use avian2d::prelude::*;
use bevy::{prelude::*, window::PrimaryWindow};
use bevy_spatial::{SpatialAccess, kdtree::KDTree2};
use crate::birdoids::{
Boid, TrackedByKdTree, center_of_boids, physics::Force, physics::Velocity, velocity_of_boids,
Boid, center_of_boids, physics::Force, physics::Velocity, velocity_of_boids,
};
const SCANRADIUS: f32 = 50.0;
@@ -106,15 +106,10 @@ fn update_scanner_mode(
}
}
fn print_gizmo_config(query: Query<(&SelectionMode, &ScannerMode), With<Cursor>>) {
let (select, scan) = query.single().unwrap();
println!("Selection: {select:?}, Scanning: {scan:?}");
}
fn do_scan(
boids_query: Query<(&Transform, &Velocity, &Force), With<Boid>>,
scanner_query: Query<(&Transform, &SelectionMode, &ScannerMode), With<Cursor>>,
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
spatial: SpatialQuery,
/* Push info to summary somewhere */
mut gizmos: Gizmos,
) {
@@ -122,25 +117,35 @@ fn do_scan(
match select_mode {
SelectionMode::NearestSingle => todo!(),
SelectionMode::CircularArea => {
let boids = spatial_tree.within_distance(cursor_pos.translation.xy(), SCANRADIUS);
let boids = spatial
.shape_intersections(
&Collider::circle(SCANRADIUS),
cursor_pos.translation.xy(),
0.0,
&SpatialQueryFilter::default(),
)
.into_iter()
.map(|entt| {
let (tsfm, vel, _) = boids_query
.get(entt)
.expect("Debugger scanned a Boid missing one of it's components!");
(tsfm, vel)
});
match scan_mode {
ScannerMode::CenterOfMass => {
if let Some(center_mass) = center_of_boids(
// boids returns too many things.
// Map over it and extract only the Vec3's
boids.iter().map(|item| item.0),
// `center_of_boids` needs an Iterator<Item = Vec2>, so we map over
// the output tuple to make one.
boids.map(|item| item.0.translation.xy()),
) {
gizmos.circle_2d(center_mass, 1.0, bevy::color::palettes::css::RED);
}
}
ScannerMode::Velocity => {
if let Some(avg_velocity) = velocity_of_boids(boids.iter().map(|item| {
let entity_id = item.1.unwrap_or_else(|| panic!("Entity has no ID!"));
let (_, vel, _) = boids_query
.get(entity_id)
.unwrap_or_else(|_| panic!("Boid has no Velocity component!"));
(*vel).xy() * 1.0
})) {
if let Some(avg_velocity) =
velocity_of_boids(boids.map(|item| (*item.1).xy() * 1.0))
{
// cursor_pos.translation is already in world space, so I can skip the window -> world transform like in update_cursor()
gizmos.line_2d(
cursor_pos.translation.xy(),

View File

@@ -1,11 +1,15 @@
use avian2d::prelude::*;
use bevy::prelude::*;
mod birdoids;
mod debug_plugin;
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::ResourceInspectorPlugin};
use birdoids::BoidsPlugin;
use debug_plugin::BoidsDebugPlugin;
use crate::birdoids::{FlockingParameters, MiscParams};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
@@ -18,5 +22,9 @@ fn main() {
}))
.add_plugins(BoidsDebugPlugin)
.add_plugins(BoidsPlugin)
.add_plugins(EguiPlugin::default())
.add_plugins(ResourceInspectorPlugin::<FlockingParameters>::new()) // TODO: monitor only the flocking params resource (once it exists)
.add_plugins(ResourceInspectorPlugin::<MiscParams>::new())
.add_plugins(PhysicsPlugins::default())
.run();
}

View File

@@ -103,8 +103,7 @@
</tr>
<tr>
<td>Program Version</td>
<!-- This version text is completely unchecked. I'll need to do something about that. -->
<td><code>v0.6.0</code></td>
<td><code>#CRATE_VERSION_PLACEHOLDER#</code></td>
</tr>
</table>
</article>