1use glam::Vec3;
13use std::f32::consts::TAU;
14
15#[derive(Clone, Debug)]
19pub struct FlockNeighbor {
20 pub position: Vec3,
21 pub velocity: Vec3,
22}
23
24#[derive(Clone, Debug)]
28pub struct Obstacle {
29 pub center: Vec3,
30 pub radius: f32,
31}
32
33impl Obstacle {
34 pub fn new(center: Vec3, radius: f32) -> Self { Self { center, radius } }
35
36 pub fn overlap(&self, pos: Vec3) -> f32 {
38 let dist = (pos - self.center).length();
39 (self.radius - dist).max(0.0)
40 }
41}
42
43#[derive(Clone, Debug)]
47pub struct FlockConfig {
48 pub alignment_weight: f32,
51 pub cohesion_weight: f32,
53 pub separation_weight: f32,
55 pub avoidance_weight: f32,
57 pub leader_weight: f32,
59 pub flee_weight: f32,
61 pub wander_weight: f32,
63 pub containment_weight: f32,
65
66 pub perception_radius: f32,
69 pub separation_radius: f32,
71 pub flee_radius: f32,
73 pub arrival_radius: f32,
75
76 pub max_speed: f32,
78 pub max_force: f32,
79 pub min_speed: f32,
80
81 pub bounds_min: Vec3,
83 pub bounds_max: Vec3,
84}
85
86impl Default for FlockConfig {
87 fn default() -> Self {
88 Self {
89 alignment_weight: 1.0,
90 cohesion_weight: 0.8,
91 separation_weight: 1.5,
92 avoidance_weight: 3.0,
93 leader_weight: 1.2,
94 flee_weight: 2.5,
95 wander_weight: 0.3,
96 containment_weight: 2.0,
97 perception_radius: 4.0,
98 separation_radius: 1.5,
99 flee_radius: 8.0,
100 arrival_radius: 2.0,
101 max_speed: 5.0,
102 max_force: 10.0,
103 min_speed: 0.5,
104 bounds_min: Vec3::splat(-50.0),
105 bounds_max: Vec3::splat(50.0),
106 }
107 }
108}
109
110#[derive(Clone, Debug)]
114pub struct Boid {
115 pub position: Vec3,
116 pub velocity: Vec3,
117 wander_angle: f32,
119 age: f32,
121 pub index: usize,
123}
124
125impl Boid {
126 pub fn new(position: Vec3, velocity: Vec3, index: usize) -> Self {
127 Self {
128 position,
129 velocity,
130 wander_angle: index as f32 * 2.399, age: 0.0,
132 index,
133 }
134 }
135
136 pub fn integrate(&mut self, steering: Vec3, dt: f32, config: &FlockConfig) {
138 let clamped = limit(steering, config.max_force);
139 self.velocity += clamped * dt;
140 self.velocity = limit(self.velocity, config.max_speed);
141
142 let speed = self.velocity.length();
144 if speed < config.min_speed && speed > 0.001 {
145 self.velocity = self.velocity / speed * config.min_speed;
146 }
147
148 self.position += self.velocity * dt;
149 self.age += dt;
150 }
151}
152
153pub fn compute_steering(
157 boid: &mut Boid,
158 neighbors: &[FlockNeighbor],
159 obstacles: &[Obstacle],
160 leader: Option<Vec3>,
161 predators: &[Vec3],
162 config: &FlockConfig,
163) -> Vec3 {
164 let mut total = Vec3::ZERO;
165
166 let (align, cohere, separate) = boid_forces(boid, neighbors, config);
168 total += align * config.alignment_weight;
169 total += cohere * config.cohesion_weight;
170 total += separate * config.separation_weight;
171
172 if config.avoidance_weight > 0.0 {
174 total += avoid_obstacles(boid, obstacles) * config.avoidance_weight;
175 }
176
177 if config.leader_weight > 0.0 {
179 if let Some(target) = leader {
180 total += seek_arrive(boid, target, config) * config.leader_weight;
181 }
182 }
183
184 if config.flee_weight > 0.0 && !predators.is_empty() {
186 total += flee_predators(boid, predators, config) * config.flee_weight;
187 }
188
189 if config.wander_weight > 0.0 {
191 total += wander(boid, config) * config.wander_weight;
192 }
193
194 if config.containment_weight > 0.0 {
196 total += contain(boid, config) * config.containment_weight;
197 }
198
199 total
200}
201
202fn boid_forces(
204 boid: &Boid,
205 neighbors: &[FlockNeighbor],
206 config: &FlockConfig,
207) -> (Vec3, Vec3, Vec3) {
208 if neighbors.is_empty() {
209 return (Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
210 }
211
212 let mut align_sum = Vec3::ZERO;
213 let mut cohere_sum = Vec3::ZERO;
214 let mut sep_sum = Vec3::ZERO;
215 let mut align_count = 0usize;
216 let mut cohere_count = 0usize;
217
218 for n in neighbors {
219 let delta = boid.position - n.position;
220 let dist = delta.length();
221
222 if dist < config.perception_radius && dist > 0.001 {
223 align_sum += n.velocity;
224 cohere_sum += n.position;
225 align_count += 1;
226 cohere_count += 1;
227
228 if dist < config.separation_radius {
229 sep_sum += delta / (dist * dist);
231 }
232 }
233 }
234
235 let align = if align_count > 0 {
236 let avg = align_sum / align_count as f32;
237 steer_toward(boid, avg.normalize_or_zero() * config.max_speed)
238 } else {
239 Vec3::ZERO
240 };
241
242 let cohere = if cohere_count > 0 {
243 let center = cohere_sum / cohere_count as f32;
244 steer_toward(boid, (center - boid.position).normalize_or_zero() * config.max_speed)
245 } else {
246 Vec3::ZERO
247 };
248
249 let separate = if sep_sum.length() > 0.001 {
250 steer_toward(boid, sep_sum.normalize_or_zero() * config.max_speed)
251 } else {
252 Vec3::ZERO
253 };
254
255 (align, cohere, separate)
256}
257
258fn avoid_obstacles(boid: &Boid, obstacles: &[Obstacle]) -> Vec3 {
260 let mut force = Vec3::ZERO;
261 let look_ahead = boid.velocity.normalize_or_zero() * 2.5;
262 let future_pos = boid.position + look_ahead;
263
264 for obs in obstacles {
265 let delta = future_pos - obs.center;
266 let dist = delta.length();
267 let threat_radius = obs.radius + 1.0; if dist < threat_radius && dist > 0.001 {
270 let push = delta / dist * (threat_radius - dist) / threat_radius;
271 force += push;
272 }
273 }
274
275 for obs in obstacles {
277 let overlap = obs.overlap(boid.position);
278 if overlap > 0.0 {
279 let dir = (boid.position - obs.center).normalize_or_zero();
280 force += dir * overlap * 5.0;
281 }
282 }
283
284 force
285}
286
287fn seek_arrive(boid: &Boid, target: Vec3, config: &FlockConfig) -> Vec3 {
289 let delta = target - boid.position;
290 let dist = delta.length();
291 if dist < 0.01 { return Vec3::ZERO; }
292
293 let desired_speed = if dist < config.arrival_radius {
294 config.max_speed * (dist / config.arrival_radius)
295 } else {
296 config.max_speed
297 };
298
299 let desired = delta / dist * desired_speed;
300 steer_toward(boid, desired)
301}
302
303fn flee_predators(boid: &Boid, predators: &[Vec3], config: &FlockConfig) -> Vec3 {
305 let mut force = Vec3::ZERO;
306 for &predator in predators {
307 let delta = boid.position - predator;
308 let dist = delta.length();
309 if dist < config.flee_radius && dist > 0.001 {
310 let urgency = 1.0 - dist / config.flee_radius;
311 force += delta.normalize_or_zero() * urgency;
312 }
313 }
314 if force.length() > 0.001 {
315 steer_toward(boid, force.normalize_or_zero() * config.max_speed)
316 } else {
317 Vec3::ZERO
318 }
319}
320
321fn wander(boid: &mut Boid, config: &FlockConfig) -> Vec3 {
323 let wander_radius = 1.5f32;
324 let wander_distance = 3.0f32;
325 let wander_jitter = 0.8f32;
326
327 let noise = lcg_f32(boid.index as u64, (boid.age * 10.0) as u64) * 2.0 - 1.0;
329 boid.wander_angle += noise * wander_jitter;
330
331 let circle_center = boid.velocity.normalize_or_zero() * wander_distance;
332 let wander_point = Vec3::new(
333 circle_center.x + boid.wander_angle.cos() * wander_radius,
334 circle_center.y + boid.wander_angle.sin() * wander_radius,
335 circle_center.z,
336 );
337
338 let _ = config; wander_point.normalize_or_zero()
340}
341
342fn contain(boid: &Boid, config: &FlockConfig) -> Vec3 {
344 let margin = 5.0f32;
345 let mut force = Vec3::ZERO;
346
347 let min = config.bounds_min;
348 let max = config.bounds_max;
349
350 if boid.position.x < min.x + margin { force.x += 1.0; }
351 if boid.position.x > max.x - margin { force.x -= 1.0; }
352 if boid.position.y < min.y + margin { force.y += 1.0; }
353 if boid.position.y > max.y - margin { force.y -= 1.0; }
354 if boid.position.z < min.z + margin { force.z += 1.0; }
355 if boid.position.z > max.z - margin { force.z -= 1.0; }
356
357 force
358}
359
360pub struct Flock {
364 pub boids: Vec<Boid>,
365 pub config: FlockConfig,
366 pub obstacles: Vec<Obstacle>,
367 pub leader: Option<Vec3>,
368 pub predators: Vec<Vec3>,
369}
370
371impl Flock {
372 pub fn new(config: FlockConfig) -> Self {
373 Self {
374 boids: Vec::new(),
375 config,
376 obstacles: Vec::new(),
377 leader: None,
378 predators: Vec::new(),
379 }
380 }
381
382 pub fn spawn_circle(&mut self, n: usize, center: Vec3, radius: f32) {
384 let start = self.boids.len();
385 for i in 0..n {
386 let angle = i as f32 / n as f32 * TAU;
387 let pos = center + Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0);
388 let vel_angle = angle + std::f32::consts::FRAC_PI_2;
389 let vel = Vec3::new(vel_angle.cos(), vel_angle.sin(), 0.0) * self.config.min_speed;
390 self.boids.push(Boid::new(pos, vel, start + i));
391 }
392 }
393
394 pub fn spawn_scatter(&mut self, n: usize, center: Vec3, radius: f32, seed: u64) {
396 let start = self.boids.len();
397 let mut rng = seed;
398 for i in 0..n {
399 rng = rng.wrapping_mul(0x6c62272e07bb0142).wrapping_add(0x62b821756295c58d);
400 let angle = (rng >> 32) as f32 / u32::MAX as f32 * TAU;
401 rng = rng.wrapping_mul(0x6c62272e07bb0142).wrapping_add(0x62b821756295c58d);
402 let r = ((rng >> 32) as f32 / u32::MAX as f32).sqrt() * radius;
403 let pos = center + Vec3::new(angle.cos() * r, angle.sin() * r, 0.0);
404 let vel_angle = angle + 1.57;
405 let vel = Vec3::new(vel_angle.cos(), vel_angle.sin(), 0.0) * self.config.min_speed;
406 self.boids.push(Boid::new(pos, vel, start + i));
407 }
408 }
409
410 pub fn tick(&mut self, dt: f32) {
412 let n = self.boids.len();
413 if n == 0 { return; }
414
415 let snapshot: Vec<FlockNeighbor> = self.boids.iter()
417 .map(|b| FlockNeighbor { position: b.position, velocity: b.velocity })
418 .collect();
419
420 let forces: Vec<Vec3> = (0..n).map(|i| {
422 let boid = &self.boids[i];
423 let neighbors: Vec<FlockNeighbor> = snapshot.iter().enumerate()
424 .filter(|(j, _)| *j != i)
425 .map(|(_, n)| FlockNeighbor { position: n.position, velocity: n.velocity })
426 .collect();
427
428 let mut tmp_boid = boid.clone();
430 compute_steering(
431 &mut tmp_boid,
432 &neighbors,
433 &self.obstacles,
434 self.leader,
435 &self.predators,
436 &self.config,
437 )
438 }).collect();
439
440 for (boid, force) in self.boids.iter_mut().zip(forces.iter()) {
442 boid.integrate(*force, dt, &self.config);
443 }
444 }
445
446 pub fn add_obstacle(&mut self, center: Vec3, radius: f32) {
448 self.obstacles.push(Obstacle::new(center, radius));
449 }
450
451 pub fn set_leader(&mut self, pos: Option<Vec3>) {
453 self.leader = pos;
454 }
455
456 pub fn set_predators(&mut self, predators: Vec<Vec3>) {
458 self.predators = predators;
459 }
460
461 pub fn len(&self) -> usize { self.boids.len() }
463 pub fn is_empty(&self) -> bool { self.boids.is_empty() }
464
465 pub fn centroid(&self) -> Vec3 {
467 if self.boids.is_empty() { return Vec3::ZERO; }
468 let sum: Vec3 = self.boids.iter().map(|b| b.position).sum();
469 sum / self.boids.len() as f32
470 }
471
472 pub fn avg_speed(&self) -> f32 {
474 if self.boids.is_empty() { return 0.0; }
475 self.boids.iter().map(|b| b.velocity.length()).sum::<f32>() / self.boids.len() as f32
476 }
477
478 pub fn cohesion_metric(&self) -> f32 {
481 if self.boids.is_empty() { return 1.0; }
482 let c = self.centroid();
483 let avg_dist = self.boids.iter()
484 .map(|b| (b.position - c).length())
485 .sum::<f32>() / self.boids.len() as f32;
486 1.0 / (1.0 + avg_dist)
487 }
488
489 pub fn polarization(&self) -> f32 {
491 if self.boids.is_empty() { return 0.0; }
492 let sum: Vec3 = self.boids.iter()
493 .map(|b| b.velocity.normalize_or_zero())
494 .sum();
495 sum.length() / self.boids.len() as f32
496 }
497
498 pub fn remove_if(&mut self, predicate: impl Fn(&Boid) -> bool) {
500 self.boids.retain(|b| !predicate(b));
501 }
502}
503
504pub fn flock_force(
509 pos: Vec3,
510 vel: Vec3,
511 neighbors: &[FlockNeighbor],
512 alignment: f32,
513 cohesion: f32,
514 separation: f32,
515 radius: f32,
516) -> Vec3 {
517 if neighbors.is_empty() { return Vec3::ZERO; }
518
519 let mut avg_vel = Vec3::ZERO;
520 let mut avg_pos = Vec3::ZERO;
521 let mut sep = Vec3::ZERO;
522 let mut count = 0usize;
523
524 for n in neighbors {
525 let delta = pos - n.position;
526 let dist = delta.length();
527 if dist < radius && dist > 0.001 {
528 avg_vel += n.velocity;
529 avg_pos += n.position;
530 count += 1;
531 if dist < radius * 0.5 {
532 sep += delta / (dist * dist);
533 }
534 }
535 }
536
537 if count == 0 { return Vec3::ZERO; }
538
539 let n = count as f32;
540 avg_vel /= n;
541 avg_pos /= n;
542
543 let align = (avg_vel - vel) * alignment;
544 let cohese = (avg_pos - pos) * cohesion;
545 let repel = sep * separation;
546
547 align + cohese + repel
548}
549
550fn steer_toward(boid: &Boid, desired: Vec3) -> Vec3 {
554 desired - boid.velocity
555}
556
557fn limit(v: Vec3, max: f32) -> Vec3 {
559 let len = v.length();
560 if len > max && len > 0.001 { v / len * max } else { v }
561}
562
563fn lcg_f32(seed1: u64, seed2: u64) -> f32 {
565 let x = seed1.wrapping_mul(0x9e3779b97f4a7c15)
566 .wrapping_add(seed2)
567 .wrapping_mul(0x6c62272e07bb0142);
568 (x >> 32) as f32 / u32::MAX as f32
569}
570
571#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn flock_ticks_without_panic() {
579 let mut flock = Flock::new(FlockConfig::default());
580 flock.spawn_circle(10, Vec3::ZERO, 3.0);
581 assert_eq!(flock.len(), 10);
582 flock.tick(0.016);
583 flock.tick(0.016);
584 }
585
586 #[test]
587 fn separation_pushes_apart() {
588 let pos = Vec3::ZERO;
589 let vel = Vec3::X;
590 let neighbors = vec![
591 FlockNeighbor { position: Vec3::new(0.5, 0.0, 0.0), velocity: Vec3::X },
592 ];
593 let force = flock_force(pos, vel, &neighbors, 0.0, 0.0, 1.0, 4.0);
594 assert!(force.x < 0.0);
596 }
597
598 #[test]
599 fn cohesion_metric_tight_flock() {
600 let mut flock = Flock::new(FlockConfig::default());
601 flock.spawn_circle(6, Vec3::ZERO, 0.1);
602 let metric = flock.cohesion_metric();
603 assert!(metric > 0.8);
604 }
605
606 #[test]
607 fn flock_flee_changes_direction() {
608 let config = FlockConfig { flee_weight: 5.0, ..Default::default() };
609 let mut flock = Flock::new(config);
610 flock.spawn_circle(5, Vec3::new(3.0, 0.0, 0.0), 0.5);
611 flock.set_predators(vec![Vec3::ZERO]);
612
613 let init_centroid = flock.centroid();
614 for _ in 0..10 {
615 flock.tick(0.016);
616 }
617 let after_centroid = flock.centroid();
618 assert!(after_centroid.x > init_centroid.x);
620 }
621}