Compare commits
21 Commits
v0.5.0
...
975f2a0b92
| Author | SHA1 | Date | |
|---|---|---|---|
| 975f2a0b92 | |||
| 3a3b8181f9 | |||
| 62feb6f313 | |||
| 9797b90415 | |||
| 1846b7065e | |||
| e252e3385c | |||
| 5f0b428811 | |||
| 119d7acf09 | |||
| 63f15ae6a7 | |||
| ba01d8137f | |||
| b8c28529e6 | |||
| fd161dc26b | |||
| c2cf100c05 | |||
| 0828518963 | |||
| 5ef91ec88c | |||
| 8346527d05 | |||
| 6329d000f5 | |||
| 46892ec32c | |||
| f6c6f26b8b | |||
| 9f8598c794 | |||
| b64771f3fc |
@@ -34,44 +34,6 @@ jobs:
|
|||||||
components: rustfmt
|
components: rustfmt
|
||||||
- name: cargo fmt --check
|
- name: cargo fmt --check
|
||||||
run: cargo fmt --check
|
run: cargo fmt --check
|
||||||
clippy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: ${{ matrix.toolchain }} / clippy
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
checks: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# Get early warning of new lints which are regularly introduced in beta channels.
|
|
||||||
toolchain: [stable, beta]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Install ${{ matrix.toolchain }}
|
|
||||||
uses: dtolnay/rust-toolchain@master
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
components: clippy
|
|
||||||
- name: cargo clippy
|
|
||||||
uses: giraffate/clippy-action@v1
|
|
||||||
with:
|
|
||||||
reporter: 'github-pr-check'
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
semver:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: semver
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Install stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt
|
|
||||||
- name: cargo-semver-checks
|
|
||||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
|
||||||
doc:
|
doc:
|
||||||
# run docs generation on nightly rather than stable. This enables features like
|
# run docs generation on nightly rather than stable. This enables features like
|
||||||
# https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an
|
# https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an
|
||||||
@@ -83,7 +45,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install Dependencies
|
- 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
|
- name: Install nightly
|
||||||
uses: dtolnay/rust-toolchain@nightly
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
- name: cargo doc
|
- name: cargo doc
|
||||||
@@ -100,7 +62,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install Dependencies
|
- 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
|
- name: Install stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: cargo install cargo-hack
|
- name: cargo install cargo-hack
|
||||||
@@ -116,14 +78,14 @@ jobs:
|
|||||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
|
# https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
msrv: ["1.79.0"]
|
msrv: ["1.88.0"]
|
||||||
name: ubuntu / ${{ matrix.msrv }}
|
name: ubuntu / ${{ matrix.msrv }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install Dependencies
|
- 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 }}
|
- name: Install ${{ matrix.msrv }}
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
2773
Cargo.lock
generated
2773
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -1,11 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "another-boids-in-rust"
|
name = "another-boids-in-rust"
|
||||||
version = "0.5.0"
|
version = "0.7.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
rust-version = "1.88.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = "0.16.0"
|
avian2d = "0.4"
|
||||||
|
bevy = "0.17.0"
|
||||||
|
bevy-inspector-egui = "0.34"
|
||||||
|
|
||||||
# Grand-dependency pins
|
# Grand-dependency pins
|
||||||
# ab_glyph = "0.2.16"
|
# ab_glyph = "0.2.16"
|
||||||
@@ -17,16 +20,6 @@ bevy = "0.16.0"
|
|||||||
# 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
|
||||||
|
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -17,19 +17,19 @@ SRCS := $(wildcard $(SRC_DIR)/**)
|
|||||||
|
|
||||||
# "Standalone" version
|
# "Standalone" version
|
||||||
# (i.e., it includes an index.html so it can be placed on a server as-is)
|
# (i.e., it includes an index.html so it can be placed on a server as-is)
|
||||||
web-standalone: out/boids.js out/boids_bg.wasm out/index.html
|
web-standalone: out/boids.js out/boids_bg.wasm.gz out/index.html
|
||||||
|
|
||||||
# "Bundle-able" version. The host site must provide it's own HTML page.
|
# "Bundle-able" version. The host site must provide it's own HTML page.
|
||||||
web: out/boids.js out/boids_bg.wasm out/boids.html
|
web: out/boids.js out/boids_bg.wasm.gz out/boids.html
|
||||||
|
|
||||||
tarball: boids_web_root.tar
|
tarball: boids_web_root.tar
|
||||||
|
|
||||||
tarball_standalone: boids_web_root_standalone.tar
|
tarball_standalone: boids_web_root_standalone.tar
|
||||||
|
|
||||||
boids_web_root.tar: out/boids.js out/boids_bg.wasm out/boids.html
|
boids_web_root.tar: out/boids.js out/boids_bg.wasm.gz out/boids.html
|
||||||
tar -caf $@ $^
|
tar -caf $@ $^
|
||||||
|
|
||||||
boids_web_root_standalone.tar: out/boids.js out/boids_bg.wasm out/index.html
|
boids_web_root_standalone.tar: out/boids.js out/boids_bg.wasm.gz out/index.html
|
||||||
tar -caf $@ $^
|
tar -caf $@ $^
|
||||||
|
|
||||||
target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm: $(SRCS) Cargo.lock Cargo.toml
|
target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm: $(SRCS) Cargo.lock Cargo.toml
|
||||||
@@ -40,8 +40,9 @@ out:
|
|||||||
|
|
||||||
# Both the JS and WASM files are generated by the wasm-bindgen call, so both
|
# Both the JS and WASM files are generated by the wasm-bindgen call, so both
|
||||||
# get to be on the target half of this recipe.
|
# get to be on the target half of this recipe.
|
||||||
out/boids.js out/boids_bg.wasm &: target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm | out
|
out/boids.js out/boids_bg.wasm.gz &: target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm | out
|
||||||
wasm-bindgen --no-typescript --target web --out-dir ./out/ --out-name boids target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm
|
wasm-bindgen --no-typescript --target web --out-dir ./out/ --out-name boids target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm
|
||||||
|
gzip -9 -f out/boids_bg.wasm
|
||||||
|
|
||||||
# Copies the index page to the output
|
# Copies the index page to the output
|
||||||
out/index.html: www/index.html
|
out/index.html: www/index.html
|
||||||
@@ -68,5 +69,5 @@ full-clean: clean
|
|||||||
install: web
|
install: web
|
||||||
install -dm0755 $(DESTDIR)
|
install -dm0755 $(DESTDIR)
|
||||||
install -m0644 out/boids.js $(DESTDIR)/
|
install -m0644 out/boids.js $(DESTDIR)/
|
||||||
install -m0644 out/boids_bg.wasm $(DESTDIR)/
|
install -m0644 out/boids_bg.wasm.gz $(DESTDIR)/
|
||||||
install -m0644 out/boids.html $(DESTDIR)/
|
install -m0644 out/boids.html $(DESTDIR)/
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ Basically, just throw in a hyperlink with `<a href="boids.html">Boids</a>`
|
|||||||
```
|
```
|
||||||
|
|
||||||
As mentioned in the option 2 description, I'm not using a JS Bundler. There is no "package.json" or anything to integrate properly with a JS framework. I plan to fix that at some point, but for now there are just a bunch of files to grab.
|
As mentioned in the option 2 description, I'm not using a JS Bundler. There is no "package.json" or anything to integrate properly with a JS framework. I plan to fix that at some point, but for now there are just a bunch of files to grab.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
You can use any HTTP server you like. In the steps above, I'm using the Python3 built-in [http.server module](https://docs.python.org/3/library/http.server.html); which is **NOT** recommended for production use. Don't put that on the Internet! Alternatives include [Miniserve](https://crates.io/crates/miniserve) and [BusyBox](https://busybox.net/). The latter of which I'm using in the Docker image.
|
You can use any HTTP server you like. In the steps above, I'm using the Python3 built-in [http.server module](https://docs.python.org/3/library/http.server.html); which is **NOT** recommended for production use. Don't put that on the Internet! Alternatives include [Miniserve](https://crates.io/crates/miniserve) and [BusyBox](https://busybox.net/). The latter of which I'm using in the Docker image.
|
||||||
@@ -73,6 +74,7 @@ You may also notice that the Dockerfile doesn't call on the Makefile. This is be
|
|||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
| Input | Effect |
|
| Input | Effect |
|
||||||
|
|-|-|
|
||||||
| Mouse | The scanner circle is attached to the mouse cursor. Move it to scan boids within the radius. |
|
| Mouse | The scanner circle is attached to the mouse cursor. Move it to scan boids within the radius. |
|
||||||
| Left mouse button | Put scanner into center-of-mass mode |
|
| Left mouse button | Put scanner into center-of-mass mode |
|
||||||
| Right mouse button | Put scanner into average velocity mode |
|
| Right mouse button | Put scanner into average velocity mode |
|
||||||
|
|||||||
@@ -1,62 +1,81 @@
|
|||||||
pub mod physics;
|
pub mod physics;
|
||||||
|
|
||||||
|
use avian2d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_spatial::{
|
|
||||||
AutomaticUpdate, SpatialAccess, SpatialStructure, TransformMode, kdtree::KDTree2,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::birdoids::physics::{Force, Velocity, apply_velocity};
|
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 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;
|
pub struct BoidsPlugin;
|
||||||
|
|
||||||
impl Plugin for BoidsPlugin {
|
impl Plugin for BoidsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugins(
|
app.insert_resource(ClearColor(BACKGROUND_COLOR))
|
||||||
AutomaticUpdate::<TrackedByKdTree>::new()
|
.insert_resource(FlockingParameters::new())
|
||||||
// .with_frequency(Duration::from_secs_f32(0.3))
|
.register_type::<FlockingParameters>()
|
||||||
.with_transform(TransformMode::GlobalTransform)
|
.insert_resource(MiscParams::new())
|
||||||
.with_spatial_ds(SpatialStructure::KDTree2),
|
.register_type::<MiscParams>()
|
||||||
)
|
.add_systems(Startup, (spawn_camera, spawn_boids))
|
||||||
.insert_resource(ClearColor(BACKGROUND_COLOR))
|
.add_systems(
|
||||||
.add_systems(Startup, (spawn_camera, spawn_boids))
|
FixedUpdate,
|
||||||
.add_systems(
|
(
|
||||||
FixedUpdate,
|
apply_velocity,
|
||||||
(
|
turn_if_edge,
|
||||||
apply_velocity,
|
cohesion,
|
||||||
turn_if_edge,
|
separation,
|
||||||
check_keyboard,
|
alignment,
|
||||||
cohesion,
|
speed_controller,
|
||||||
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)]
|
#[derive(Component)]
|
||||||
#[require(Velocity, Force, TrackedByKdTree)]
|
#[require(Velocity, Force)]
|
||||||
pub(crate) struct Boid;
|
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) {
|
fn spawn_camera(mut commands: Commands) {
|
||||||
commands.spawn(Camera2d);
|
commands.spawn(Camera2d);
|
||||||
}
|
}
|
||||||
@@ -76,26 +95,21 @@ fn spawn_boids(
|
|||||||
Mesh2d(meshes.add(Circle::new(1.0))),
|
Mesh2d(meshes.add(Circle::new(1.0))),
|
||||||
MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 1.0))),
|
MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 1.0))),
|
||||||
Transform::from_translation(vel * 20.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
|
/// 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
|
/// high-threshold. Boids moving too slow are sped up, and boids moving too
|
||||||
/// fast are slowed down.
|
/// 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 {
|
for (vel, mut impulse) in &mut mobs {
|
||||||
if vel.0.length() < LOW_SPEED_THRESHOLD {
|
if vel.0.length() < params.minimum_speed {
|
||||||
impulse.0 += vel.0 * SPACEBRAKES_COEFFICIENT;
|
impulse.0 += vel.0 * params.spacebrakes;
|
||||||
} else if vel.0.length() > HIGH_SPEED_THRESHOLD {
|
} else if vel.0.length() > params.maximum_speed {
|
||||||
impulse.0 += -vel.0 * SPACEBRAKES_COEFFICIENT;
|
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(
|
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>,
|
||||||
|
params: Res<MiscParams>,
|
||||||
) {
|
) {
|
||||||
if let Ok(window) = window.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();
|
||||||
if boid_pos.x <= -width / 2. + 50. {
|
if boid_pos.x <= -width / 2. + 50. {
|
||||||
velocity.x += TURN_FACTOR;
|
velocity.x += params.turn_factor;
|
||||||
} else if boid_pos.x >= width / 2. - 50. {
|
} else if boid_pos.x >= width / 2. - 50. {
|
||||||
velocity.x -= TURN_FACTOR;
|
velocity.x -= params.turn_factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if boid_pos.y <= -height / 2. + 50. {
|
if boid_pos.y <= -height / 2. + 50. {
|
||||||
velocity.y += TURN_FACTOR;
|
velocity.y += params.turn_factor;
|
||||||
} else if boid_pos.y >= height / 2. - 50. {
|
} else if boid_pos.y >= height / 2. - 50. {
|
||||||
velocity.y -= TURN_FACTOR;
|
velocity.y -= params.turn_factor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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(
|
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>>,
|
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
|
||||||
|
props: Res<FlockingParameters>,
|
||||||
) {
|
) {
|
||||||
// for each boid
|
// for each boid
|
||||||
// find neighbors
|
// find neighbors
|
||||||
@@ -164,14 +153,23 @@ fn cohesion(
|
|||||||
// find vector from boid to flock CoM
|
// find vector from boid to flock CoM
|
||||||
// apply force
|
// apply force
|
||||||
for (this_entt, transform, mut force) in &mut boids {
|
for (this_entt, transform, mut force) in &mut boids {
|
||||||
let (len, sum) = spatial_tree
|
let (len, sum) = spatial
|
||||||
.within_distance(transform.translation.xy(), BOID_VIEW_RANGE)
|
.shape_intersections(
|
||||||
|
&Collider::circle(props.view_range),
|
||||||
|
transform.translation.xy(),
|
||||||
|
0.0,
|
||||||
|
&SpatialQueryFilter::default(),
|
||||||
|
)
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(pos, entt)| {
|
.filter_map(|&entt| {
|
||||||
|
// extract neighbor's position
|
||||||
// Skip self-comparison. A boid should not try to separate from itself.
|
// Skip self-comparison. A boid should not try to separate from itself.
|
||||||
let entt = entt
|
if this_entt == entt {
|
||||||
.expect("within_distance gave me an entity... with no entity ID... somehow");
|
None
|
||||||
if this_entt == entt { None } else { Some(pos) }
|
} else {
|
||||||
|
let tsfm = boid_locations.get(entt).unwrap();
|
||||||
|
Some(tsfm.translation.xy())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.fold((0, Vec2::ZERO), |(_len, com), (idx, pos)| (idx, com + pos));
|
.fold((0, Vec2::ZERO), |(_len, com), (idx, pos)| (idx, com + pos));
|
||||||
@@ -183,47 +181,56 @@ fn cohesion(
|
|||||||
continue;
|
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(
|
fn separation(
|
||||||
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
spatial: SpatialQuery,
|
||||||
|
boid_locations: Query<&Transform, With<Boid>>,
|
||||||
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
|
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
|
||||||
|
props: Res<FlockingParameters>,
|
||||||
) {
|
) {
|
||||||
// for each boid
|
// for each boid
|
||||||
// find neighbors
|
// find neighbors
|
||||||
// sum force from neighbors
|
// sum force from neighbors
|
||||||
// apply force
|
// apply force
|
||||||
for (this_entt, tsfm, mut force) in &mut boids {
|
for (this_entt, tsfm, mut force) in &mut boids {
|
||||||
let impulse = spatial_tree
|
let impulse = spatial
|
||||||
.within_distance(tsfm.translation.xy(), BOID_VIEW_RANGE / 4.0)
|
.shape_intersections(
|
||||||
|
&Collider::circle(props.view_range / 4.0),
|
||||||
|
tsfm.translation.xy(),
|
||||||
|
0.0,
|
||||||
|
&SpatialQueryFilter::default(),
|
||||||
|
)
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(pos, entt)| {
|
.filter_map(|&entt| {
|
||||||
// Skip self-comparison. A boid should not try to separate from itself.
|
// 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 {
|
if this_entt == entt {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
let pos = boid_locations.get(entt).unwrap().translation.xy();
|
||||||
Some(pos.extend(0.0))
|
Some(pos.extend(0.0))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fold(Vec3::ZERO, |acc, other| {
|
.fold(Vec3::ZERO, |acc, other| {
|
||||||
// let force = tsfm.translation - 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
|
acc + force.0
|
||||||
});
|
});
|
||||||
force.0 += impulse * SEPARATION_FACTOR;
|
force.0 += impulse * props.separation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alignment(
|
fn alignment(
|
||||||
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
spatial: SpatialQuery,
|
||||||
mut boids: Query<(Entity, &Transform, &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>>,
|
||||||
|
props: Res<FlockingParameters>,
|
||||||
) {
|
) {
|
||||||
// for each boid
|
// for each boid
|
||||||
// find neighbors
|
// find neighbors
|
||||||
@@ -233,14 +240,16 @@ fn alignment(
|
|||||||
// apply steering force
|
// apply steering force
|
||||||
|
|
||||||
for (this_entt, transform, mut force) 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.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
|
// averaging divides by length. Guard against an empty set of neighbors
|
||||||
let (len, sum) = neighbors
|
let (len, sum) = neighbors
|
||||||
.iter()
|
.iter()
|
||||||
// Extract the velocities by `get()`ing from another query param.
|
.filter_map(|&entt| {
|
||||||
.filter_map(|(_pos, maybe_entt)| {
|
|
||||||
let entt = maybe_entt
|
|
||||||
.expect("Neighbor boid has no Entity ID. I don't know what this means");
|
|
||||||
if this_entt == entt {
|
if this_entt == entt {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -261,7 +270,7 @@ fn alignment(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let boid_vel = boid_velocities.get(this_entt).unwrap();
|
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)
|
// 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;
|
let deviation = target - boid;
|
||||||
/*
|
/*
|
||||||
Scale deviation vector by the boid's view range. The curve is made to
|
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.
|
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();
|
let mag = scaled.length();
|
||||||
if mag > 0.0 {
|
if mag > 0.0 {
|
||||||
let cube: f32 = (mag - 0.5).powf(3.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
|
// 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
|
// 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();
|
let mag = distance_unit.length();
|
||||||
if mag > 0.0 {
|
if mag > 0.0 {
|
||||||
let force_mag = mag.powf(2.0) - 1.0;
|
let force_mag = mag.powf(2.0) - 1.0;
|
||||||
@@ -329,14 +338,15 @@ mod tests {
|
|||||||
|
|
||||||
use crate::birdoids::{cohesive_force, separation_force};
|
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
|
// 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
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_cohesion_zero_zero() {
|
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());
|
assert!(force.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,36 +357,56 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn check_cohesion_midpoint_x_positive() {
|
fn check_cohesion_midpoint_x_positive() {
|
||||||
// Pull right 0.5 units
|
// Pull right 0.5 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.5, 0.0, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_midpoint_x_negative() {
|
fn check_cohesion_midpoint_x_negative() {
|
||||||
// Pull left 0.5 units
|
// Pull left 0.5 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(-0.5, 0.0, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_edge_x_positive() {
|
fn check_cohesion_edge_x_positive() {
|
||||||
// pull left 1.0 units
|
// pull left 1.0 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(1.0, 0.0, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_edge_x_negative() {
|
fn check_cohesion_edge_x_negative() {
|
||||||
// pull left 1.0 units
|
// pull left 1.0 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(-1.0, 0.0, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_midpoint_y_positive() {
|
fn check_cohesion_midpoint_y_positive() {
|
||||||
// Pull up 0.5 units
|
// Pull up 0.5 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.0, 0.5, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_midpoint_y_negative() {
|
fn check_cohesion_midpoint_y_negative() {
|
||||||
// Pull down 0.5 units
|
// Pull down 0.5 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.0, -0.5, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_edge_y_positive() {
|
fn check_cohesion_edge_y_positive() {
|
||||||
// Pull up 1.0 units
|
// Pull up 1.0 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.0, 1.0, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_cohesion_edge_y_negative() {
|
fn check_cohesion_edge_y_negative() {
|
||||||
// pull down 0.2 units
|
// pull down 0.2 units
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.0, -1.0, 0.0))),
|
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
|
// Separation 0,0 test
|
||||||
#[test]
|
#[test]
|
||||||
fn check_separation_zero_zero() {
|
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());
|
assert!(force.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,39 +483,53 @@ mod tests {
|
|||||||
// *********************
|
// *********************
|
||||||
#[test]
|
#[test]
|
||||||
fn check_separation_midpoint_x_positive() {
|
fn check_separation_midpoint_x_positive() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.75, 0.0, 0.0))), // expected force
|
Some(Force(Vec3::new(0.75, 0.0, 0.0))), // expected force
|
||||||
separation_force(
|
separation_force(
|
||||||
Vec2::new(0.5 * BOID_VIEW_RANGE, 0.0), // boid position
|
Vec2::new(0.5 * props.view_range, 0.0), // boid position
|
||||||
Vec2::ZERO, // obstacle position
|
Vec2::ZERO, // obstacle position
|
||||||
|
props.view_range
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_separation_midpoint_x_negative() {
|
fn check_separation_midpoint_x_negative() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(-0.75, 0.0, 0.0))), // expected force
|
Some(Force(Vec3::new(-0.75, 0.0, 0.0))), // expected force
|
||||||
separation_force(
|
separation_force(
|
||||||
Vec2::new(-0.5 * BOID_VIEW_RANGE, 0.0), // boid position
|
Vec2::new(-0.5 * props.view_range, 0.0), // boid position
|
||||||
Vec2::ZERO, // obstacle position
|
Vec2::ZERO, // obstacle position
|
||||||
|
props.view_range
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_separation_edge_x_positive() {
|
fn check_separation_edge_x_positive() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::ZERO)),
|
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]
|
#[test]
|
||||||
fn check_separation_edge_x_negative() {
|
fn check_separation_edge_x_negative() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::ZERO)),
|
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]
|
#[test]
|
||||||
fn check_separation_midpoint_y_positive() {
|
fn check_separation_midpoint_y_positive() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.0, 0.75, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_separation_midpoint_y_negative() {
|
fn check_separation_midpoint_y_negative() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::new(0.0, -0.75, 0.0))),
|
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]
|
#[test]
|
||||||
fn check_separation_edge_y_positive() {
|
fn check_separation_edge_y_positive() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::ZERO)),
|
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]
|
#[test]
|
||||||
fn check_separation_edge_y_negative() {
|
fn check_separation_edge_y_negative() {
|
||||||
|
let props = FlockingParameters::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Force(Vec3::ZERO)),
|
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::{prelude::*, window::PrimaryWindow};
|
||||||
use bevy_spatial::{SpatialAccess, kdtree::KDTree2};
|
|
||||||
|
|
||||||
use crate::birdoids::{
|
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;
|
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(
|
fn do_scan(
|
||||||
boids_query: Query<(&Transform, &Velocity, &Force), With<Boid>>,
|
boids_query: Query<(&Transform, &Velocity, &Force), With<Boid>>,
|
||||||
scanner_query: Query<(&Transform, &SelectionMode, &ScannerMode), With<Cursor>>,
|
scanner_query: Query<(&Transform, &SelectionMode, &ScannerMode), With<Cursor>>,
|
||||||
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
spatial: SpatialQuery,
|
||||||
/* Push info to summary somewhere */
|
/* Push info to summary somewhere */
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
) {
|
) {
|
||||||
@@ -122,25 +117,35 @@ fn do_scan(
|
|||||||
match select_mode {
|
match select_mode {
|
||||||
SelectionMode::NearestSingle => todo!(),
|
SelectionMode::NearestSingle => todo!(),
|
||||||
SelectionMode::CircularArea => {
|
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 {
|
match scan_mode {
|
||||||
ScannerMode::CenterOfMass => {
|
ScannerMode::CenterOfMass => {
|
||||||
if let Some(center_mass) = center_of_boids(
|
if let Some(center_mass) = center_of_boids(
|
||||||
// boids returns too many things.
|
// `center_of_boids` needs an Iterator<Item = Vec2>, so we map over
|
||||||
// Map over it and extract only the Vec3's
|
// the output tuple to make one.
|
||||||
boids.iter().map(|item| item.0),
|
boids.map(|item| item.0.translation.xy()),
|
||||||
) {
|
) {
|
||||||
gizmos.circle_2d(center_mass, 1.0, bevy::color::palettes::css::RED);
|
gizmos.circle_2d(center_mass, 1.0, bevy::color::palettes::css::RED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScannerMode::Velocity => {
|
ScannerMode::Velocity => {
|
||||||
if let Some(avg_velocity) = velocity_of_boids(boids.iter().map(|item| {
|
if let Some(avg_velocity) =
|
||||||
let entity_id = item.1.unwrap_or_else(|| panic!("Entity has no ID!"));
|
velocity_of_boids(boids.map(|item| (*item.1).xy() * 1.0))
|
||||||
let (_, vel, _) = boids_query
|
{
|
||||||
.get(entity_id)
|
|
||||||
.unwrap_or_else(|_| panic!("Boid has no Velocity component!"));
|
|
||||||
(*vel).xy() * 1.0
|
|
||||||
})) {
|
|
||||||
// cursor_pos.translation is already in world space, so I can skip the window -> world transform like in update_cursor()
|
// cursor_pos.translation is already in world space, so I can skip the window -> world transform like in update_cursor()
|
||||||
gizmos.line_2d(
|
gizmos.line_2d(
|
||||||
cursor_pos.translation.xy(),
|
cursor_pos.translation.xy(),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
use avian2d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
mod birdoids;
|
mod birdoids;
|
||||||
mod debug_plugin;
|
mod debug_plugin;
|
||||||
|
|
||||||
|
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::ResourceInspectorPlugin};
|
||||||
use birdoids::BoidsPlugin;
|
use birdoids::BoidsPlugin;
|
||||||
use debug_plugin::BoidsDebugPlugin;
|
use debug_plugin::BoidsDebugPlugin;
|
||||||
|
|
||||||
|
use crate::birdoids::{FlockingParameters, MiscParams};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||||
@@ -18,5 +22,9 @@ fn main() {
|
|||||||
}))
|
}))
|
||||||
.add_plugins(BoidsDebugPlugin)
|
.add_plugins(BoidsDebugPlugin)
|
||||||
.add_plugins(BoidsPlugin)
|
.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();
|
.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Program Version</td>
|
<td>Program Version</td>
|
||||||
<!-- This version text is completely unchecked. I'll need to do something about that. -->
|
<!-- This version text is completely unchecked. I'll need to do something about that. -->
|
||||||
<td><code>v0.5.0</code></td>
|
<td><code>v0.6.0</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
@@ -112,7 +112,11 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
import init from './boids.js'
|
import init from './boids.js'
|
||||||
|
|
||||||
init().catch((error) => {
|
let compressed = await fetch("./boids_bg.wasm.gz")
|
||||||
|
let wasm_stream = compressed.body.pipeThrough(new DecompressionStream("gzip"))
|
||||||
|
let blob = await new Response(wasm_stream).blob();
|
||||||
|
|
||||||
|
init(await blob.arrayBuffer()).catch((error) => {
|
||||||
if (!error.message.startsWith("Using exceptions for control flow, don't mind me. This isn't actually an error!")) {
|
if (!error.message.startsWith("Using exceptions for control flow, don't mind me. This isn't actually an error!")) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user