Compare commits
20 Commits
release
...
ac60482688
| Author | SHA1 | Date | |
|---|---|---|---|
| ac60482688 | |||
| 75a675977a | |||
| 8f2ab97cf0 | |||
| e494fc6c35 | |||
| 03c7d44aad | |||
| 975f2a0b92 | |||
| 3a3b8181f9 | |||
| 62feb6f313 | |||
| 9797b90415 | |||
| 1846b7065e | |||
| e252e3385c | |||
| 5f0b428811 | |||
| 119d7acf09 | |||
| 63f15ae6a7 | |||
| ba01d8137f | |||
| b8c28529e6 | |||
| fd161dc26b | |||
| c2cf100c05 | |||
| 0828518963 | |||
| 5ef91ec88c |
@@ -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
2757
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -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
|
||||
|
||||
21
Makefile
21
Makefile
@@ -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.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user