1use ponko::*;
11
12#[derive(serde::Serialize, serde::Deserialize)]
14struct Vel(Vec2);
15
16const RADIUS: f32 = 24.0;
17const MAX_SPEED: f32 = 140.0;
18const MIN_SPEED: f32 = 60.0;
19
20fn env_number(name: &str, default: f64) -> f64 {
21 std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
22}
23
24fn flocking(bounds: Vec2) -> impl FnMut(&mut Ctx) {
28 let mut grid = SpatialGrid::with_data(RADIUS);
29 move |ctx: &mut Ctx| {
30 let dt = ctx.dt();
31 if ctx.sim_tick().is_multiple_of(SPATIAL_SORT_EVERY) {
34 ctx.world.sort_by::<Transform2D>(|t| spatial_key(t.position, RADIUS));
35 }
36 grid.rebuild_with_data(
37 ctx.world
38 .query::<(Entity, &Transform2D, &Vel)>()
39 .iter()
40 .map(|(e, t, v)| (e, t.position, v.0)),
41 );
42 let grid = &grid;
43 ctx.world
44 .query::<(Entity, &mut Transform2D, &mut Vel)>()
45 .par_for_each(|(me, t, v)| {
46 let (mut away, mut heading, mut center, mut n) = (Vec2::ZERO, Vec2::ZERO, Vec2::ZERO, 0.0);
47 grid.for_each_in_radius(t.position, RADIUS, |o| {
48 if o.entity == me {
49 return;
50 }
51 let d = t.position - o.position;
52 away += d / d.length_squared().max(1.0);
53 heading += o.data;
54 center += o.position;
55 n += 1.0;
56 });
57 let mut steer = away * 900.0;
58 if n > 0.0 {
59 steer += (heading / n - v.0) * 1.5 + (center / n - t.position) * 1.2;
60 }
61 let speed = (v.0 + steer * dt).length().clamp(MIN_SPEED, MAX_SPEED);
62 v.0 = (v.0 + steer * dt).normalize_or(Vec2::X) * speed;
63 t.position = (t.position + v.0 * dt).rem_euclid(bounds);
64 t.rotation = v.0.to_angle();
65 });
66 }
67}
68fn controls(ctx: &mut Ctx) {
71 if ctx.key_pressed(Key::Space) {
72 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
73 }
74 if ctx.key_pressed(Key::Period) {
75 ctx.step();
76 }
77 for (key, speed) in [(Key::Num1, 1.0), (Key::Num2, 4.0), (Key::Num3, 16.0), (Key::Num4, 64.0)] {
78 if ctx.key_pressed(key) {
79 ctx.set_sim_speed(speed);
80 }
81 }
82 if ctx.key_pressed(Key::Escape) {
83 ctx.quit();
84 }
85}
86
87fn spawn_boids(ctx: &mut Ctx, n: usize, bounds: Vec2) {
88 ctx.world.reflect::<Vel>();
89 let tex = ctx.load_texture("crab.png");
90 let mut s = 0x9E37_79B9_7F4A_7C15u64;
91 let mut rand = move || {
92 s ^= s << 13;
93 s ^= s >> 7;
94 s ^= s << 17;
95 (s >> 40) as f32 / (1u64 << 24) as f32
96 };
97 let mut batch = Vec::with_capacity(n);
98 for _ in 0..n {
99 let pos = vec2(rand(), rand()) * bounds;
100 let vel = Vec2::from_angle(rand() * std::f32::consts::TAU) * MIN_SPEED;
101 let tint = Color::rgb(0.5 + rand() * 0.5, 0.7 + rand() * 0.3, 0.9);
102 batch.push((
103 Transform2D::at(pos).scaled(Vec2::splat(0.5)),
104 Sprite::new(tex).with_color(tint),
105 Vel(vel),
106 ));
107 }
108 ctx.world.spawn_batch(batch);
109}
110
111fn main() {
112 let n = env_number("BOIDS", 20_000.0) as usize;
113 let headless = env_number("HEADLESS", 0.0) != 0.0;
114
115 if headless {
116 let side = (n as f32 / 20_000.0).sqrt() * 1280.0;
118 let bounds = Vec2::splat(side);
119 let ticks = env_number("TICKS", 300.0) as u64;
120 let mut start = Instant::now();
121 let world = Game::new("boids", 1, 1)
122 .sim_system(flocking(bounds))
123 .run_headless(ticks, |ctx| {
124 spawn_boids(ctx, n, bounds);
125 start = Instant::now();
126 });
127 let secs = start.elapsed().as_secs_f64();
128 println!(
129 "{} boids, {ticks} ticks: {:.2} ms/tick ({:.0} ticks/s)",
130 world.len(),
131 secs * 1e3 / ticks as f64,
132 ticks as f64 / secs
133 );
134 return;
135 }
136
137 let bounds = vec2(1280.0, 720.0);
138 Game::new("Crabby Engine: boids", 1280, 720)
139 .sim_system(flocking(bounds))
140 .system(controls)
141 .run(move |ctx| {
142 spawn_boids(ctx, n, bounds);
143 let mut next = 1.0;
144 move |ctx: &mut Ctx| {
145 if ctx.time() >= next {
146 next += 1.0;
147 let sim_ms: f32 = ctx.system_timings().iter().filter(|t| t.sim).map(|t| t.avg_ms).sum();
148 println!(
149 "boids: {} fps: {:.0} sim: {:.2} ms/frame tick: {}",
150 ctx.world.len(),
151 ctx.fps(),
152 sim_ms,
153 ctx.sim_tick()
154 );
155 }
156 }
157 });
158}