Skip to main content

ecs/
ecs.rs

1//! ECS basics: components, simulation systems, a resource and commands.
2//!
3//!   `cargo run --example ecs`
4//!
5//! - Crabs wander around and fade out as they age; old crabs are despawned.
6//! - Click (or hold) the left mouse button to spawn more.
7//! - Space: pause / resume.  Period: one tick while paused.
8//! - 1 / 2 / 3 / 4: simulation speed 1x / 4x / 16x / 64x.  Escape: quit.
9
10use ponko::*;
11
12/// Velocity in pixels per second.
13struct Vel(Vec2);
14/// Seconds left to live.
15struct Life(f32);
16
17/// Resource: shared settings and a tiny random generator.
18struct Settings {
19    rng: u64,
20    crab: Texture,
21}
22
23impl Settings {
24    fn random(&mut self) -> f32 {
25        self.rng ^= self.rng << 13;
26        self.rng ^= self.rng >> 7;
27        self.rng ^= self.rng << 17;
28        (self.rng >> 40) as f32 / (1u64 << 24) as f32
29    }
30}
31
32const LIFETIME: f32 = 8.0;
33
34fn spawn_crab(ctx: &mut Ctx, at: Vec2) {
35    let s = ctx.world.resource_mut::<Settings>();
36    let angle = s.random() * std::f32::consts::TAU;
37    let speed = 40.0 + s.random() * 120.0;
38    let tint = Color::rgb(0.5 + s.random() * 0.5, 0.5 + s.random() * 0.5, 0.5 + s.random() * 0.5);
39    let crab = s.crab;
40    ctx.world.spawn((
41        Transform2D::at(at),
42        Sprite::new(crab).with_color(tint),
43        Vel(Vec2::from_angle(angle) * speed),
44        Life(LIFETIME),
45    ));
46}
47
48/// Simulation: move and bounce off the window edges.
49fn movement(ctx: &mut Ctx) {
50    let dt = ctx.dt();
51    let bounds = ctx.window_size();
52    ctx.world.query::<(&mut Transform2D, &mut Vel)>().for_each(|(t, v)| {
53        t.position += v.0 * dt;
54        t.rotation = v.0.to_angle();
55        if t.position.x < 0.0 || t.position.x > bounds.x {
56            v.0.x = -v.0.x;
57        }
58        if t.position.y < 0.0 || t.position.y > bounds.y {
59            v.0.y = -v.0.y;
60        }
61        t.position = t.position.clamp(Vec2::ZERO, bounds);
62    });
63}
64
65/// Simulation: age, fade out, and despawn through commands.
66fn aging(ctx: &mut Ctx) {
67    let dt = ctx.dt();
68    let commands = &mut ctx.commands;
69    ctx.world
70        .query::<(Entity, &mut Life, &mut Sprite)>()
71        .for_each(|(e, life, sprite)| {
72            life.0 -= dt;
73            sprite.color = sprite.color.with_alpha((life.0 / LIFETIME).clamp(0.0, 1.0));
74            if life.0 <= 0.0 {
75                commands.despawn(e);
76            }
77        });
78}
79
80/// Frame system: input.
81fn controls(ctx: &mut Ctx) {
82    if ctx.key_pressed(Key::Space) {
83        if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
84    }
85    if ctx.key_pressed(Key::Period) {
86        ctx.step();
87    }
88    for (key, speed) in [(Key::Num1, 1.0), (Key::Num2, 4.0), (Key::Num3, 16.0), (Key::Num4, 64.0)] {
89        if ctx.key_pressed(key) {
90            ctx.set_sim_speed(speed);
91        }
92    }
93    if ctx.mouse_down(MouseButton::Left) {
94        let at = ctx.mouse_pos();
95        for _ in 0..5 {
96            spawn_crab(ctx, at);
97        }
98    }
99    if ctx.key_pressed(Key::Escape) {
100        ctx.quit();
101    }
102}
103
104fn main() {
105    Game::new("Crabby Engine: ECS", 1280, 720)
106        .sim_system(movement)
107        .sim_system(aging)
108        .system(controls)
109        .run(|ctx| {
110            let crab = ctx.load_texture("crab.png");
111            ctx.world.insert_resource(Settings {
112                rng: 0x9E37_79B9_7F4A_7C15,
113                crab,
114            });
115            let center = ctx.window_size() / 2.0;
116            for _ in 0..500 {
117                spawn_crab(ctx, center);
118            }
119
120            // Optional per-frame closure: show stats in the title bar once a second.
121            let mut next_report = 1.0;
122            move |ctx: &mut Ctx| {
123                if ctx.time() >= next_report {
124                    next_report += 1.0;
125                    let sim = if ctx.is_paused() {
126                        "paused".to_string()
127                    } else {
128                        format!("{}x", ctx.sim_speed())
129                    };
130                    println!(
131                        "crabs: {:>6}  fps: {:>5.1}  sim: {sim:>6}  tick: {}",
132                        ctx.world.len(),
133                        ctx.fps(),
134                        ctx.sim_tick()
135                    );
136                }
137            }
138        });
139}