use std::collections::HashMap;
use bevy_math::Vec2;
use crate::{Agent, OrcaScratch, new_velocity_in};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct CrowdMember {
pub key: u64,
pub pos: Vec2,
pub vel: Vec2,
pub radius: f32,
pub max_speed: f32,
pub pref_vel: Option<Vec2>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct CrowdParams {
pub time_horizon: f32,
pub neighbour_range: f32,
pub max_neighbours: usize,
pub nudge: f32,
}
impl CrowdParams {
pub fn new(neighbour_range: f32) -> Self {
Self { time_horizon: 2.0, neighbour_range, max_neighbours: 10, nudge: 0.05 }
}
}
#[derive(Debug, Default)]
pub struct Crowd {
scratch: OrcaScratch,
cells: HashMap<(i32, i32), Vec<usize>>,
near: Vec<(f32, u64, usize)>,
neighbours: Vec<Agent>,
walls: Vec<(Vec2, f32)>,
out: Vec<Option<Vec2>>,
}
fn cell_of(pos: Vec2, range: f32) -> Option<(i32, i32)> {
if !pos.is_finite() || range.is_nan() || range <= 0.0 {
return None;
}
Some(((pos.x / range).floor() as i32, (pos.y / range).floor() as i32))
}
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = x;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
impl Crowd {
pub fn solve(
&mut self,
members: &[CrowdMember],
params: &CrowdParams,
dt: f32,
mut sees: impl FnMut(usize, usize) -> bool,
mut walls: impl FnMut(usize, &mut Vec<(Vec2, f32)>),
) -> &[Option<Vec2>] {
let range = params.neighbour_range;
let search = range > 0.0 && range.is_finite();
for list in self.cells.values_mut() {
list.clear();
}
for (i, m) in members.iter().enumerate() {
let Some(cell) = cell_of(m.pos, range).filter(|_| search) else {
continue;
};
self.cells.entry(cell).or_default().push(i);
}
self.out.clear();
self.out.resize(members.len(), None);
for i in 0..members.len() {
let m = members[i];
let (Some(pref), true) = (m.pref_vel, m.pos.is_finite()) else {
continue;
};
if !pref.is_finite() {
continue;
}
self.near.clear();
if search && let Some((cx, cy)) = cell_of(m.pos, range) {
for dx in -1..=1 {
for dy in -1..=1 {
let Some(list) = self.cells.get(&(cx + dx, cy + dy)) else {
continue;
};
for &j in list {
if j == i {
continue;
}
let other = members[j];
let dist = other.pos.distance(m.pos);
if dist > range {
continue;
}
if sees(i, j) {
self.near.push((dist, other.key, j));
}
}
}
}
}
self.near.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
let keep = params.max_neighbours.min(self.near.len());
self.neighbours.clear();
for &(_, _, j) in &self.near[..keep] {
let other = members[j];
self.neighbours.push(Agent {
pos: other.pos,
vel: other.vel,
radius: other.radius,
avoids: other.pref_vel.is_some(),
});
}
let wish = if keep == 0 {
pref
} else {
let side = if splitmix64(m.key) & 1 == 0 { 1.0 } else { -1.0 };
pref + pref.perp() * (params.nudge * side)
};
self.walls.clear();
walls(i, &mut self.walls);
let me = Agent { pos: m.pos, vel: m.vel, radius: m.radius, avoids: true };
let solved = new_velocity_in(
&mut self.scratch,
&me,
wish,
&self.neighbours,
&self.walls,
params.time_horizon,
dt,
m.max_speed,
);
self.out[i] = Some(solved);
}
&self.out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn member(key: u64, pos: Vec2, vel: Vec2, pref: Option<Vec2>) -> CrowdMember {
CrowdMember { key, pos, vel, radius: 0.5, max_speed: 1.5, pref_vel: pref }
}
#[test]
fn head_on_members_keep_their_distance() {
let params = CrowdParams::new(4.0);
let mut crowd = Crowd::default();
let (mut a, mut b) = (
member(1, Vec2::new(-2.0, 0.0), Vec2::new(1.0, 0.0), Some(Vec2::X)),
member(2, Vec2::new(2.0, 0.0), Vec2::new(-1.0, 0.0), Some(-Vec2::X)),
);
let dt = 1.0 / 60.0;
let mut min_gap = f32::INFINITY;
for _ in 0..240 {
let members = [a, b];
let solved = crowd.solve(&members, ¶ms, dt, |_, _| true, |_, buf| buf.clear());
if let Some(v) = solved[0] {
a.vel = v;
}
if let Some(v) = solved[1] {
b.vel = v;
}
a.pos += a.vel * dt;
b.pos += b.vel * dt;
min_gap = min_gap.min(a.pos.distance(b.pos));
}
let sum_radii = a.radius + b.radius;
assert!(
min_gap >= sum_radii - 0.05,
"members closed to {min_gap}, inside the {sum_radii} summed radii"
);
}
#[test]
fn an_unsteered_member_is_not_moved_and_is_avoided() {
let params = CrowdParams::new(4.0);
let mut crowd = Crowd::default();
let still = member(7, Vec2::new(0.0, 0.0), Vec2::ZERO, None);
let mover = member(8, Vec2::new(-1.5, 0.0), Vec2::new(1.0, 0.0), Some(Vec2::X));
let members = [still, mover];
let solved = crowd.solve(&members, ¶ms, 1.0 / 60.0, |_, _| true, |_, buf| buf.clear());
assert_eq!(solved[0], None, "an unsteered member must not be solved for");
let v = solved[1].expect("the steered member is solved");
assert!(v.is_finite());
assert!((v - Vec2::X).length() > 1.0e-3, "expected avoidance, got {v}");
}
#[test]
fn a_hidden_member_is_ignored() {
let params = CrowdParams::new(4.0);
let mut crowd = Crowd::default();
let a = member(1, Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0), Some(Vec2::X));
let b = member(2, Vec2::new(1.0, 0.0), Vec2::new(-1.0, 0.0), Some(-Vec2::X));
let members = [a, b];
let solved = crowd.solve(&members, ¶ms, 1.0 / 60.0, |i, j| i == j, |_, buf| buf.clear());
for (i, v) in solved.iter().enumerate() {
let v = v.expect("both are steered");
let pref = if i == 0 { Vec2::X } else { -Vec2::X };
assert!(
(v - pref).length() < 1.0e-4,
"member {i} should ignore the other and keep {pref}, got {v}"
);
}
}
#[test]
fn results_do_not_depend_on_member_order() {
let params = CrowdParams::new(4.0);
let members: Vec<CrowdMember> = (0..8)
.map(|i| {
let angle = (i as f32) * std::f32::consts::TAU / 8.0;
member(
i as u64,
Vec2::new(angle.cos(), angle.sin()) * 1.2,
Vec2::ZERO,
Some(Vec2::new(angle.cos(), angle.sin())),
)
})
.collect();
let mut reference = Crowd::default();
let base: Vec<(u64, Option<Vec2>)> = reference
.solve(&members, ¶ms, 1.0 / 60.0, |_, _| true, |_, buf| buf.clear())
.iter()
.enumerate()
.map(|(i, v)| (members[i].key, *v))
.collect();
let order: Vec<usize> =
(0..members.len()).map(|i| (members.len() - 1 - i + 3) % members.len()).collect();
let shuffled: Vec<CrowdMember> = order.iter().map(|&i| members[i]).collect();
let mut other = Crowd::default();
let got = other.solve(&shuffled, ¶ms, 1.0 / 60.0, |_, _| true, |_, buf| buf.clear());
for (k, &member) in shuffled.iter().enumerate() {
let expect = base.iter().find(|(key, _)| *key == member.key).expect("a key per member").1;
let actual = got[k];
match (expect, actual) {
(Some(a), Some(b)) => {
assert_eq!(
a.x.to_bits(),
b.x.to_bits(),
"key {}: x differs under reordering",
member.key
);
assert_eq!(
a.y.to_bits(),
b.y.to_bits(),
"key {}: y differs under reordering",
member.key
);
}
(None, None) => {}
_ => panic!("key {}: solved for in one order and not the other", member.key),
}
}
}
#[test]
fn a_bad_range_leaves_members_alone() {
let a = member(1, Vec2::new(-0.1, 0.0), Vec2::new(1.0, 0.0), Some(Vec2::X));
let b = member(2, Vec2::new(0.1, 0.0), Vec2::new(-1.0, 0.0), Some(-Vec2::X));
let members = [a, b];
for range in [0.0_f32, -1.0, f32::NAN] {
let params = CrowdParams::new(range);
let mut crowd = Crowd::default();
let solved = crowd.solve(&members, ¶ms, 1.0 / 60.0, |_, _| true, |_, buf| buf.clear());
assert_eq!(solved[0], Some(Vec2::X), "range {range} should leave the wish alone");
assert_eq!(solved[1], Some(-Vec2::X), "range {range} should leave the wish alone");
}
}
#[test]
fn a_non_finite_position_is_reported_unsolved() {
let params = CrowdParams::new(4.0);
let mut crowd = Crowd::default();
let broken = member(1, Vec2::new(f32::NAN, 0.0), Vec2::ZERO, Some(Vec2::X));
let fine = member(2, Vec2::new(2.0, 0.0), Vec2::ZERO, Some(-Vec2::X));
let members = [broken, fine];
let solved = crowd.solve(&members, ¶ms, 1.0 / 60.0, |_, _| true, |_, buf| buf.clear());
assert_eq!(solved[0], None);
assert_eq!(solved[1], Some(-Vec2::X), "the broken member must not be a neighbour");
}
}