Skip to main content

physics/
physics.rs

1//! Physics: crates, balls and rocks fall, stack, bounce and slide down a
2//! ramp. Left click drops a crate, right click a ball, middle click a rock
3//! (a polygon), Space blasts everything near the mouse away,
4//! and the bell rings (turns red) when something lands in the bucket.
5//!
6//!   `cargo run --release --example physics --features physics`
7//!
8//! Keys: R starts over, Escape quits.
9
10use ponko::*;
11
12const W: f32 = 1000.0;
13const H: f32 = 700.0;
14
15/// Marks the sensor in the bucket.
16struct Bell {
17    rang: f32,
18}
19
20// ANCHOR: bodies
21fn build_level(ctx: &mut Ctx) {
22    // Fixed bodies never move: the ground and two walls.
23    for (at, size) in [
24        (vec2(W / 2.0, H - 10.0), vec2(W, 20.0)),
25        (vec2(10.0, H / 2.0), vec2(20.0, H)),
26        (vec2(W - 10.0, H / 2.0), vec2(20.0, H)),
27    ] {
28        ctx.world
29            .spawn((Transform2D::at(at), RigidBody::fixed(), Collider::rect(size)));
30    }
31    // A bouncy ball and a crate, both dynamic: gravity and collisions move them.
32    ctx.world.spawn((
33        Transform2D::at(vec2(300.0, 100.0)),
34        RigidBody::dynamic(),
35        Collider::circle(16.0).restitution(0.8),
36    ));
37    ctx.world.spawn((
38        Transform2D::at(vec2(320.0, 0.0)),
39        RigidBody::dynamic(),
40        Collider::rect(vec2(40.0, 40.0)).friction(0.9),
41    ));
42    // A ramp: any convex shape of up to 8 corners, around its position.
43    ctx.world.spawn((
44        Transform2D::at(vec2(150.0, H - 80.0)),
45        RigidBody::fixed(),
46        Collider::polygon(&[vec2(-140.0, 60.0), vec2(140.0, 60.0), vec2(-140.0, -60.0)]),
47    ));
48    // A sensor reports overlaps but doesn't push.
49    ctx.world.spawn((
50        Transform2D::at(vec2(800.0, H - 60.0)),
51        RigidBody::fixed(),
52        Collider::rect(vec2(120.0, 80.0)).sensor(),
53        Bell { rang: 0.0 },
54    ));
55}
56// ANCHOR_END: bodies
57
58// ANCHOR: collisions
59fn ring_the_bell(ctx: &mut Ctx) {
60    let mut rang = false;
61    for c in ctx.events.read::<CollisionStarted>() {
62        rang |= c.sensor && [c.a, c.b].iter().any(|&e| ctx.world.has::<Bell>(e));
63    }
64    let dt = ctx.dt();
65    for bell in ctx.world.query::<&mut Bell>().iter() {
66        bell.rang = if rang { 1.0 } else { (bell.rang - dt * 2.0).max(0.0) };
67    }
68}
69// ANCHOR_END: collisions
70
71// ANCHOR: queries
72fn blast(ctx: &mut Ctx) {
73    if !ctx.key_pressed(Key::Space) {
74        return;
75    }
76    let center = ctx.mouse_world();
77    let mut near = Vec::new();
78    ctx.physics.entities_in_circle(center, 200.0, &mut near);
79    for e in near {
80        let Some(t) = ctx.world.get::<Transform2D>(e) else {
81            continue;
82        };
83        let away = (t.position - center).normalize_or(Vec2::NEG_Y);
84        if let Some(mass) = ctx.physics.mass(e) {
85            ctx.physics.apply_impulse(e, away * mass * 600.0);
86        }
87    }
88}
89
90/// What's under the mouse, straight down: a laser from the cursor.
91fn laser(ctx: &mut Ctx) -> Option<RayHit> {
92    ctx.physics.cast_ray(ctx.mouse_world(), vec2(0.0, 1.0), H)
93}
94// ANCHOR_END: queries
95
96fn drop_things(ctx: &mut Ctx) {
97    let at = ctx.mouse_world();
98    if ctx.mouse_pressed(MouseButton::Left) {
99        ctx.world.spawn((
100            Transform2D::at(at),
101            RigidBody::dynamic(),
102            Collider::rect(vec2(30.0, 30.0)).friction(0.8),
103        ));
104    }
105    if ctx.mouse_pressed(MouseButton::Right) {
106        ctx.world.spawn((
107            Transform2D::at(at),
108            RigidBody::dynamic(),
109            Collider::circle(12.0).restitution(0.6),
110        ));
111    }
112    if ctx.mouse_pressed(MouseButton::Middle) {
113        let rock: Vec<Vec2> = (0..5)
114            .map(|i| Vec2::from_angle(i as f32 * std::f32::consts::TAU / 5.0) * 18.0)
115            .collect();
116        ctx.world.spawn((
117            Transform2D::at(at),
118            RigidBody::dynamic(),
119            Collider::polygon(&rock).friction(0.7),
120        ));
121    }
122    if ctx.key_pressed(Key::R) {
123        let all: Vec<Entity> = ctx.world.query::<Entity>().with::<RigidBody>().iter().collect();
124        for e in all {
125            ctx.world.despawn(e);
126        }
127        build_level(ctx);
128    }
129    if ctx.key_pressed(Key::Escape) {
130        ctx.quit();
131    }
132}
133
134fn draw(ctx: &mut Ctx) {
135    ctx.clear(Color::hex(0x1d2330));
136    let shapes: Vec<(Transform2D, Collider, bool, f32)> = ctx
137        .world
138        .query::<(&Transform2D, &Collider, &RigidBody, Option<&Bell>)>()
139        .iter()
140        .map(|(t, c, b, bell)| (*t, *c, b.kind == BodyKind::Fixed, bell.map_or(0.0, |b| b.rang)))
141        .collect();
142    for (t, c, fixed, rang) in shapes {
143        let color = if c.sensor {
144            Color::rgba(1.0, 0.3 + 0.7 * (1.0 - rang), 0.3, 0.35)
145        } else if fixed {
146            Color::hex(0x4a5568)
147        } else {
148            Color::hex(0xf6ad55)
149        };
150        match c.shape {
151            Shape::Circle { radius } => {
152                ctx.draw_circle(t.position, radius, color);
153                // A spoke shows the spin.
154                let spoke = t.position + Vec2::from_angle(t.rotation) * radius;
155                ctx.draw_line(t.position, spoke, 2.0, Color::hex(0x1d2330));
156            }
157            Shape::Rect { size } => {
158                // Four corners turned with the body, drawn as edges.
159                let (sin, cos) = t.rotation.sin_cos();
160                let turn = |v: Vec2| t.position + vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
161                let h = size / 2.0;
162                let corners = [vec2(-h.x, -h.y), vec2(h.x, -h.y), vec2(h.x, h.y), vec2(-h.x, h.y)].map(turn);
163                if fixed || c.sensor {
164                    ctx.draw_rect(Rect::new(t.position.x - h.x, t.position.y - h.y, size.x, size.y), color);
165                } else {
166                    for i in 0..4 {
167                        ctx.draw_line(corners[i], corners[(i + 1) % 4], 3.0, color);
168                    }
169                }
170            }
171            Shape::Capsule { height, radius } => {
172                let up = Vec2::from_angle(t.rotation + std::f32::consts::FRAC_PI_2) * height / 2.0;
173                ctx.draw_line(t.position - up, t.position + up, radius * 2.0, color);
174            }
175            Shape::Polygon { .. } => {
176                let (sin, cos) = t.rotation.sin_cos();
177                let turn = |v: Vec2| t.position + vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
178                let corners: Vec<Vec2> = c.shape.points().iter().map(|&p| turn(p + c.offset)).collect();
179                for i in 0..corners.len() {
180                    ctx.draw_line(corners[i], corners[(i + 1) % corners.len()], 3.0, color);
181                }
182            }
183            _ => {} // shapes added in later versions
184        }
185    }
186    if let Some(hit) = laser(ctx) {
187        ctx.draw_line(ctx.mouse_world(), hit.point, 1.0, Color::rgba(1.0, 0.2, 0.2, 0.8));
188        ctx.draw_circle(hit.point, 4.0, Color::rgb(1.0, 0.2, 0.2));
189    }
190    let bodies = ctx.physics.body_count();
191    ctx.draw_text(
192        &format!(
193            "{bodies} bodies  |  left click: crate  right click: ball  middle click: rock  space: blast  R: reset"
194        ),
195        vec2(30.0, 30.0),
196        18.0,
197        Color::WHITE,
198    );
199}
200
201fn main() {
202    // ANCHOR: setup
203    Game::new("Physics", W as u32, H as u32)
204        .physics(vec2(0.0, 980.0)) // pixels/s², y is down
205        .sim_system(ring_the_bell)
206        // ANCHOR_END: setup
207        .system(drop_things)
208        .system(blast)
209        .system(draw)
210        .run(build_level);
211}