Convert flocking systems to use Avian2d

This commit is contained in:
2025-12-24 16:02:46 -06:00
parent e252e3385c
commit 1846b7065e

View File

@@ -2,9 +2,6 @@ pub mod physics;
use avian2d::prelude::*; 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}; use bevy_inspector_egui::{InspectorOptions, prelude::ReflectInspectorOptions};
@@ -15,29 +12,23 @@ 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(
.insert_resource(FlockingParameters::new()) FixedUpdate,
.register_type::<FlockingParameters>() (
.insert_resource(MiscParams::new()) apply_velocity,
.register_type::<MiscParams>() turn_if_edge,
.add_systems(Startup, (spawn_camera, spawn_boids)) cohesion,
.add_systems( separation,
FixedUpdate, alignment,
( speed_controller,
apply_velocity, ),
turn_if_edge, );
cohesion,
separation,
alignment,
speed_controller,
),
);
} }
} }
@@ -82,12 +73,9 @@ impl MiscParams {
} }
#[derive(Component)] #[derive(Component)]
#[require(Velocity, Force, TrackedByKdTree)] #[require(Velocity, Force)]
pub(crate) struct Boid; pub(crate) struct Boid;
#[derive(Component, Default)]
pub struct TrackedByKdTree;
fn spawn_camera(mut commands: Commands) { fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2d); commands.spawn(Camera2d);
} }
@@ -153,7 +141,9 @@ fn turn_if_edge(
} }
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>, props: Res<FlockingParameters>,
) { ) {
@@ -163,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(), props.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));
@@ -190,7 +189,8 @@ fn 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>, props: Res<FlockingParameters>,
) { ) {
@@ -199,16 +199,20 @@ fn separation(
// 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(), props.view_range / 4.0) .shape_intersections(
&Collider::circle(props.view_range),
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))
} }
}) })
@@ -223,7 +227,7 @@ fn 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>, props: Res<FlockingParameters>,
@@ -236,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(), props.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 {