Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df86f05876 | |||
| 68d6de9a83 | |||
| ccd8a12b79 | |||
| f2a71e712a | |||
| dca8bcdaa7 | |||
| e6e56506f8 | |||
| bc0630b4ae | |||
| 76a6b4f999 | |||
| 451311126d | |||
| 7b380196a5 | |||
| 8b61d38636 | |||
| b3cf47e684 | |||
| 69403bc6ca | |||
| a6240c844a | |||
| 3eb23fb4bf | |||
| 532025b42f | |||
| 4150f85ccc | |||
| 214da65db5 | |||
| 325e515a7b | |||
| e85114c4c8 | |||
| feeeb15d22 |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
runner = "wasm-server-runner"
|
||||||
2838
Cargo.lock
generated
2838
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -1,11 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "another-boids-in-rust"
|
name = "another-boids-in-rust"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = "0.14.0"
|
bevy = "0.16.0"
|
||||||
bevy_spatial = "0.9.0"
|
|
||||||
|
|
||||||
# Grand-dependency pins
|
# Grand-dependency pins
|
||||||
ab_glyph = "0.2.16"
|
ab_glyph = "0.2.16"
|
||||||
@@ -17,6 +16,16 @@ miniz-sys = "0.1.10"
|
|||||||
nonmax = "0.5.1"
|
nonmax = "0.5.1"
|
||||||
rand = "0.8.0"
|
rand = "0.8.0"
|
||||||
|
|
||||||
|
# Use regular bevy_spatial on non-WASM builds
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
bevy_spatial = "0.11.0"
|
||||||
|
|
||||||
|
# Use bevy_spatial *without* the kdtree_rayon feature when building for WASM.
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.bevy_spatial]
|
||||||
|
version = "0.11.0"
|
||||||
|
default-features = false
|
||||||
|
features = ["kdtree"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
use bevy::{prelude::*, sprite::MaterialMesh2dBundle};
|
pub mod physics;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
use bevy_spatial::{
|
use bevy_spatial::{
|
||||||
kdtree::KDTree2, AutomaticUpdate, SpatialAccess, SpatialStructure, TransformMode,
|
AutomaticUpdate, SpatialAccess, SpatialStructure, TransformMode, kdtree::KDTree2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::birdoids::physics::{Force, Velocity, apply_velocity};
|
||||||
|
|
||||||
const BACKGROUND_COLOR: Color = Color::srgb(0.4, 0.4, 0.4);
|
const BACKGROUND_COLOR: Color = Color::srgb(0.4, 0.4, 0.4);
|
||||||
const PLAYERBOID_COLOR: Color = Color::srgb(1.0, 0.0, 0.0);
|
const PLAYERBOID_COLOR: Color = Color::srgb(1.0, 0.0, 0.0);
|
||||||
const TURN_FACTOR: f32 = 1.0;
|
const TURN_FACTOR: f32 = 1.0;
|
||||||
const BOID_VIEW_RANGE: f32 = 50.0;
|
const BOID_VIEW_RANGE: f32 = 15.0;
|
||||||
const COHESION_FACTOR: f32 = 1.0;
|
const COHESION_FACTOR: f32 = 1.0;
|
||||||
const SEPARATION_FACTOR: f32 = 1.0;
|
const SEPARATION_FACTOR: f32 = 10.0;
|
||||||
const ALIGNMENT_FACTOR: f32 = 1.0;
|
const ALIGNMENT_FACTOR: f32 = 1.0;
|
||||||
const SPACEBRAKES_COEFFICIENT: f32 = 0.01;
|
const SPACEBRAKES_COEFFICIENT: f32 = 0.5;
|
||||||
|
const LOW_SPEED_THRESHOLD: f32 = 50.0;
|
||||||
|
const HIGH_SPEED_THRESHOLD: f32 = 200.0;
|
||||||
|
|
||||||
pub struct BoidsPlugin;
|
pub struct BoidsPlugin;
|
||||||
|
|
||||||
@@ -33,13 +39,14 @@ impl Plugin for BoidsPlugin {
|
|||||||
cohesion,
|
cohesion,
|
||||||
separation,
|
separation,
|
||||||
alignment,
|
alignment,
|
||||||
// space_brakes,
|
speed_controller,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
|
#[require(Velocity, Force, TrackedByKdTree)]
|
||||||
pub(crate) struct Boid;
|
pub(crate) struct Boid;
|
||||||
|
|
||||||
// It's a Boid, but with an extra component so the player
|
// It's a Boid, but with an extra component so the player
|
||||||
@@ -47,36 +54,11 @@ pub(crate) struct Boid;
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct PlayerBoid;
|
struct PlayerBoid;
|
||||||
|
|
||||||
#[derive(Component, Deref, DerefMut)]
|
#[derive(Component, Default)]
|
||||||
pub(crate) struct Velocity(Vec3);
|
pub struct TrackedByKdTree;
|
||||||
|
|
||||||
#[derive(Component, Default, Deref, DerefMut, PartialEq, Debug)]
|
|
||||||
pub(crate) struct Force(Vec3);
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub(crate) struct TrackedByKdTree;
|
|
||||||
|
|
||||||
#[derive(Bundle)]
|
|
||||||
struct BoidBundle {
|
|
||||||
boid: Boid,
|
|
||||||
velocity: Velocity,
|
|
||||||
accel: Force,
|
|
||||||
spatial: TrackedByKdTree,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoidBundle {
|
|
||||||
fn new(vel: Vec3) -> Self {
|
|
||||||
Self {
|
|
||||||
boid: Boid,
|
|
||||||
velocity: Velocity(vel),
|
|
||||||
accel: Force(Vec3::ZERO),
|
|
||||||
spatial: TrackedByKdTree,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_camera(mut commands: Commands) {
|
fn spawn_camera(mut commands: Commands) {
|
||||||
commands.spawn(Camera2dBundle::default());
|
commands.spawn(Camera2d);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_boids(
|
fn spawn_boids(
|
||||||
@@ -89,34 +71,32 @@ fn spawn_boids(
|
|||||||
let frac = 2.0 * std::f32::consts::PI / (num_boids as f32) * (i as f32);
|
let frac = 2.0 * std::f32::consts::PI / (num_boids as f32) * (i as f32);
|
||||||
let vel = Vec3::new(frac.cos() * 1.0, frac.sin() * 1.0, 0.0) * 10.0;
|
let vel = Vec3::new(frac.cos() * 1.0, frac.sin() * 1.0, 0.0) * 10.0;
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
BoidBundle::new(vel),
|
Boid,
|
||||||
MaterialMesh2dBundle {
|
Velocity(vel),
|
||||||
mesh: meshes.add(Circle::default()).into(),
|
Mesh2d(meshes.add(Circle::new(1.0))),
|
||||||
material: materials.add(Color::srgb(1.0, 1.0, 1.0)),
|
MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 1.0))),
|
||||||
transform: Transform {
|
Transform::from_translation(vel * 20.0),
|
||||||
translation: vel * 20.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
BoidBundle::new(Vec3::new(0.0, 0.0, 0.0)),
|
Boid,
|
||||||
PlayerBoid,
|
PlayerBoid,
|
||||||
MaterialMesh2dBundle {
|
Mesh2d(meshes.add(Triangle2d::default())),
|
||||||
mesh: meshes.add(Triangle2d::default()).into(),
|
MeshMaterial2d(materials.add(PLAYERBOID_COLOR)),
|
||||||
material: materials.add(PLAYERBOID_COLOR),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn space_brakes(mut mobs: Query<&mut Force, With<Boid>>) {
|
/// Controls the boid's minimum and maximum speed according to a low- and
|
||||||
for mut accel in &mut mobs {
|
/// high-threshold. Boids moving too slow are sped up, and boids moving too
|
||||||
let braking_dir = -accel.0 * SPACEBRAKES_COEFFICIENT;
|
/// fast are slowed down.
|
||||||
accel.0 += braking_dir;
|
fn speed_controller(mut mobs: Query<(&Velocity, &mut Force), With<Boid>>) {
|
||||||
|
for (vel, mut impulse) in &mut mobs {
|
||||||
|
if vel.0.length() < LOW_SPEED_THRESHOLD {
|
||||||
|
impulse.0 += vel.0 * SPACEBRAKES_COEFFICIENT;
|
||||||
|
} else if vel.0.length() > HIGH_SPEED_THRESHOLD {
|
||||||
|
impulse.0 += -vel.0 * SPACEBRAKES_COEFFICIENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +104,7 @@ fn turn_if_edge(
|
|||||||
mut query: Query<(&mut Transform, &mut Velocity), With<Boid>>,
|
mut query: Query<(&mut Transform, &mut Velocity), With<Boid>>,
|
||||||
window: Query<&Window>,
|
window: Query<&Window>,
|
||||||
) {
|
) {
|
||||||
if let Ok(window) = window.get_single() {
|
if let Ok(window) = window.single() {
|
||||||
let (width, height) = (window.resolution.width(), window.resolution.height());
|
let (width, height) = (window.resolution.width(), window.resolution.height());
|
||||||
for (transform, mut velocity) in &mut query {
|
for (transform, mut velocity) in &mut query {
|
||||||
let boid_pos = transform.translation.xy();
|
let boid_pos = transform.translation.xy();
|
||||||
@@ -145,25 +125,18 @@ fn turn_if_edge(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity, &mut Force)>, time: Res<Time>) {
|
|
||||||
for (mut transform, velocity, mut acceleration) in &mut query {
|
|
||||||
let delta_v = **acceleration * time.delta_seconds();
|
|
||||||
**acceleration = Vec3::ZERO;
|
|
||||||
let delta_position = (**velocity + delta_v) * time.delta_seconds();
|
|
||||||
transform.translation += delta_position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_keyboard(
|
fn check_keyboard(
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
mut app_exit_events: ResMut<Events<bevy::app::AppExit>>,
|
mut app_exit_events: ResMut<Events<bevy::app::AppExit>>,
|
||||||
mut query: Query<&mut Velocity, With<PlayerBoid>>,
|
mut query: Query<&mut Force, With<PlayerBoid>>,
|
||||||
) {
|
) {
|
||||||
if keyboard_input.just_pressed(KeyCode::KeyQ) {
|
if keyboard_input.just_pressed(KeyCode::KeyQ) {
|
||||||
app_exit_events.send(bevy::app::AppExit::Success);
|
app_exit_events.send(bevy::app::AppExit::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pvelocity = query.single_mut();
|
let mut impulse = query
|
||||||
|
.single_mut()
|
||||||
|
.expect("[birdoids_plugin::check_keyboard()] ->> There seems to be more than one player... How did that happen?");
|
||||||
let mut dir = Vec2::ZERO;
|
let mut dir = Vec2::ZERO;
|
||||||
if keyboard_input.pressed(KeyCode::ArrowLeft) {
|
if keyboard_input.pressed(KeyCode::ArrowLeft) {
|
||||||
dir.x -= 1.0;
|
dir.x -= 1.0;
|
||||||
@@ -178,53 +151,78 @@ fn check_keyboard(
|
|||||||
dir.y += 1.0;
|
dir.y += 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
**pvelocity = **pvelocity + dir.extend(0.0);
|
**impulse += dir.extend(0.0) * 50.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cohesion(
|
fn cohesion(
|
||||||
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
||||||
mut boids: Query<(&Transform, &mut Force), With<Boid>>,
|
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
|
||||||
) {
|
) {
|
||||||
// for each boid
|
// for each boid
|
||||||
// find neighbors
|
// find neighbors
|
||||||
// find center-of-mass of neighbors
|
// find center-of-mass of neighbors
|
||||||
// find vector from boid to flock CoM
|
// find vector from boid to flock CoM
|
||||||
// apply force
|
// apply force
|
||||||
for (transform, mut acceleration) in &mut boids {
|
for (this_entt, transform, mut force) in &mut boids {
|
||||||
let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE);
|
let (len, sum) = spatial_tree
|
||||||
if let Some(center_mass) = center_of_boids(neighbors.iter().map(|boid| boid.0)) {
|
.within_distance(transform.translation.xy(), BOID_VIEW_RANGE)
|
||||||
let force = cohesive_force(center_mass, transform.translation.xy()).unwrap_or_default();
|
.iter()
|
||||||
acceleration.0 += *force * COHESION_FACTOR;
|
.filter_map(|(pos, entt)| {
|
||||||
}
|
// Skip self-comparison. A boid should not try to separate from itself.
|
||||||
|
let entt = entt
|
||||||
|
.expect("within_distance gave me an entity... with no entity ID... somehow");
|
||||||
|
if this_entt == entt { None } else { Some(pos) }
|
||||||
|
})
|
||||||
|
.enumerate()
|
||||||
|
.fold((0, Vec2::ZERO), |(_len, com), (idx, pos)| (idx, com + pos));
|
||||||
|
|
||||||
|
// Skip to next boid if the current one has no neighbors.
|
||||||
|
let center_of_mass = if len > 0 {
|
||||||
|
sum / ((len + 1) as f32)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let impulse = cohesive_force(center_of_mass, transform.translation.xy()).expect("damn");
|
||||||
|
|
||||||
|
force.0 -= impulse.0 * COHESION_FACTOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn separation(
|
fn separation(
|
||||||
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
||||||
mut boids: Query<(&Transform, &mut Force), With<Boid>>,
|
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
|
||||||
) {
|
) {
|
||||||
// for each boid
|
// for each boid
|
||||||
// find neighbors
|
// find neighbors
|
||||||
// sum force from neighbors
|
// sum force from neighbors
|
||||||
// apply force
|
// apply force
|
||||||
for (boid_transform, mut boid_acceleration) in &mut boids {
|
for (this_entt, tsfm, mut force) in &mut boids {
|
||||||
let neighbors =
|
let impulse = spatial_tree
|
||||||
spatial_tree.within_distance(boid_transform.translation.xy(), BOID_VIEW_RANGE / 4.0);
|
.within_distance(tsfm.translation.xy(), BOID_VIEW_RANGE / 4.0)
|
||||||
let accel = neighbors.iter().map(|(pos, _)| pos.extend(0.0)).fold(
|
.iter()
|
||||||
Vec3::ZERO,
|
.filter_map(|(pos, entt)| {
|
||||||
|accumulator, neighbor| {
|
// Skip self-comparison. A boid should not try to separate from itself.
|
||||||
let force = separation_force(boid_transform.translation.xy(), neighbor.xy())
|
let entt = entt
|
||||||
.unwrap_or_default();
|
.expect("within_distance gave me an entity... with no entity ID... somehow");
|
||||||
accumulator + *force * SEPARATION_FACTOR
|
if this_entt == entt {
|
||||||
},
|
None
|
||||||
);
|
} else {
|
||||||
boid_acceleration.0 += accel;
|
Some(pos.extend(0.0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fold(Vec3::ZERO, |acc, other| {
|
||||||
|
// let force = tsfm.translation - other;
|
||||||
|
let force = separation_force(tsfm.translation.xy(), other.xy()).expect("angy");
|
||||||
|
acc + force.0
|
||||||
|
});
|
||||||
|
force.0 += impulse * SEPARATION_FACTOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alignment(
|
fn alignment(
|
||||||
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
spatial_tree: Res<KDTree2<TrackedByKdTree>>,
|
||||||
mut boids: Query<(&Transform, &Velocity, &mut Force), With<Boid>>,
|
mut boids: Query<(Entity, &Transform, &mut Force), With<Boid>>,
|
||||||
boid_velocities: Query<&Velocity, With<Boid>>,
|
boid_velocities: Query<&Velocity, With<Boid>>,
|
||||||
) {
|
) {
|
||||||
// for each boid
|
// for each boid
|
||||||
@@ -234,27 +232,36 @@ fn alignment(
|
|||||||
// perpendicular so that magnitude is constant
|
// perpendicular so that magnitude is constant
|
||||||
// apply steering force
|
// apply steering force
|
||||||
|
|
||||||
for (transform, velocity, mut acceleration) in &mut boids {
|
for (this_entt, transform, mut force) in &mut boids {
|
||||||
let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE);
|
let neighbors = spatial_tree.within_distance(transform.translation.xy(), BOID_VIEW_RANGE);
|
||||||
// averaging divides by length. Guard against an empty set of neighbors
|
// averaging divides by length. Guard against an empty set of neighbors
|
||||||
// so that we don't divide by zero.
|
let (len, sum) = neighbors
|
||||||
if neighbors.len() > 0 {
|
.iter()
|
||||||
if let Some(avg_velocity) =
|
// Extract the velocities by `get()`ing from another query param.
|
||||||
velocity_of_boids(neighbors.iter().map(|(vel, opt_entity)| {
|
.filter_map(|(_pos, maybe_entt)| {
|
||||||
// I've observed no panics in the old version, nor the debug_plugins version
|
let entt = maybe_entt
|
||||||
// I'm not clear on the conditions that cause a None option, but I want to
|
.expect("Neighbor boid has no Entity ID. I don't know what this means");
|
||||||
// crash when I find one.
|
if this_entt == entt {
|
||||||
let entity_id = opt_entity.unwrap_or_else(|| panic!("Boid has no Entity ID!"));
|
None
|
||||||
let vel = boid_velocities
|
} else {
|
||||||
.get(entity_id)
|
let vel = boid_velocities.get(entt).expect("Boid has no velocity!");
|
||||||
.unwrap_or_else(|_| panic!("Boid has no velocity!"));
|
Some(vel.xy())
|
||||||
(*vel).xy()
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
let deviation = -velocity.0 + avg_velocity.extend(0.0);
|
|
||||||
acceleration.0 += deviation * ALIGNMENT_FACTOR;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.enumerate()
|
||||||
|
.fold((0, Vec2::ZERO), |(_len, vel_acc), (idx, vel)| {
|
||||||
|
(idx, vel_acc + vel)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip to next boid if the current one has no neighbors.
|
||||||
|
let avg = if len > 0 {
|
||||||
|
sum / ((len + 1) as f32)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let boid_vel = boid_velocities.get(this_entt).unwrap();
|
||||||
|
force.0 += (avg.extend(0.0) - boid_vel.0) * ALIGNMENT_FACTOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,18 +274,9 @@ pub(crate) fn velocity_of_boids(points: impl Iterator<Item = Vec2>) -> Option<Ve
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn average_of_vec2s(points: impl Iterator<Item = Vec2>) -> Option<Vec2> {
|
fn average_of_vec2s(points: impl Iterator<Item = Vec2>) -> Option<Vec2> {
|
||||||
// Average the points by summing them all together, and dividing by
|
let (len, sum) = points
|
||||||
// the total count.
|
.enumerate()
|
||||||
// Passing the points as an iterator means we lose the length of the
|
.fold((0, Vec2::ZERO), |(_len, sum), (idx, point)| {
|
||||||
// list. The `.enumerate()` iterator reintroduces that count.
|
|
||||||
let mut points = points.enumerate();
|
|
||||||
|
|
||||||
// Empty iterators have no points and so no center of mass.
|
|
||||||
// Try to get the first one, but exit with None if it doesn't yield.
|
|
||||||
let init = points.next()?;
|
|
||||||
|
|
||||||
// if we get one, fold all the remaining values into it.
|
|
||||||
let (len, sum) = points.fold(init, |(len, sum), (idx, point)| {
|
|
||||||
// replace length with most recent index
|
// replace length with most recent index
|
||||||
// add running sum & new point for new running sum
|
// add running sum & new point for new running sum
|
||||||
(idx, sum + point)
|
(idx, sum + point)
|
||||||
@@ -329,9 +327,9 @@ fn separation_force(boid: Vec2, target: Vec2) -> Option<Force> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::birdoids_plugin::{cohesive_force, separation_force};
|
use crate::birdoids::{cohesive_force, separation_force};
|
||||||
|
|
||||||
use super::{Force, BOID_VIEW_RANGE};
|
use super::{BOID_VIEW_RANGE, physics::Force};
|
||||||
|
|
||||||
// forces are relative to the boid's view range, so all
|
// forces are relative to the boid's view range, so all
|
||||||
// distances need to be fractions of that
|
// distances need to be fractions of that
|
||||||
23
src/birdoids/physics.rs
Normal file
23
src/birdoids/physics.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Component, Default, Deref, DerefMut)]
|
||||||
|
pub struct Velocity(pub Vec3);
|
||||||
|
|
||||||
|
#[derive(Component, Default, Deref, DerefMut, PartialEq, Debug)]
|
||||||
|
pub struct Force(pub Vec3);
|
||||||
|
|
||||||
|
pub fn apply_velocity(
|
||||||
|
mut query: Query<(&mut Transform, &mut Velocity, &mut Force)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (mut transform, mut velocity, mut force) in &mut query {
|
||||||
|
// integrate forces into new velocity (assume mass of 1kg)
|
||||||
|
let delta_v = **force * time.delta_secs();
|
||||||
|
velocity.0 += delta_v;
|
||||||
|
force.0 = Vec3::ZERO; // clear force value
|
||||||
|
|
||||||
|
// integrate velocity into position
|
||||||
|
let delta_p = **velocity * time.delta_secs();
|
||||||
|
transform.translation += delta_p;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use bevy::{prelude::*, sprite::MaterialMesh2dBundle, window::PrimaryWindow};
|
use bevy::{prelude::*, window::PrimaryWindow};
|
||||||
use bevy_spatial::{kdtree::KDTree2, SpatialAccess};
|
use bevy_spatial::{SpatialAccess, kdtree::KDTree2};
|
||||||
|
|
||||||
use crate::birdoids_plugin::{
|
use crate::birdoids::{
|
||||||
center_of_boids, velocity_of_boids, Boid, Force, TrackedByKdTree, Velocity,
|
Boid, TrackedByKdTree, center_of_boids, physics::Force, physics::Velocity, velocity_of_boids,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCANRADIUS: f32 = 50.0;
|
const SCANRADIUS: f32 = 50.0;
|
||||||
@@ -22,13 +22,8 @@ fn setup(
|
|||||||
) {
|
) {
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
ScannerWidget::default(),
|
ScannerWidget::default(),
|
||||||
MaterialMesh2dBundle {
|
Mesh2d(meshes.add(Annulus::new(SCANRADIUS - 1.0, SCANRADIUS))),
|
||||||
mesh: meshes
|
MeshMaterial2d(materials.add(Color::srgb(0.0, 0.0, 0.0))),
|
||||||
.add(Annulus::new(SCANRADIUS - 1.0, SCANRADIUS))
|
|
||||||
.into(),
|
|
||||||
material: materials.add(Color::srgb(0.0, 0.0, 0.0)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +66,8 @@ fn update_cursor(
|
|||||||
) {
|
) {
|
||||||
// I'm trusting that only one thing has the `Cursor` component
|
// I'm trusting that only one thing has the `Cursor` component
|
||||||
// It's defined here in this module, so that *should* be the case...
|
// It's defined here in this module, so that *should* be the case...
|
||||||
let win = window.get_single().unwrap();
|
let win = window.single().unwrap();
|
||||||
let (cam, cam_transform) = camera.get_single().unwrap();
|
let (cam, cam_transform) = camera.single().unwrap();
|
||||||
// the cursor might not be on the window. Only adjust position when it is.
|
// the cursor might not be on the window. Only adjust position when it is.
|
||||||
if let Some(cursor_pos_in_window) = win.cursor_position() {
|
if let Some(cursor_pos_in_window) = win.cursor_position() {
|
||||||
// transform the window position into world space
|
// transform the window position into world space
|
||||||
@@ -81,7 +76,7 @@ fn update_cursor(
|
|||||||
let cursor_in_world = cam
|
let cursor_in_world = cam
|
||||||
.viewport_to_world_2d(cam_transform, cursor_pos_in_window)
|
.viewport_to_world_2d(cam_transform, cursor_pos_in_window)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut cursor = cursor_query.get_single_mut().unwrap();
|
let mut cursor = cursor_query.single_mut().unwrap();
|
||||||
cursor.translation = cursor_in_world.extend(0.0);
|
cursor.translation = cursor_in_world.extend(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +89,7 @@ fn update_scanner_mode(
|
|||||||
mut scanner_query: Query<(&mut SelectionMode, &mut ScannerMode), With<Cursor>>,
|
mut scanner_query: Query<(&mut SelectionMode, &mut ScannerMode), With<Cursor>>,
|
||||||
) {
|
) {
|
||||||
// I'm making another assertion that there is exactly one scanner.
|
// I'm making another assertion that there is exactly one scanner.
|
||||||
let (mut select_mode, mut scan_mode) = scanner_query.get_single_mut().unwrap();
|
let (mut select_mode, mut scan_mode) = scanner_query.single_mut().unwrap();
|
||||||
|
|
||||||
// Assign selection mode
|
// Assign selection mode
|
||||||
if keycodes.just_pressed(KeyCode::Digit1) {
|
if keycodes.just_pressed(KeyCode::Digit1) {
|
||||||
@@ -112,7 +107,7 @@ fn update_scanner_mode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_gizmo_config(query: Query<(&SelectionMode, &ScannerMode), With<Cursor>>) {
|
fn print_gizmo_config(query: Query<(&SelectionMode, &ScannerMode), With<Cursor>>) {
|
||||||
let (select, scan) = query.get_single().unwrap();
|
let (select, scan) = query.single().unwrap();
|
||||||
println!("Selection: {select:?}, Scanning: {scan:?}");
|
println!("Selection: {select:?}, Scanning: {scan:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +118,7 @@ fn do_scan(
|
|||||||
/* Push info to summary somewhere */
|
/* Push info to summary somewhere */
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
) {
|
) {
|
||||||
let (cursor_pos, select_mode, scan_mode) = scanner_query.get_single().unwrap();
|
let (cursor_pos, select_mode, scan_mode) = scanner_query.single().unwrap();
|
||||||
match select_mode {
|
match select_mode {
|
||||||
SelectionMode::NearestSingle => todo!(),
|
SelectionMode::NearestSingle => todo!(),
|
||||||
SelectionMode::CircularArea => {
|
SelectionMode::CircularArea => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
mod birdoids_plugin;
|
mod birdoids;
|
||||||
mod debug_plugin;
|
mod debug_plugin;
|
||||||
|
|
||||||
use birdoids_plugin::BoidsPlugin;
|
use birdoids::BoidsPlugin;
|
||||||
use debug_plugin::BoidsDebugPlugin;
|
use debug_plugin::BoidsDebugPlugin;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user