Compare commits
35 Commits
v0.4.0
...
03c7d44aad
| Author | SHA1 | Date | |
|---|---|---|---|
| 03c7d44aad | |||
| 975f2a0b92 | |||
| 3a3b8181f9 | |||
| 62feb6f313 | |||
| 9797b90415 | |||
| 1846b7065e | |||
| e252e3385c | |||
| 5f0b428811 | |||
| 119d7acf09 | |||
| 63f15ae6a7 | |||
| ba01d8137f | |||
| b8c28529e6 | |||
| fd161dc26b | |||
| c2cf100c05 | |||
| 0828518963 | |||
| 5ef91ec88c | |||
| 8346527d05 | |||
| 6329d000f5 | |||
| 46892ec32c | |||
| f6c6f26b8b | |||
| 9f8598c794 | |||
| b64771f3fc | |||
| d1941217c4 | |||
| ba5183b30d | |||
| 3e3bbd973f | |||
| 5abe894354 | |||
| 86483497e9 | |||
| 3f6a4ae532 | |||
| 7a5148dc74 | |||
| 8d4b033922 | |||
| 0c915b025e | |||
| 15a1d5c6de | |||
| e7e5337f22 | |||
| e65deec5ea | |||
| 29d0b644ec |
@@ -34,44 +34,6 @@ jobs:
|
||||
components: rustfmt
|
||||
- name: 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:
|
||||
# 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
|
||||
@@ -83,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
|
||||
@@ -100,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
|
||||
@@ -116,14 +78,14 @@ jobs:
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
|
||||
strategy:
|
||||
matrix:
|
||||
msrv: ["1.79.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:
|
||||
|
||||
2797
Cargo.lock
generated
2797
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -1,11 +1,14 @@
|
||||
[package]
|
||||
name = "another-boids-in-rust"
|
||||
version = "0.4.0"
|
||||
version = "0.8.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.16.0"
|
||||
avian2d = "0.4"
|
||||
bevy = "0.17.0"
|
||||
bevy-inspector-egui = "0.34"
|
||||
|
||||
# Grand-dependency pins
|
||||
# ab_glyph = "0.2.16"
|
||||
@@ -17,16 +20,6 @@ bevy = "0.16.0"
|
||||
# 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"]
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,19 +1,12 @@
|
||||
FROM rust:1.89 AS builder
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
RUN rustup target add wasm32-unknown-unknown
|
||||
RUN cargo install --locked wasm-bindgen-cli
|
||||
|
||||
# Copy only the package manifest and source code. Otherwise changes to anything
|
||||
# will cause Docker to re-run `cargo build` even when the source hasn't changed.
|
||||
COPY src/ ./src
|
||||
COPY Cargo.toml ./Cargo.toml
|
||||
COPY Cargo.lock ./Cargo.lock
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --locked --target wasm32-unknown-unknown --profile=wasm-release
|
||||
RUN wasm-bindgen --no-typescript --target web --out-dir ./out/ --out-name "boids" target/wasm32-unknown-unknown/wasm-release/another-boids-in-rust.wasm
|
||||
COPY www/index.html out/index.html
|
||||
RUN make -j
|
||||
|
||||
FROM busybox:musl
|
||||
RUN mkdir -p /var/www
|
||||
|
||||
73
Makefile
Normal file
73
Makefile
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
CARGO_TARGET := wasm32-unknown-unknown
|
||||
CARGO_PROFILE := wasm-release
|
||||
|
||||
# Override DESTDIR to set a custom install path (such as your web root)
|
||||
DESTDIR ?= .
|
||||
|
||||
SRC_DIR = ./src
|
||||
SRCS := $(wildcard $(SRC_DIR)/**)
|
||||
|
||||
.PHONY: clean full-clean install tarball tarball-standalone web web-standalone
|
||||
|
||||
# "Standalone" version
|
||||
# (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.gz out/index.html
|
||||
|
||||
# "Bundle-able" version. The host site must provide it's own HTML page.
|
||||
web: out/boids.js out/boids_bg.wasm.gz out/boids.html
|
||||
|
||||
tarball: boids_web_root.tar
|
||||
|
||||
tarball_standalone: boids_web_root_standalone.tar
|
||||
|
||||
boids_web_root.tar: out/boids.js out/boids_bg.wasm.gz out/boids.html
|
||||
tar -caf $@ $^
|
||||
|
||||
boids_web_root_standalone.tar: out/boids.js out/boids_bg.wasm.gz out/index.html
|
||||
tar -caf $@ $^
|
||||
|
||||
target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm: $(SRCS) Cargo.lock Cargo.toml
|
||||
cargo build --profile $(CARGO_PROFILE) --target $(CARGO_TARGET)
|
||||
|
||||
out:
|
||||
mkdir $@
|
||||
|
||||
# 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.
|
||||
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
|
||||
gzip -9 -f out/boids_bg.wasm
|
||||
|
||||
# Copies the index page to the output
|
||||
out/index.html: www/index.html
|
||||
cp -a $< $@
|
||||
rm -f out/boids.html
|
||||
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
clean:
|
||||
rm -rf out/ boids_web_root.tar boids_web_root_standalone.tar
|
||||
|
||||
# Delete everything, including the Cargo build cache. In case someone needs
|
||||
# this, I guess.
|
||||
full-clean: clean
|
||||
cargo clean
|
||||
|
||||
# Installation goal. It's meant to be a helper utility for moving the built
|
||||
# output into the web root. Only supports the "bundle-able" mode.
|
||||
install: web
|
||||
install -dm0755 $(DESTDIR)
|
||||
install -m0644 out/boids.js $(DESTDIR)/
|
||||
install -m0644 out/boids_bg.wasm.gz $(DESTDIR)/
|
||||
install -m0644 out/boids.html $(DESTDIR)/
|
||||
36
README.md
36
README.md
@@ -21,13 +21,42 @@ You'll need a working Rust toolchain, of course. See the [rustup](https://rustup
|
||||
|
||||
### Web
|
||||
|
||||
This project creates a "static site," meaning a complete deployment of the site is simply copying the output folder onto a webserver.
|
||||
This project creates a "static site," meaning a complete deployment of the site is simply copying the output folder onto a webserver. There are **two** ways to use this.
|
||||
|
||||
1. Build `make -f ./makefile_web web`
|
||||
1. A standalone application which can be quickly hosted as-is. This includes the WASM, it's JS glue, and an index.html page.
|
||||
2. A sub-page in a larger website. This is actually the same, but names it's HTML page "boids.html" so consumers (you) can provide their own index.html.
|
||||
- I'm not using a JS Bundler at this time. If you're familiar with JS development, this probably looks like a dumb way to do it. Sorry about that.
|
||||
|
||||
#### Standalone build:
|
||||
|
||||
1. Build `make web-standalone`
|
||||
2. Serve `python3 -m http.server -d ./out`
|
||||
3. Visit site in browser: `http://localhost:8000`
|
||||
|
||||
You can use any HTTP server you like. Here, 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.
|
||||
To quickly get a tarball, use `make tarball_standalone`. If you're trying to build and upload the program somewhere, this may provide a bit of convenience. Compressing it may be a good idea, too.
|
||||
|
||||
#### "Bundle-able" build
|
||||
|
||||
For a "bundle-able" build, you'll need to write your own index.html and link to the boids.html file.
|
||||
|
||||
Basically, just throw in a hyperlink with `<a href="boids.html">Boids</a>`
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- your page content, etc -->
|
||||
<body>
|
||||
<a href="boids.html">Boids</a>
|
||||
</body>
|
||||
<!-- more page content, etc -->
|
||||
</html>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Web, but as a Docker Container
|
||||
|
||||
@@ -45,6 +74,7 @@ You may also notice that the Dockerfile doesn't call on the Makefile. This is be
|
||||
## Controls
|
||||
|
||||
| Input | Effect |
|
||||
|-|-|
|
||||
| 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 |
|
||||
| Right mouse button | Put scanner into average velocity mode |
|
||||
|
||||
25
fly.toml
25
fly.toml
@@ -1,25 +0,0 @@
|
||||
# fly.toml app configuration file generated for boids-autumn-lake-5810 on 2025-09-03T16:02:00-05:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = 'boids-autumn-lake-5810'
|
||||
primary_region = 'ord'
|
||||
|
||||
[build]
|
||||
|
||||
[env]
|
||||
PORT = '8080'
|
||||
|
||||
[http_service]
|
||||
internal_port = 8080
|
||||
force_https = true
|
||||
auto_stop_machines = 'stop'
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ['app']
|
||||
|
||||
[[vm]]
|
||||
memory = '256mb'
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
41
makefile_web
41
makefile_web
@@ -1,41 +0,0 @@
|
||||
SRC_DIR = ./src
|
||||
SRCS := $(wildcard $(SRC_DIR)/**)
|
||||
|
||||
# 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.
|
||||
CARGO_TARGET := wasm32-unknown-unknown
|
||||
CARGO_PROFILE := wasm-release
|
||||
|
||||
.PHONY: clean full-clean web tarball
|
||||
|
||||
web: out/boids.js out/boids_bg.wasm out/index.html
|
||||
|
||||
tarball: boids_web_root.tar
|
||||
|
||||
boids_web_root.tar: out/boids.js out/boids_bg.wasm out/index.html
|
||||
tar -caf $@ $^
|
||||
|
||||
target/$(CARGO_TARGET)/$(CARGO_PROFILE)/another-boids-in-rust.wasm: $(SRCS) Cargo.lock Cargo.toml
|
||||
cargo build --profile $(CARGO_PROFILE) --target $(CARGO_TARGET)
|
||||
|
||||
out:
|
||||
mkdir $@
|
||||
|
||||
# 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.
|
||||
out/boids.js out/boids_bg.wasm &: 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
|
||||
|
||||
out/index.html: www/index.html
|
||||
cp -a $< $@
|
||||
|
||||
# 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.
|
||||
clean:
|
||||
rm -rf out/ boids_web_root.tar
|
||||
|
||||
# Delete everything, including the Cargo build cache. In case someone needs
|
||||
# this, I guess.
|
||||
full-clean: clean
|
||||
cargo clean
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
background-color: hsl(100, 10%, 60%);
|
||||
color: hsl(0, 0%, 15%);
|
||||
}
|
||||
h1 {
|
||||
background-color: rgb(72, 97, 72);
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
padding: 0.5em;
|
||||
}
|
||||
canvas {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-top: 1em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
outline-color: hsl(100, 100%, 15%);
|
||||
outline-style: outset;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(40%, 40%, 40%);
|
||||
}
|
||||
main {
|
||||
margin-left: auto;
|
||||
@@ -18,11 +30,12 @@
|
||||
width: 70%;
|
||||
}
|
||||
table {
|
||||
margin-bottom: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid;
|
||||
padding: 2px 4px;
|
||||
padding: 0.1em 0.3em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -91,7 +104,7 @@
|
||||
<tr>
|
||||
<td>Program Version</td>
|
||||
<!-- This version text is completely unchecked. I'll need to do something about that. -->
|
||||
<td><code>v0.4.0</code></td>
|
||||
<td><code>v0.6.0</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
</article>
|
||||
@@ -99,7 +112,11 @@
|
||||
<script type="module">
|
||||
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!")) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user