Skip to main content

ponko/
physics.rs

1//! 2D physics with rapier (feature `physics`): rigid bodies and colliders as
2//! components, stepped inside the simulation clock.
3//!
4//! [`Transform2D`] stays the source of truth: set it and the body teleports;
5//! dynamic bodies write their new position back after every step.
6//! Physics is opt-in per entity, so the million-entity paths (spatial grid,
7//! flow fields) stay as fast as before.
8
9use std::sync::mpsc;
10
11use rapier2d::prelude as rp;
12use serde::{Deserialize, Serialize};
13
14use crate::collision::{BodyKind, CollisionStarted, CollisionStopped};
15use crate::{Ctx, Entity, Transform2D, Vec2, World};
16
17/// Makes an entity a physics body. It also needs a [`Collider`] and a
18/// [`Transform2D`].
19///
20/// ```
21/// use ponko::*;
22/// let crate_body = RigidBody::dynamic();
23/// let wall = RigidBody::fixed();
24/// assert_eq!(crate_body.gravity_scale, 1.0);
25/// assert_eq!(wall.kind, BodyKind::Fixed);
26/// ```
27#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
28#[serde(default)]
29pub struct RigidBody {
30    /// Dynamic, fixed or kinematic.
31    pub kind: BodyKind,
32    /// 1 normal, 0 floats, negative falls up.
33    pub gravity_scale: f32,
34    /// Slows movement down over time (air drag); 0 none.
35    pub linear_damping: f32,
36    /// Slows spinning down over time; 0 none.
37    pub angular_damping: f32,
38    /// Never rotates (characters).
39    pub lock_rotation: bool,
40    /// Continuous collision detection: fast bodies (bullets) don't pass
41    /// through thin walls. Costs a little more.
42    pub ccd: bool,
43}
44
45impl Default for RigidBody {
46    fn default() -> Self {
47        Self::dynamic()
48    }
49}
50
51impl RigidBody {
52    /// A body moved by gravity and collisions.
53    #[must_use]
54    pub const fn dynamic() -> Self {
55        Self {
56            kind: BodyKind::Dynamic,
57            gravity_scale: 1.0,
58            linear_damping: 0.0,
59            angular_damping: 0.0,
60            lock_rotation: false,
61            ccd: false,
62        }
63    }
64    /// A body that never moves.
65    #[must_use]
66    pub const fn fixed() -> Self {
67        Self {
68            kind: BodyKind::Fixed,
69            ..Self::dynamic()
70        }
71    }
72    /// A body only you move.
73    #[must_use]
74    pub const fn kinematic() -> Self {
75        Self {
76            kind: BodyKind::Kinematic,
77            ..Self::dynamic()
78        }
79    }
80}
81
82/// Most corners a [`Shape::Polygon`] can have.
83pub const MAX_POLYGON_POINTS: usize = 8;
84
85/// A collider's shape, in pixels, centered on the entity (plus
86/// [`Collider::offset`]) and turned with it.
87#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
88#[non_exhaustive]
89pub enum Shape {
90    /// A circle.
91    Circle {
92        /// In pixels.
93        radius: f32,
94    },
95    /// A rectangle `size` wide and high.
96    Rect {
97        /// Width and height in pixels.
98        size: Vec2,
99    },
100    /// A vertical capsule: `height` between the centers of its round ends.
101    Capsule {
102        /// Distance between the centers of the round ends, in pixels.
103        height: f32,
104        /// Radius of the round ends, in pixels.
105        radius: f32,
106    },
107    /// A convex polygon: the convex hull of its corners, in pixels around
108    /// the entity's position. Make one with [`Collider::polygon`]; read the
109    /// corners with [`Shape::points`].
110    Polygon {
111        /// The corners; only the first `count` are used.
112        points: [Vec2; MAX_POLYGON_POINTS],
113        /// How many corners: 3 to [`MAX_POLYGON_POINTS`].
114        count: u8,
115    },
116}
117
118impl Shape {
119    /// A polygon's corners (empty for other shapes).
120    ///
121    /// ```
122    /// use ponko::*;
123    /// let wedge = Collider::polygon(&[vec2(0.0, 0.0), vec2(40.0, 0.0), vec2(0.0, -20.0)]);
124    /// assert_eq!(wedge.shape.points().len(), 3);
125    /// assert!(Collider::circle(5.0).shape.points().is_empty());
126    /// ```
127    #[must_use]
128    pub fn points(&self) -> &[Vec2] {
129        match self {
130            Shape::Polygon { points, count } => &points[..usize::from(*count).min(MAX_POLYGON_POINTS)],
131            _ => &[],
132        }
133    }
134}
135
136/// The solid shape of a body, and how it touches others.
137///
138/// ```
139/// use ponko::*;
140/// let ball = Collider::circle(8.0).restitution(0.9);
141/// let trigger = Collider::rect(vec2(64.0, 64.0)).sensor();
142/// assert!(trigger.sensor);
143/// assert_eq!(ball.restitution, 0.9);
144/// ```
145#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
146pub struct Collider {
147    /// Circle, rectangle, capsule or convex polygon.
148    pub shape: Shape,
149    /// Mass per square pixel (mass = density × area). Default 1.
150    #[serde(default = "one")]
151    pub density: f32,
152    /// 0 ice, 1 rubber. Default 0.5.
153    #[serde(default = "half")]
154    pub friction: f32,
155    /// Bounciness: 0 none, 1 keeps all its speed. Default 0.
156    #[serde(default)]
157    pub restitution: f32,
158    /// Detects overlaps (sends [`CollisionStarted`]) but doesn't push.
159    #[serde(default)]
160    pub sensor: bool,
161    /// Where the shape sits relative to the entity's position.
162    #[serde(default)]
163    pub offset: Vec2,
164}
165
166// Collider defaults for fields missing from JSON, as in `Collider::of`.
167fn one() -> f32 {
168    1.0
169}
170fn half() -> f32 {
171    0.5
172}
173
174impl Collider {
175    fn of(shape: Shape) -> Self {
176        Self {
177            shape,
178            density: 1.0,
179            friction: 0.5,
180            restitution: 0.0,
181            sensor: false,
182            offset: Vec2::ZERO,
183        }
184    }
185    /// A circle.
186    #[must_use]
187    pub fn circle(radius: f32) -> Self {
188        Self::of(Shape::Circle { radius })
189    }
190    /// A rectangle `size` wide and high.
191    #[must_use]
192    pub fn rect(size: Vec2) -> Self {
193        Self::of(Shape::Rect { size })
194    }
195    /// A vertical capsule (good for characters).
196    #[must_use]
197    pub fn capsule(height: f32, radius: f32) -> Self {
198        Self::of(Shape::Capsule { height, radius })
199    }
200    /// A convex polygon around the entity's position: the convex hull of
201    /// `points` (3 to 8 of them, any order). Ramps, wedges, rocks, ships.
202    /// For a concave shape, put convex pieces on separate bodies.
203    ///
204    /// ```
205    /// use ponko::*;
206    /// let ramp = Collider::polygon(&[vec2(-50.0, 10.0), vec2(50.0, 10.0), vec2(50.0, -10.0)]);
207    /// assert_eq!(ramp.shape.points().len(), 3);
208    /// ```
209    ///
210    /// # Panics
211    ///
212    /// With fewer than 3 or more than [`MAX_POLYGON_POINTS`] points, or a
213    /// point that isn't finite.
214    #[must_use]
215    #[track_caller]
216    pub fn polygon(points: &[Vec2]) -> Self {
217        assert!(
218            (3..=MAX_POLYGON_POINTS).contains(&points.len()),
219            "Collider::polygon: 3 to {MAX_POLYGON_POINTS} points, got {}",
220            points.len()
221        );
222        assert!(
223            points.iter().all(|p| p.is_finite()),
224            "Collider::polygon: points must be finite"
225        );
226        let mut corners = [Vec2::ZERO; MAX_POLYGON_POINTS];
227        corners[..points.len()].copy_from_slice(points);
228        Self::of(Shape::Polygon {
229            points: corners,
230            count: points.len() as u8,
231        })
232    }
233    /// With this density.
234    #[must_use]
235    pub fn density(self, density: f32) -> Self {
236        Self { density, ..self }
237    }
238    /// With this friction.
239    #[must_use]
240    pub fn friction(self, friction: f32) -> Self {
241        Self { friction, ..self }
242    }
243    /// With this bounciness.
244    #[must_use]
245    pub fn restitution(self, restitution: f32) -> Self {
246        Self { restitution, ..self }
247    }
248    /// As a sensor: reports overlaps, doesn't collide.
249    #[must_use]
250    pub fn sensor(self) -> Self {
251        Self { sensor: true, ..self }
252    }
253    /// Shifted from the entity's position.
254    #[must_use]
255    pub fn offset(self, offset: Vec2) -> Self {
256        Self { offset, ..self }
257    }
258}
259
260/// A body's speed, in pixels and radians per second. Read it after a step;
261/// write it to set the speed (a jump, a thrown crate, a moving platform).
262///
263/// ```
264/// use ponko::*;
265/// let v = Velocity::linear(vec2(0.0, -300.0)); // a jump, y is down
266/// assert_eq!(v.angular, 0.0);
267/// ```
268#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
269#[serde(default)]
270#[repr(C)] // C sees it as CrabbyVelocity.
271pub struct Velocity {
272    /// Pixels per second.
273    pub linear: Vec2,
274    /// Radians per second, clockwise on screen.
275    pub angular: f32,
276}
277
278impl Velocity {
279    /// Moving at `linear`, not spinning.
280    #[must_use]
281    pub const fn linear(linear: Vec2) -> Self {
282        Self { linear, angular: 0.0 }
283    }
284}
285
286/// What a ray hit.
287#[derive(Clone, Copy, Debug, PartialEq)]
288pub struct RayHit {
289    /// The entity whose collider was hit.
290    pub entity: Entity,
291    /// Where the ray hit, in world pixels.
292    pub point: Vec2,
293    /// The surface's outward direction at `point`.
294    pub normal: Vec2,
295    /// From the ray's start to `point`.
296    pub distance: f32,
297}
298
299/// What the world last knew of one body, to notice your changes.
300#[derive(Clone, Copy)]
301struct Link {
302    entity: Entity,
303    body: rp::RigidBodyHandle,
304    rigid: RigidBody,
305    shape: Collider,
306    transform: Transform2D,
307    velocity: Velocity,
308    seen: u64,
309}
310
311/// The physics world: `ctx.physics`. Add it to a game with
312/// [`Game::physics`](crate::Game::physics); then entities with
313/// [`RigidBody`], [`Collider`] and [`Transform2D`] are simulated.
314pub struct Physics {
315    world: rp::PhysicsWorld,
316    /// By entity index.
317    links: Vec<Option<Link>>,
318    gravity: Vec2,
319    step: u64,
320    collisions: mpsc::Receiver<rp::CollisionEvent>,
321    collector: rp::ChannelEventCollector,
322    _forces: mpsc::Receiver<rp::ContactForceEvent>,
323    _tears: mpsc::Receiver<rp::SoftBodyTearEvent>,
324}
325
326impl Default for Physics {
327    fn default() -> Self {
328        Self::new(Vec2::ZERO)
329    }
330}
331
332fn to_rp(v: Vec2) -> rp::Vector {
333    rp::Vector::new(v.x, v.y)
334}
335fn from_rp(v: rp::Vector) -> Vec2 {
336    Vec2::new(v.x, v.y)
337}
338
339impl Physics {
340    /// Physics pulling everything by `gravity` (pixels/s²; y is down, so
341    /// `vec2(0.0, 980.0)` falls).
342    #[must_use]
343    pub fn new(gravity: Vec2) -> Self {
344        let (collision_tx, collisions) = mpsc::channel();
345        let (force_tx, forces) = mpsc::channel();
346        let (tear_tx, tears) = mpsc::channel();
347        let mut world = rp::PhysicsWorld::new();
348        // Objects are tens of pixels, not about a meter: tells rapier's
349        // tolerances what "small" means.
350        world.integration_parameters.length_unit = 50.0;
351        Self {
352            world,
353            links: Vec::new(),
354            gravity,
355            step: 0,
356            collisions,
357            collector: rp::ChannelEventCollector::new(collision_tx, force_tx, tear_tx),
358            _forces: forces,
359            _tears: tears,
360        }
361    }
362
363    /// The pull on every dynamic body.
364    pub fn gravity(&self) -> Vec2 {
365        self.gravity
366    }
367    /// Changes the pull on every dynamic body.
368    pub fn set_gravity(&mut self, gravity: Vec2) {
369        self.gravity = gravity;
370    }
371
372    /// How many bodies are simulated.
373    pub fn body_count(&self) -> usize {
374        self.world.bodies.len()
375    }
376
377    fn entity_of(&self, collider: rp::ColliderHandle) -> Option<Entity> {
378        self.world
379            .colliders
380            .get(collider)
381            .map(|c| Entity::from_bits(c.user_data as u64))
382    }
383
384    /// The first collider a ray from `origin` towards `direction` hits
385    /// within `max_distance`. Up to date as of the last physics step.
386    ///
387    /// ```
388    /// use ponko::*;
389    /// let world = Game::new("t", 1, 1).physics(Vec2::ZERO).run_headless(1, |ctx| {
390    ///     ctx.world.spawn((Transform2D::at(vec2(100.0, 0.0)), RigidBody::fixed(), Collider::circle(10.0)));
391    /// });
392    /// # let _ = world;
393    /// ```
394    pub fn cast_ray(&self, origin: Vec2, direction: Vec2, max_distance: f32) -> Option<RayHit> {
395        let dir = direction.normalize_or_zero();
396        if dir == Vec2::ZERO {
397            return None;
398        }
399        let ray = rp::Ray::new(to_rp(origin), to_rp(dir));
400        let (handle, hit) = self
401            .world
402            .cast_ray_and_get_normal(&ray, max_distance, true, rp::QueryFilter::default())?;
403        Some(RayHit {
404            entity: self.entity_of(handle)?,
405            point: origin + dir * hit.time_of_impact,
406            normal: from_rp(hit.normal),
407            distance: hit.time_of_impact,
408        })
409    }
410
411    /// Every entity whose collider contains `point`, into `out` (cleared first).
412    pub fn entities_at(&self, point: Vec2, out: &mut Vec<Entity>) {
413        out.clear();
414        for (handle, _) in self.world.intersect_point(to_rp(point), rp::QueryFilter::default()) {
415            out.extend(self.entity_of(handle));
416        }
417    }
418
419    /// Every entity whose collider overlaps the circle, into `out` (cleared first).
420    pub fn entities_in_circle(&self, center: Vec2, radius: f32, out: &mut Vec<Entity>) {
421        out.clear();
422        let ball = rp::Ball::new(radius);
423        let pose = rp::Pose::from_translation(to_rp(center));
424        for (handle, _) in self.world.intersect_shape(pose, &ball, rp::QueryFilter::default()) {
425            out.extend(self.entity_of(handle));
426        }
427    }
428
429    /// Pushes a dynamic body once (an explosion, a kick), in mass ×
430    /// pixels/s. Returns false if `e` isn't a physics body.
431    pub fn apply_impulse(&mut self, e: Entity, impulse: Vec2) -> bool {
432        let Some(link) = self.link(e) else { return false };
433        let handle = link.body;
434        match self.world.bodies.get_mut(handle) {
435            Some(body) => {
436                body.apply_impulse(to_rp(impulse), true);
437                true
438            }
439            None => false,
440        }
441    }
442
443    /// The mass of a body (from its collider's density and area).
444    pub fn mass(&self, e: Entity) -> Option<f32> {
445        let link = self.link(e)?;
446        self.world.bodies.get(link.body).map(|b| b.mass())
447    }
448
449    fn link(&self, e: Entity) -> Option<&Link> {
450        self.links.get(e.index() as usize)?.as_ref().filter(|l| l.entity == e)
451    }
452
453    fn insert(&mut self, e: Entity, t: &Transform2D, rigid: RigidBody, shape: Collider, velocity: Velocity) {
454        let body = self.world.insert_body(body_of(t, rigid, velocity));
455        self.world.insert_collider(collider_of(shape, e), Some(body));
456        let i = e.index() as usize;
457        if self.links.len() <= i {
458            self.links.resize(i + 1, None);
459        }
460        self.links[i] = Some(Link {
461            entity: e,
462            body,
463            rigid,
464            shape,
465            transform: *t,
466            velocity,
467            seen: self.step,
468        });
469    }
470
471    fn remove(&mut self, i: usize) {
472        if let Some(link) = self.links[i].take() {
473            self.world.remove_body(link.body);
474        }
475    }
476}
477
478fn body_of(t: &Transform2D, rigid: RigidBody, velocity: Velocity) -> rp::RigidBody {
479    let builder = match rigid.kind {
480        BodyKind::Dynamic => rp::RigidBodyBuilder::dynamic(),
481        BodyKind::Fixed => rp::RigidBodyBuilder::fixed(),
482        BodyKind::Kinematic => rp::RigidBodyBuilder::kinematic_velocity_based(),
483    };
484    let mut builder = builder
485        .pose(rp::Pose::new(to_rp(t.position), t.rotation))
486        .linvel(to_rp(velocity.linear))
487        .angvel(velocity.angular)
488        .gravity_scale(rigid.gravity_scale)
489        .linear_damping(rigid.linear_damping)
490        .angular_damping(rigid.angular_damping)
491        .ccd_enabled(rigid.ccd);
492    if rigid.lock_rotation {
493        builder = builder.lock_rotations();
494    }
495    builder.build()
496}
497
498fn collider_of(c: Collider, e: Entity) -> rp::Collider {
499    let builder = match c.shape {
500        Shape::Circle { radius } => rp::ColliderBuilder::ball(radius.max(f32::EPSILON)),
501        Shape::Rect { size } => {
502            rp::ColliderBuilder::cuboid((size.x * 0.5).max(f32::EPSILON), (size.y * 0.5).max(f32::EPSILON))
503        }
504        Shape::Capsule { height, radius } => {
505            rp::ColliderBuilder::capsule_y((height * 0.5).max(0.0), radius.max(f32::EPSILON))
506        }
507        Shape::Polygon { .. } => {
508            let points: Vec<rp::Vector> = c.shape.points().iter().map(|&p| to_rp(p)).collect();
509            // All corners on one line: no area, so a sliver of a ball instead.
510            rp::ColliderBuilder::convex_hull(&points).unwrap_or_else(|| rp::ColliderBuilder::ball(f32::EPSILON))
511        }
512    };
513    builder
514        .density(c.density)
515        .friction(c.friction)
516        .restitution(c.restitution)
517        .sensor(c.sensor)
518        .translation(to_rp(c.offset))
519        .active_events(rp::ActiveEvents::COLLISION_EVENTS)
520        .user_data(u128::from(e.to_bits()))
521        .build()
522}
523
524/// Sets up `ctx.physics` for a game that uses physics, and makes its
525/// components visible to the inspector, saves and agents.
526pub(crate) fn start(ctx: &mut Ctx, gravity: Vec2) {
527    use crate::WorldReflect;
528    ctx.physics = Physics::new(gravity);
529    ctx.world.reflect::<RigidBody>();
530    ctx.world.reflect::<Collider>();
531    ctx.world.reflect::<Velocity>();
532}
533
534/// Steps the physics world one simulation tick: picks up new, changed and
535/// removed bodies, simulates, writes positions and velocities back, and
536/// sends [`CollisionStarted`] / [`CollisionStopped`] events. Added by
537/// [`Game::physics`](crate::Game::physics); remove or reorder it like any
538/// other system.
539pub fn step_physics(ctx: &mut Ctx) {
540    let dt = ctx.dt();
541    let physics = &mut ctx.physics;
542    physics.step += 1;
543    sync_into(physics, &mut ctx.world);
544    physics.world.gravity = to_rp(physics.gravity);
545    physics.world.integration_parameters.dt = dt;
546    physics.world.step_with_events(&(), &physics.collector);
547    write_back(physics, &mut ctx.world);
548    while let Ok(event) = physics.collisions.try_recv() {
549        let (a, b, flags, started) = match event {
550            rp::CollisionEvent::Started(a, b, f) => (a, b, f, true),
551            rp::CollisionEvent::Stopped(a, b, f) => (a, b, f, false),
552        };
553        let (Some(a), Some(b)) = (physics.entity_of(a), physics.entity_of(b)) else {
554            continue;
555        };
556        let sensor = flags.contains(rp::CollisionEventFlags::SENSOR);
557        if started {
558            ctx.events.send(CollisionStarted { a, b, sensor });
559        } else {
560            ctx.events.send(CollisionStopped { a, b, sensor });
561        }
562    }
563}
564
565/// New bodies in, your changes applied, gone ones out.
566fn sync_into(physics: &mut Physics, world: &mut World) {
567    let step = physics.step;
568    world
569        .query::<(Entity, &Transform2D, &RigidBody, &Collider, Option<&Velocity>)>()
570        .for_each(|(e, t, rigid, shape, velocity)| {
571            let i = e.index() as usize;
572            let Some(link) = physics
573                .links
574                .get_mut(i)
575                .and_then(Option::as_mut)
576                .filter(|l| l.entity == e)
577            else {
578                if let Some(Some(old)) = physics.links.get(i).copied() {
579                    // A recycled index: the old entity's body goes first.
580                    physics.world.remove_body(old.body);
581                    physics.links[i] = None;
582                }
583                physics.insert(e, t, *rigid, *shape, velocity.copied().unwrap_or_default());
584                return;
585            };
586            link.seen = step;
587            if link.rigid != *rigid || link.shape != *shape {
588                // Rare (a door that becomes dynamic, a resized collider): rebuild.
589                let current = link.velocity;
590                physics.world.remove_body(link.body);
591                physics.insert(e, t, *rigid, *shape, velocity.copied().unwrap_or(current));
592                return;
593            }
594            // `bodies.get_mut` marks a body as changed (rapier then redoes its
595            // broad phase and wakes it), so only touch bodies you changed.
596            if link.transform != *t
597                && let Some(body) = physics.world.bodies.get_mut(link.body)
598            {
599                body.set_position(rp::Pose::new(to_rp(t.position), t.rotation), true);
600                link.transform = *t;
601            }
602            if let Some(&v) = velocity
603                && link.velocity != v
604                && let Some(body) = physics.world.bodies.get_mut(link.body)
605            {
606                body.set_linvel(to_rp(v.linear), true);
607                body.set_angvel(v.angular, true);
608                link.velocity = v;
609            }
610        });
611    // Despawned entities and ones that lost a physics component, in index
612    // order so runs stay deterministic.
613    for i in 0..physics.links.len() {
614        if physics.links[i].is_some_and(|l| l.seen != step) {
615            physics.remove(i);
616        }
617    }
618}
619
620fn write_back(physics: &mut Physics, world: &mut World) {
621    world
622        .query::<(Entity, &mut Transform2D, Option<&mut Velocity>)>()
623        .with::<RigidBody>()
624        .with::<Collider>()
625        .for_each(|(e, t, velocity)| {
626            let Some(link) = physics
627                .links
628                .get_mut(e.index() as usize)
629                .and_then(Option::as_mut)
630                .filter(|l| l.entity == e)
631            else {
632                return;
633            };
634            if link.rigid.kind == BodyKind::Fixed {
635                return;
636            }
637            // A sleeping body hasn't moved since it fell asleep.
638            let Some(body) = physics.world.bodies.get(link.body).filter(|b| !b.is_sleeping()) else {
639                return;
640            };
641            t.position = from_rp(body.translation());
642            t.rotation = body.rotation().angle();
643            link.transform = *t;
644            let v = Velocity {
645                linear: from_rp(body.linvel()),
646                angular: body.angvel(),
647            };
648            link.velocity = v;
649            if let Some(velocity) = velocity {
650                *velocity = v;
651            }
652        });
653}