pub const fn vec2(x: f32, y: f32) -> Vec2Expand description
2D vector used for positions, sizes and velocities (re-exported from glam).
Creates a 2-dimensional vector.
Examples found in repository?
examples/ants.rs (line 28)
28const NEST_AREA: Vec2 = vec2(1280.0, 720.0);
29/// Pixels per pheromone cell.
30const CELL: f32 = 4.0;
31const SPEED: f32 = 45.0;
32/// How far ahead ants smell, and how wide.
33const SENSE_DIST: f32 = 10.0;
34const SENSE_ANGLE: f32 = 0.6;
35/// Ants see food within this distance, their nest within three times it.
36const SIGHT: f32 = 24.0;
37const NEST_RADIUS: f32 = 16.0;
38/// Trail strength below which food carriers stop following the home trail
39/// and head straight for home instead.
40const FAINT: f32 = 0.05;
41/// Seconds an ant can go without delivering food.
42const LIFESPAN: f32 = 90.0;
43
44#[derive(Serialize, Deserialize)]
45struct Ant {
46 /// The nest this ant belongs to (an entity), and where it is.
47 home: Entity,
48 home_at: Vec2,
49 heading: f32,
50 carrying: bool,
51 /// Seconds since the ant last touched its nest or food: trails get
52 /// weaker the farther the ant has wandered.
53 since: f32,
54 /// Seconds left before starving; refilled on delivery.
55 energy: f32,
56 /// What happened this tick, decided in parallel and applied after.
57 #[serde(skip)]
58 event: Event,
59}
60
61#[derive(Clone, Copy, Default, PartialEq)]
62enum Event {
63 #[default]
64 None,
65 Starved,
66 PickedUp(Entity),
67 Delivered,
68}
69
70#[derive(Serialize, Deserialize)]
71struct Nest {
72 stored: u32,
73 delivered: u64,
74 max_ants: u32,
75 ants: u32,
76}
77
78#[derive(Serialize, Deserialize)]
79struct Food {
80 amount: u32,
81}
82
83/// What new ants look like: a sprite when there's a window, nothing headless.
84struct AntLook(Option<Texture>);
85
86/// Two pheromone layers over the world: "food is this way" (laid by ants
87/// carrying food home) and "home is this way" (laid by searching ants).
88#[derive(Default)]
89struct Field {
90 w: usize,
91 h: usize,
92 to_food: Vec<f32>,
93 to_home: Vec<f32>,
94}
95
96impl Field {
97 fn new(size: Vec2) -> Self {
98 let (w, h) = ((size.x / CELL) as usize, (size.y / CELL) as usize);
99 Self {
100 w,
101 h,
102 to_food: vec![0.0; w * h],
103 to_home: vec![0.0; w * h],
104 }
105 }
106
107 fn index(&self, p: Vec2) -> Option<usize> {
108 let (x, y) = ((p.x / CELL).floor(), (p.y / CELL).floor());
109 (x >= 0.0 && y >= 0.0 && (x as usize) < self.w && (y as usize) < self.h)
110 .then(|| y as usize * self.w + x as usize)
111 }
112}
113
114fn env_number(name: &str, default: f64) -> f64 {
115 std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
116}
117
118// ------------------------------------------------------------------ systems
119
120/// Every tick, on all cores: smell ahead, turn toward the stronger trail,
121/// wobble a little, walk. Seeing food or the nest beats smelling it.
122fn steer(bounds: Vec2) -> impl FnMut(&mut Ctx) {
123 let mut food_grid = SpatialGrid::new(SIGHT);
124 move |ctx: &mut Ctx| {
125 let (dt, random) = (ctx.dt(), ctx.random());
126 food_grid.rebuild_from(
127 ctx.world
128 .query::<(Entity, &Transform2D, &Food)>()
129 .iter()
130 .map(|(e, t, _)| (e, t.position)),
131 );
132 let field = ctx.world.remove_resource::<Field>().expect("field");
133 let (grid, f) = (&food_grid, &field);
134 ctx.world
135 .query::<(Entity, &mut Transform2D, &mut Ant)>()
136 .par_for_each(|(e, t, ant)| {
137 let mut rng = random.of(e);
138 let p = t.position;
139 let target = if ant.carrying {
140 (p.distance(ant.home_at) < SIGHT * 3.0).then_some(ant.home_at)
141 } else {
142 grid.nearest(p, SIGHT, |_| true).map(|food| food.position)
143 };
144 if let Some(target) = target {
145 ant.heading = (target - p).to_angle();
146 } else {
147 let layer = if ant.carrying { &f.to_home } else { &f.to_food };
148 let smell = |angle: f32| {
149 let at = p + Vec2::from_angle(ant.heading + angle) * SENSE_DIST;
150 f.index(at).map_or(-1.0, |i| layer[i])
151 };
152 let (left, ahead, right) = (smell(-SENSE_ANGLE), smell(0.0), smell(SENSE_ANGLE));
153 if ant.carrying && left.max(ahead).max(right) < FAINT {
154 // No trail to follow: real ants keep track of where
155 // home is (path integration) and head that way.
156 let off = (ant.home_at - p).to_angle() - ant.heading;
157 let off = (off + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU) - std::f32::consts::PI;
158 ant.heading += off.clamp(-2.0 * dt, 2.0 * dt);
159 } else if left > ahead && left > right {
160 ant.heading -= 4.0 * dt;
161 } else if right > ahead && right > left {
162 ant.heading += 4.0 * dt;
163 }
164 }
165 ant.heading += rng.range(-2.5, 2.5) * dt;
166 let mut next = p + Vec2::from_angle(ant.heading) * SPEED * dt;
167 // Bounce off the edges.
168 if next.x < 0.0 || next.x >= bounds.x {
169 ant.heading = std::f32::consts::PI - ant.heading;
170 next.x = next.x.clamp(0.0, bounds.x - 0.01);
171 }
172 if next.y < 0.0 || next.y >= bounds.y {
173 ant.heading = -ant.heading;
174 next.y = next.y.clamp(0.0, bounds.y - 0.01);
175 }
176 t.position = next;
177 t.rotation = ant.heading;
178 ant.since += dt;
179 ant.energy -= dt;
180 });
181 ctx.world.insert_resource(field);
182 }
183}
184
185/// Every tick: ants mark where they walk. Searching ants lay "home is this
186/// way", carrying ants lay "food is this way"; fresher trails are stronger.
187fn lay_trails(ctx: &mut Ctx) {
188 let mut field = ctx.world.remove_resource::<Field>().expect("field");
189 ctx.world.query::<(&Transform2D, &Ant)>().for_each(|(t, ant)| {
190 if let Some(i) = field.index(t.position) {
191 let strength = (1.0 - ant.since / 30.0).max(0.0);
192 let layer = if ant.carrying {
193 &mut field.to_food
194 } else {
195 &mut field.to_home
196 };
197 layer[i] = layer[i].max(strength);
198 }
199 });
200 ctx.world.insert_resource(field);
201}
202
203/// Every 4th tick: trails fade.
204fn evaporate(ctx: &mut Ctx) {
205 let keep = 0.985f32.powf(ctx.dt() * 60.0);
206 let field = ctx.world.resource_mut::<Field>();
207 for v in field.to_food.iter_mut().chain(field.to_home.iter_mut()) {
208 *v *= keep;
209 }
210}
211
212/// Ants take turns (each every 2nd tick) picking up food and dropping it at
213/// their nest; starving ants die. Each ant decides on all cores and notes
214/// what happened in its `event`; one quick pass then applies the events.
215fn forage(ctx: &mut Ctx) {
216 let turn = ctx.turns(2);
217 let mut food_grid = SpatialGrid::new(8.0);
218 food_grid.rebuild_from(
219 ctx.world
220 .query::<(Entity, &Transform2D, &Food)>()
221 .iter()
222 .map(|(e, t, _)| (e, t.position)),
223 );
224 let grid = &food_grid;
225 ctx.world
226 .query::<(Entity, &Transform2D, &mut Ant)>()
227 .par_for_each(|(e, t, ant)| {
228 ant.event = Event::None;
229 if ant.energy <= 0.0 {
230 ant.event = Event::Starved;
231 return;
232 }
233 if !turn.includes(e) {
234 return;
235 }
236 let at_home = t.position.distance(ant.home_at) < NEST_RADIUS;
237 if ant.carrying {
238 if at_home {
239 ant.carrying = false;
240 ant.since = 0.0;
241 ant.energy = LIFESPAN;
242 ant.heading += std::f32::consts::PI;
243 ant.event = Event::Delivered;
244 }
245 } else if at_home {
246 ant.since = 0.0;
247 } else if let Some(f) = grid.nearest(t.position, 4.0, |_| true) {
248 ant.carrying = true;
249 ant.since = 0.0;
250 ant.heading += std::f32::consts::PI;
251 ant.event = Event::PickedUp(f.entity);
252 }
253 });
254
255 let (mut picked, mut delivered) = (Vec::new(), Vec::new());
256 let cmds = &mut ctx.commands;
257 ctx.world
258 .query::<(Entity, &Ant)>()
259 .for_each(|(e, ant)| match ant.event {
260 Event::None => {}
261 Event::Starved => cmds.despawn(e),
262 Event::PickedUp(food) => picked.push(food),
263 Event::Delivered => delivered.push(ant.home),
264 });
265 for f in picked {
266 if let Some(food) = ctx.world.get_mut::<Food>(f) {
267 food.amount = food.amount.saturating_sub(1);
268 if food.amount == 0 {
269 ctx.world.despawn(f);
270 }
271 }
272 }
273 for nest in delivered {
274 if let Some(n) = ctx.world.get_mut::<Nest>(nest) {
275 n.stored += 1;
276 n.delivered += 1;
277 }
278 }
279}
280
281/// Twice a second: nests turn stored food into ants, and new food appears
282/// where piles ran out.
283fn grow(bounds: Vec2) -> impl FnMut(&mut Ctx) {
284 move |ctx: &mut Ctx| {
285 let mut rng = ctx.random().stream(0);
286 let sprite = ctx.world.resource::<AntLook>().0;
287 let mut counts = std::collections::HashMap::new();
288 ctx.world
289 .query::<&Ant>()
290 .for_each(|a| *counts.entry(a.home).or_insert(0u32) += 1);
291 let mut nests = Vec::new();
292 ctx.world
293 .query::<(Entity, &Transform2D, &mut Nest)>()
294 .for_each(|(e, t, n)| {
295 n.ants = counts.get(&e).copied().unwrap_or(0);
296 let born = n.stored.min(n.max_ants.saturating_sub(n.ants));
297 n.stored -= born;
298 nests.push((e, t.position, born));
299 });
300 for &(home, at, born) in &nests {
301 for _ in 0..born {
302 spawn_ant(&mut ctx.commands, sprite, home, at, rng.angle());
303 }
304 }
305 let piles = ctx.world.query::<&Food>().count();
306 let homes: Vec<Vec2> = nests.iter().map(|n| n.1).collect();
307 for _ in piles..food_piles(bounds) {
308 spawn_food(&mut ctx.commands, &mut rng, bounds, &homes);
309 }
310 }
311}
312
313// -------------------------------------------------------------------- setup
314
315fn food_piles(bounds: Vec2) -> usize {
316 ((bounds.x * bounds.y) / 60_000.0).ceil() as usize
317}
318
319fn spawn_ant(cmds: &mut Commands, sprite: Option<Texture>, home: Entity, at: Vec2, heading: f32) {
320 let ant = Ant {
321 home,
322 home_at: at,
323 heading,
324 carrying: false,
325 since: 0.0,
326 energy: LIFESPAN,
327 event: Event::None,
328 };
329 let t = Transform2D::at(at).rotated(heading);
330 match sprite {
331 Some(tex) => cmds.spawn((t, ant, Sprite::new(tex).with_color(Color::hex(0xD4553A)))),
332 None => cmds.spawn((t, ant)),
333 }
334}
335
336fn spawn_food(cmds: &mut Commands, rng: &mut Rng, bounds: Vec2, nests: &[Vec2]) {
337 // Not too close to any nest, so trails have to form.
338 let p = loop {
339 let p = vec2(rng.range(20.0, bounds.x - 20.0), rng.range(20.0, bounds.y - 20.0));
340 if nests.iter().all(|n| p.distance(*n) > 180.0) {
341 break p;
342 }
343 };
344 cmds.spawn((Transform2D::at(p), Food { amount: 400 }));
345}
346
347/// The simulation, shared by the windowed and headless runs.
348fn colonies(game: Game, bounds: Vec2) -> Game {
349 game.sim_system(steer(bounds))
350 .sim_system(lay_trails)
351 .sim_system(forage)
352 .sim_system_every(4, evaporate)
353 .sim_system_every(30, grow(bounds))
354}
355
356/// Nests on a grid, one per `NEST_AREA`; ants and food around them.
357fn setup(ctx: &mut Ctx, bounds: Vec2, ants: usize, sprite: Option<Texture>) {
358 ctx.world.reflect::<Ant>();
359 ctx.world.reflect::<Nest>();
360 ctx.world.reflect::<Food>();
361 ctx.world.insert_resource(Field::new(bounds));
362 ctx.world.insert_resource(AntLook(sprite));
363 let mut rng = ctx.random().stream(1);
364 let (cols, rows) = ((bounds.x / NEST_AREA.x) as usize, (bounds.y / NEST_AREA.y) as usize);
365 let (cols, rows) = (cols.max(1), rows.max(1));
366 let per_nest = (ants / (cols * rows)).max(1);
367 let mut homes = Vec::new();
368 for row in 0..rows {
369 for col in 0..cols {
370 let at = (vec2(col as f32, row as f32) + 0.5) * bounds / vec2(cols as f32, rows as f32);
371 let nest = Nest {
372 stored: 0,
373 delivered: 0,
374 max_ants: per_nest as u32 * 2,
375 ants: per_nest as u32,
376 };
377 let home = ctx.world.spawn((Transform2D::at(at), nest));
378 homes.push(at);
379 for _ in 0..per_nest {
380 spawn_ant(&mut ctx.commands, sprite, home, at, rng.angle());
381 }
382 }
383 }
384 for _ in 0..food_piles(bounds) {
385 spawn_food(&mut ctx.commands, &mut rng, bounds, &homes);
386 }
387}
388
389fn delivered(world: &mut World) -> u64 {
390 world.query::<&Nest>().iter().map(|n| n.delivered).sum()
391}
392
393// ------------------------------------------------------------------ drawing
394
395fn controls(ctx: &mut Ctx) {
396 if ctx.key_pressed(Key::Space) {
397 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
398 }
399 if ctx.key_pressed(Key::Period) {
400 ctx.step();
401 }
402 for (key, speed) in [(Key::Num1, 1.0), (Key::Num2, 4.0), (Key::Num3, 16.0), (Key::Num4, 64.0)] {
403 if ctx.key_pressed(key) {
404 ctx.set_sim_speed(speed);
405 }
406 }
407 if ctx.key_pressed(Key::Escape) {
408 ctx.quit();
409 }
410}
411
412/// Colony stats and sim controls, in a panel on top of the world.
413fn hud(ctx: &mut Ctx) {
414 let ants = ctx.world.query::<&Ant>().count();
415 let nests = ctx.world.query::<&Nest>().count();
416 let food = delivered(&mut ctx.world);
417 let panel = Rect::new(12.0, 12.0, 260.0, 156.0);
418 ctx.ui_panel(panel);
419 let mut at = panel.min + ctx.ui_style.padding;
420 let speed = if ctx.is_paused() {
421 "paused".to_string()
422 } else {
423 format!("{}x", ctx.sim_speed())
424 };
425 let stats = format!(
426 "{ants} ants in {nests} colonies\n{food} food delivered\nspeed {speed}, {:.0} FPS",
427 ctx.fps()
428 );
429 at.y += ctx.ui_label(at, &stats).y + 8.0;
430 let pause = if ctx.is_paused() { "Run" } else { "Pause" };
431 if ctx.ui_button(Rect::new(at.x, at.y, 72.0, 30.0), pause) {
432 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
433 }
434 for (i, speed) in [1.0f32, 4.0, 16.0].into_iter().enumerate() {
435 let r = Rect::new(at.x + 80.0 + i as f32 * 54.0, at.y, 48.0, 30.0);
436 if ctx.ui_button(r, &format!("{speed}x")) {
437 ctx.set_sim_speed(speed);
438 ctx.resume();
439 }
440 }
441}
442
443/// Draws the pheromone field (one texture, updated every frame), the food
444/// piles and the nests. Ants are sprites, drawn by the engine.
445fn draw_world(bounds: Vec2) -> impl FnMut(&mut Ctx) {
446 let mut pixels = Vec::new();
447 let mut dots: Vec<(Vec2, f32, Color)> = Vec::new();
448 let mut field_texture = None;
449 move |ctx: &mut Ctx| {
450 let (w, h) = {
451 let f = ctx.world.resource::<Field>();
452 (f.w, f.h)
453 };
454 let field_tex =
455 *field_texture.get_or_insert_with(|| ctx.create_texture(w as u32, h as u32, &vec![0; w * h * 4]));
456 let field = ctx.world.resource::<Field>();
457 pixels.clear();
458 pixels.extend(field.to_food.iter().zip(&field.to_home).flat_map(|(&food, &home)| {
459 let g = (food.sqrt() * 200.0) as u8;
460 let b = (home.sqrt() * 120.0) as u8;
461 [g / 3, g, b.max(g / 4), 255]
462 }));
463 ctx.update_texture(&field_tex, &pixels);
464 ctx.clear(Color::BLACK);
465 let scale = bounds / vec2(w as f32, h as f32);
466 ctx.draw_ex(
467 &field_tex,
468 bounds * 0.5,
469 DrawParams {
470 scale,
471 z: -10.0,
472 ..Default::default()
473 },
474 );
475
476 dots.clear();
477 ctx.world.query::<(&Transform2D, &Food)>().for_each(|(t, f)| {
478 dots.push((t.position, 2.0 + (f.amount as f32).sqrt() / 2.0, Color::hex(0x7BD389)));
479 });
480 ctx.world.query::<(&Transform2D, &Nest)>().for_each(|(t, _)| {
481 dots.push((t.position, NEST_RADIUS, Color::hex(0x8E5A3C)));
482 });
483 for &(p, radius, color) in &dots {
484 ctx.draw_circle(p, radius, color);
485 }
486 }
487}
488
489fn main() {
490 let ants = env_number("ANTS", ANTS_PER_NEST as f64) as usize;
491 let seed = env_number("SEED", DEFAULT_SEED as f64) as u64;
492 // One colony's worth of space per ANTS_PER_NEST ants.
493 let nests = (ants as f32 / ANTS_PER_NEST as f32).max(1.0);
494 let side = nests.sqrt().ceil();
495 let bounds = NEST_AREA * vec2(side, (nests / side).ceil());
496
497 if env_number("HEADLESS", 0.0) != 0.0 {
498 let ticks = env_number("TICKS", 600.0) as u64;
499 let mut start = Instant::now();
500 let mut world = colonies(Game::new("ants", 1, 1).seed(seed), bounds).run_headless(ticks, |ctx| {
501 setup(ctx, bounds, ants, None);
502 start = Instant::now();
503 });
504 let secs = start.elapsed().as_secs_f64();
505 println!(
506 "{ticks} ticks: {:.2} ms/tick ({:.0} ticks/s). {} ants, {} food delivered",
507 secs * 1e3 / ticks as f64,
508 ticks as f64 / secs,
509 world.query::<&Ant>().count(),
510 delivered(&mut world)
511 );
512 return;
513 }
514
515 colonies(Game::new("Crabby Engine: ants", 1280, 720).seed(seed), bounds)
516 .system(controls)
517 .system(camera_controls)
518 .system(draw_world(bounds))
519 .system(hud)
520 .run(move |ctx| {
521 let sprite = ctx.create_texture(3, 2, &[255; 3 * 2 * 4]);
522 setup(ctx, bounds, ants, Some(sprite));
523 // Start with the whole world in view.
524 let fit = ctx.window_size() / bounds;
525 ctx.camera.zoom = fit.x.min(fit.y);
526 ctx.camera.look_at(bounds / 2.0);
527 let mut next = 1.0;
528 move |ctx: &mut Ctx| {
529 if ctx.time() >= next {
530 next += 1.0;
531 let sim_ms: f32 = ctx.system_timings().iter().filter(|t| t.sim).map(|t| t.avg_ms).sum();
532 println!(
533 "ants: {} delivered: {} fps: {:.0} sim: {sim_ms:.2} ms/frame tick: {}",
534 ctx.world.query::<&Ant>().count(),
535 delivered(&mut ctx.world),
536 ctx.fps(),
537 ctx.sim_tick()
538 );
539 }
540 }
541 });
542}More examples
examples/hotpatch.rs (line 29)
25fn wobble(ctx: &mut Ctx) {
26 let time = ctx.sim_time() as f32;
27 for (t, crab) in ctx.world.query::<(&mut Transform2D, &Crab)>().iter() {
28 let s = 1.0 + 0.15 * (time * 3.0 + crab.0).sin();
29 t.scale = vec2(s, s);
30 }
31}
32
33/// Prints the first crab's rotation once a second, so you can see that
34/// state carries over a patch.
35fn report(ctx: &mut Ctx) {
36 let tick = ctx.sim_tick();
37 if tick.is_multiple_of(60)
38 && let Some(t) = ctx.world.query::<&Transform2D>().iter().next()
39 {
40 println!("tick {tick:>5} rotation {:>7.2}", t.rotation);
41 }
42}
43
44/// A phase, so crabs don't all move together.
45struct Crab(f32);
46
47fn main() {
48 Game::new("Crabby Engine: hot patching", 1280, 720)
49 .sim_system(spin)
50 .sim_system(wobble)
51 .sim_system(report)
52 .run(|ctx| {
53 let crab = ctx.load_texture("crab.png");
54 for i in 0..12 {
55 let at = vec2(160.0 + (i % 6) as f32 * 190.0, 220.0 + (i / 6) as f32 * 280.0);
56 ctx.world
57 .spawn((Transform2D::at(at), Sprite::new(crab), Crab(i as f32 * 0.7)));
58 }
59 });
60}examples/shaders.rs (line 23)
14fn blade() -> MeshData {
15 MeshData {
16 positions: vec![
17 vec3(-0.03, 0.0, 0.0),
18 vec3(0.03, 0.0, 0.0),
19 vec3(0.01, 0.6, 0.0),
20 vec3(-0.01, 0.6, 0.0),
21 ],
22 normals: vec![Vec3::Z; 4],
23 uvs: vec![vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0)],
24 indices: vec![0, 1, 2, 0, 2, 3],
25 ..Default::default()
26 }
27}examples/physics3d.rs (line 106)
92fn grass(ctx: &mut Ctx) -> Material {
93 let green: Vec<u8> = (0..16 * 16)
94 .flat_map(|i| {
95 let shade = 200 + (i * 37 % 56) as u8;
96 [
97 (60 * shade as u32 / 255) as u8,
98 (110 * shade as u32 / 255) as u8,
99 40,
100 255,
101 ]
102 })
103 .collect();
104 let layers = ctx.create_texture_array(16, 16, &[&green], TextureKind::Color);
105 let weights = ctx.create_texture_array(1, 1, &[&[255, 0, 0, 0]], TextureKind::Data);
106 let corner = vec2(-SIZE / 2.0, -SIZE / 2.0);
107 ctx.create_terrain_material(&TerrainMaterialDesc::new(layers, weights, corner, vec2(SIZE, SIZE)))
108}examples/vehicle.rs (line 218)
204fn grass(ctx: &mut Ctx) -> Material {
205 let green: Vec<u8> = (0..16 * 16)
206 .flat_map(|i| {
207 let shade = 200 + (i * 37 % 56) as u8;
208 [
209 (70 * shade as u32 / 255) as u8,
210 (120 * shade as u32 / 255) as u8,
211 45,
212 255,
213 ]
214 })
215 .collect();
216 let layers = ctx.create_texture_array(16, 16, &[&green], TextureKind::Color);
217 let weights = ctx.create_texture_array(1, 1, &[&[255, 0, 0, 0]], TextureKind::Data);
218 let corner = vec2(-SIZE / 2.0, -SIZE / 2.0);
219 ctx.create_terrain_material(&TerrainMaterialDesc::new(layers, weights, corner, vec2(SIZE, SIZE)))
220}examples/bunnymark_ecs.rs (line 50)
44fn spawn(ctx: &mut Ctx, n: usize, at: Vec2) {
45 let b = ctx.world.resource_mut::<Bench>();
46 let crab = b.crab;
47 let mut batch = Vec::with_capacity(n);
48 for _ in 0..n {
49 let r = &mut b.rng;
50 let vel = vec2(r.next_f32() * 400.0 - 200.0, r.next_f32() * 400.0 - 300.0);
51 let color = Color::rgb(
52 0.6 + r.next_f32() * 0.4,
53 0.6 + r.next_f32() * 0.4,
54 0.6 + r.next_f32() * 0.4,
55 );
56 batch.push((
57 Transform2D {
58 scale: Vec2::splat(b.scale),
59 ..Transform2D::at(at)
60 },
61 Sprite::new(crab).with_color(color),
62 Vel(vel),
63 ));
64 }
65 ctx.world.spawn_batch(batch);
66}
67
68fn bounce(ctx: &mut Ctx) {
69 let dt = ctx.dt();
70 let bounds = ctx.window_size();
71 let gravity = 600.0;
72 ctx.world.query::<(&mut Transform2D, &mut Vel)>().for_each(|(t, v)| {
73 let (p, v) = (&mut t.position, &mut v.0);
74 v.y += gravity * dt;
75 *p += *v * dt;
76 if p.x < 0.0 {
77 p.x = 0.0;
78 v.x = v.x.abs();
79 }
80 if p.x > bounds.x {
81 p.x = bounds.x;
82 v.x = -v.x.abs();
83 }
84 if p.y > bounds.y {
85 p.y = bounds.y;
86 v.y = -v.y.abs() * 0.85;
87 }
88 if p.y < 0.0 {
89 p.y = 0.0;
90 v.y = 0.0;
91 }
92 });
93}
94
95fn report(ctx: &mut Ctx) {
96 if ctx.mouse_down(MouseButton::Left) {
97 let at = ctx.mouse_pos();
98 spawn(ctx, 1000, at);
99 }
100 // "update + draw calls" = every system except this one, as measured by the
101 // engine (smoothed averages; draw_sprites hasn't run yet this frame).
102 let systems_ms: f32 = ctx
103 .system_timings()
104 .iter()
105 .filter(|t| !t.name.ends_with("report"))
106 .map(|t| t.avg_ms)
107 .sum();
108 let (time, count, render) = (ctx.time(), ctx.world.len(), ctx.render_stats());
109 let b = ctx.world.resource_mut::<Bench>();
110 let now = Instant::now();
111 let frame_ms = now.duration_since(b.frame_start).as_secs_f32() * 1000.0;
112 b.frame_start = now;
113 b.worst_ms = b.worst_ms.max(frame_ms);
114 b.frames += 1;
115 if time >= b.report_at {
116 println!(
117 "crabs: {:>8} fps: {:>6.1} avg frame: {:>6.2} ms worst: {:>6.2} ms update+draw calls: {:>6.2} ms render cpu: {:>5.2} ms gpu: {}",
118 count,
119 b.frames,
120 1000.0 / b.frames as f64,
121 b.worst_ms,
122 systems_ms,
123 render.cpu_ms,
124 render.gpu_ms.map_or("n/a".into(), |g| format!("{g:.2} ms")),
125 );
126 b.report_at += 1.0;
127 b.frames = 0;
128 b.worst_ms = 0.0;
129 }
130 let quit_after = b.quit_after;
131 if ctx.key_pressed(Key::Escape) || (quit_after > 0.0 && time >= quit_after) {
132 ctx.quit();
133 }
134}
135
136fn main() {
137 let start_count = env_number("CRABS", 10_000.0) as usize;
138 let quit_after = env_number("SECONDS", 0.0);
139
140 Game::new("Crabby Engine: bunnymark (ECS)", 1280, 720)
141 .vsync(false)
142 .system(bounce)
143 .system(report)
144 .run(move |ctx| {
145 let crab = ctx.load_texture("crab.png");
146 ctx.world.insert_resource(Bench {
147 crab,
148 rng: Rng(0x9E37_79B9_7F4A_7C15),
149 report_at: 1.0,
150 frames: 0,
151 worst_ms: 0.0,
152 frame_start: Instant::now(),
153 quit_after,
154 scale: env_number("SCALE", 1.0) as f32,
155 });
156 spawn(ctx, start_count, vec2(20.0, 20.0));
157 });
158}