Skip to main content

vec2

Function vec2 

Source
pub const fn vec2(x: f32, y: f32) -> Vec2
Expand 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
Hide additional 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}