Skip to main content

unlit3dx/
physics.rs

1//! The physics threaded world: a fixed-step rapier simulation on its own thread.
2//!
3//! Gameplay sends jobs that build bodies; the thread steps the simulation at
4//! a fixed rate. A step drains rapier's contact events into
5//! [`PhysicsContacts`], the world's own component, and publishes nothing by
6//! itself: a caller that wants the body transforms or the contacts subscribes
7//! with a collector of its own. Rapier's math types are glam aliases, so
8//! whatever a collector copies crosses threads with no conversion.
9//!
10//! Contacts are events, not state. A step's contacts sit in
11//! [`PhysicsContacts`] until the next step overwrites them, so a caller that
12//! must see every one of them — bullet damage, say — has its collector queue
13//! them as they appear. A snapshot mailbox keeps only the newest, which is
14//! right for transforms and wrong for contacts.
15
16use std::sync::mpsc::{Receiver, channel};
17use std::time::Duration;
18
19use glam::{Quat, Vec3};
20use rapier3d::prelude::*;
21
22pub use rapier3d::prelude::{PhysicsWorld, RigidBodyHandle};
23use unlit_ecs::{Bundle, DynamicBundle, Entity, World};
24
25use crate::handle::WorldHandle;
26use crate::thread::{FixedClock, fixed_steps};
27use crate::world::WorldRunner;
28use unlit_ecs::bundle;
29
30/// The simulation's default step rate, in hertz.
31pub const PHYSICS_HZ: f64 = 60.0;
32
33/// How the physics world is set up before its first step.
34#[derive(Clone, Debug)]
35pub struct PhysicsConfig {
36    /// The step rate, in hertz.
37    pub hz: f64,
38    /// The gravity every dynamic body feels.
39    pub gravity: Vec3,
40    /// The command channel's capacity, in queued jobs.
41    pub command_capacity: usize,
42}
43
44impl Default for PhysicsConfig {
45    fn default() -> Self {
46        Self {
47            hz: PHYSICS_HZ,
48            gravity: Vec3::Y * -9.81,
49            command_capacity: 64,
50        }
51    }
52}
53
54/// The contacts rapier reported on the last step.
55///
56/// The component lives in the physics world and is rewritten by every step:
57/// it holds the contacts that began during that step, with the speed the pair
58/// was closing at, and those that ended. It is a reusable buffer, so a
59/// collector reads the contacts where they are instead of asking the world to
60/// build a copy it may not want.
61///
62/// A contact lives in exactly one step. A caller that must react to every one
63/// of them has its collector queue them as they appear; a caller that only
64/// wants the newest state can ignore them.
65#[derive(Clone, Debug, Default)]
66pub struct PhysicsContacts {
67    /// Contacts that began this step, with the closing speed of the pair.
68    pub started: Vec<(RigidBodyHandle, RigidBodyHandle, f32)>,
69    /// Contacts that ended this step.
70    pub stopped: Vec<(RigidBodyHandle, RigidBodyHandle)>,
71}
72
73/// The event plumbing a physics world carries between steps.
74///
75/// Rapier reports contacts through channels: the collector it is stepped
76/// with owns the sending ends, and this component keeps the collector and
77/// the receiving end of the collision channel alive across steps, so each
78/// step reuses them instead of allocating fresh channels.
79pub(crate) struct PhysicsEvents {
80    receiver: Receiver<CollisionEvent>,
81    collector: ChannelEventCollector,
82    /// The moving bodies' velocities, sampled before the step so a contact
83    /// can be reported with the speed the pair was closing at.
84    velocities: Vec<(RigidBodyHandle, Vec3)>,
85}
86
87impl PhysicsEvents {
88    fn new() -> Self {
89        let (collision_sender, receiver) = channel();
90        // The unused event kinds just vanish: a dropped receiver discards them.
91        let (force_sender, force_receiver) = channel::<ContactForceEvent>();
92        drop(force_receiver);
93        let (tear_sender, tear_receiver) = channel::<SoftBodyTearEvent>();
94        drop(tear_receiver);
95        Self {
96            receiver,
97            collector: ChannelEventCollector::new(collision_sender, force_sender, tear_sender),
98            velocities: Vec::new(),
99        }
100    }
101
102    /// Record the moving bodies' velocities before the step runs.
103    fn sample_velocities(&mut self, physics: &PhysicsWorld) {
104        self.velocities.clear();
105        self.velocities.extend(
106            physics
107                .bodies
108                .iter()
109                .filter(|(_, body)| body.is_dynamic() || body.is_kinematic())
110                .map(|(handle, body)| (handle, body.linvel())),
111        );
112    }
113
114    /// The velocity a body had before the step, or zero if it was still.
115    fn velocity(&self, body: RigidBodyHandle) -> Vec3 {
116        self.velocities
117            .iter()
118            .find(|(known, _)| *known == body)
119            .map_or(Vec3::ZERO, |(_, velocity)| *velocity)
120    }
121}
122
123/// A physics world, its event plumbing and the contacts its last step found.
124///
125/// The three are spawned together and read together: the collector is what
126/// lets a step report contacts at all, and the buffer is where those contacts
127/// land, so a world carrying only some of them either reports nothing or has
128/// nowhere to put what it found.
129pub struct Physics {
130    /// The rapier world the bodies live in.
131    ///
132    /// The one field a caller reaches into, because the body API takes it. The
133    /// other two are the plumbing [`step_once`] drives, and a caller reads the
134    /// contacts back as a component rather than through the bundle it spawned.
135    pub world: PhysicsWorld,
136    events: PhysicsEvents,
137    contacts: PhysicsContacts,
138}
139
140impl Physics {
141    /// A physics world under the given gravity, with empty event plumbing.
142    #[must_use]
143    pub fn new(gravity: Vec3) -> Self {
144        let mut world = PhysicsWorld::new();
145        world.gravity = gravity;
146        Self {
147            world,
148            events: PhysicsEvents::new(),
149            contacts: PhysicsContacts::default(),
150        }
151    }
152}
153
154impl Bundle for Physics {
155    fn into_bundle(self) -> DynamicBundle {
156        bundle![self.world, self.events, self.contacts]
157    }
158}
159
160/// The event plumbing a physics world carries, if it has any.
161fn physics_events(world: &World) -> Option<unlit_ecs::QueryOne<'_, &mut PhysicsEvents>> {
162    let entity = world
163        .root_scope()
164        .query::<(Entity, &PhysicsEvents)>()
165        .iter()
166        .next()
167        .map(|(entity, _events)| entity)?;
168    world
169        .root_scope()
170        .query_one::<&mut PhysicsEvents>(entity)
171        .ok()
172}
173
174/// The contact buffer a physics world carries, if it has one.
175fn physics_contacts(world: &World) -> Option<unlit_ecs::QueryOne<'_, &mut PhysicsContacts>> {
176    let entity = world
177        .root_scope()
178        .query::<(Entity, &PhysicsContacts)>()
179        .iter()
180        .next()
181        .map(|(entity, _contacts)| entity)?;
182    world
183        .root_scope()
184        .query_one::<&mut PhysicsContacts>(entity)
185        .ok()
186}
187
188/// The collider shape a dynamic body carries.
189#[derive(Clone, Copy, Debug)]
190pub enum BodyShape {
191    /// A sphere of the given radius, in metres.
192    Ball(f32),
193    /// An axis-aligned box of the given half extents, in metres.
194    Cuboid(Vec3),
195    /// A cylinder along the y axis.
196    Cylinder {
197        /// Half the height, in metres.
198        half_height: f32,
199        /// The radius, in metres.
200        radius: f32,
201    },
202    /// A cone pointing up the y axis.
203    Cone {
204        /// Half the height, in metres.
205        half_height: f32,
206        /// The base radius, in metres.
207        radius: f32,
208    },
209}
210
211/// A falling, bouncing body: the simulation the example drives.
212#[derive(Clone, Copy, Debug)]
213pub struct BodyDesc {
214    /// Where the body starts, in metres.
215    pub position: Vec3,
216    /// The velocity the body starts with, in metres per second.
217    pub velocity: Vec3,
218    /// How bouncy the body is, from zero to one.
219    pub restitution: f32,
220    /// The collider shape the body carries.
221    pub shape: BodyShape,
222}
223
224/// Spawn a dynamic body with a collision-event collider, returning it.
225///
226/// # Panics
227///
228/// Panics when the world carries no [`PhysicsWorld`]. The physics world is
229/// created before any job runs, so jobs sent through
230/// [`spawn_physics_world_with`] never see this.
231#[must_use]
232pub fn spawn_body(world: &mut World, desc: BodyDesc) -> RigidBodyHandle {
233    let mut physics = physics_world(world);
234    let body = physics.get().insert_body(
235        RigidBodyBuilder::dynamic()
236            .translation(desc.position)
237            .linvel(desc.velocity)
238            .build(),
239    );
240    let collider = match desc.shape {
241        BodyShape::Ball(radius) => ColliderBuilder::ball(radius),
242        BodyShape::Cuboid(half) => ColliderBuilder::cuboid(half.x, half.y, half.z),
243        BodyShape::Cylinder {
244            half_height,
245            radius,
246        } => ColliderBuilder::cylinder(half_height, radius),
247        BodyShape::Cone {
248            half_height,
249            radius,
250        } => ColliderBuilder::cone(half_height, radius),
251    };
252    physics.get().insert_collider(
253        collider
254            .restitution(desc.restitution)
255            .active_events(ActiveEvents::COLLISION_EVENTS),
256        Some(body),
257    );
258    body
259}
260
261/// Remove a body and its colliders from the physics world.
262///
263/// Returns whether the handle named a body. A caller that recycles a pool
264/// of bodies — a shape that has rested long enough to be replaced — calls
265/// this to give the simulation its memory back; the ground a scene builds
266/// once is left alone.
267///
268/// # Panics
269///
270/// Panics when the world carries no [`PhysicsWorld`].
271#[must_use]
272pub fn despawn_body(world: &World, body: RigidBodyHandle) -> bool {
273    physics_world(world).get().remove_body(body).is_some()
274}
275
276/// Spawn a fixed cuboid: a floor, a wall, whatever gameplay needs.
277///
278/// # Panics
279///
280/// Panics when the world carries no [`PhysicsWorld`].
281pub fn spawn_fixed_cuboid(world: &mut World, position: Vec3, half_extents: Vec3, friction: f32) {
282    let mut physics = physics_world(world);
283    let body = physics
284        .get()
285        .insert_body(RigidBodyBuilder::fixed().translation(position).build());
286    physics.get().insert_collider(
287        ColliderBuilder::cuboid(half_extents.x, half_extents.y, half_extents.z).friction(friction),
288        Some(body),
289    );
290}
291
292/// The physics world as a mutable guard, for jobs that build bodies.
293fn physics_world(world: &World) -> unlit_ecs::QueryOne<'_, &mut PhysicsWorld> {
294    let entity = world
295        .root_scope()
296        .query::<(Entity, &PhysicsWorld)>()
297        .iter()
298        .next()
299        .map(|(entity, _physics)| entity)
300        .expect("the physics world exists");
301    world
302        .root_scope()
303        .query_one::<&mut PhysicsWorld>(entity)
304        .expect("the physics world exists")
305}
306
307/// Spawn the physics world driven by `R`: on its own thread
308/// ([`Threaded`](crate::world::Threaded)), or stepped by the host
309/// ([`Stepped`](crate::world::Stepped)).
310///
311/// The world owns the rapier world as a [`PhysicsWorld`] component, and
312/// gameplay never touches it: jobs build bodies, and a subscriber reads the
313/// state back. The world is created before the first job runs, so a job that
314/// builds a body — the static geometry, usually — finds it ready.
315///
316/// Nothing is published until someone subscribes: the world runs a
317/// subscriber's collector once per step, after the step has drained its
318/// contacts. Subscribing with the handle's [`JobSender`](crate::job::JobSender)
319/// is how a host asks for transforms, contacts, or both.
320///
321/// Both runners share one tick body, so a stepped world simulates exactly
322/// what a threaded one does: the only difference is that its host calls
323/// [`crate::world::step_stepped_world`] once per frame instead of a thread
324/// pacing itself.
325pub fn spawn_physics_world_with<R: WorldRunner>(
326    host: &mut World,
327    config: PhysicsConfig,
328) -> WorldHandle {
329    let step_seconds = 1.0 / config.hz as f32;
330    let mut clock = FixedClock::new(Duration::from_secs_f32(step_seconds));
331    let mut debt = 0.0;
332    let mut started = false;
333    R::spawn(
334        host,
335        "physics",
336        config.command_capacity,
337        move |world| init_physics_world(world, &config),
338        move |world, control, (), delta_seconds| {
339            if control.stop_requested() {
340                return false;
341            }
342            if control.self_driven() {
343                let steps = fixed_steps(&mut debt, delta_seconds, step_seconds);
344                // The first turn steps immediately, before its first wait.
345                for _ in 0..steps.max(u32::from(!started)) {
346                    step_once(world);
347                }
348                started = true;
349                clock.wait(control);
350            } else {
351                // A stepped host has already decided that one step is due.
352                step_once(world);
353            }
354            true
355        },
356    )
357}
358
359/// Create the physics world and its event plumbing in `world`, with
360/// `config` applied.
361///
362/// Thread-free callers that drive [`step_once`] on their own cadence —
363/// deterministic tests, offline rendering — call this to get the world
364/// [`spawn_physics_world_with`] builds, contact events included.
365pub fn init_physics_world(world: &mut World, config: &PhysicsConfig) {
366    let mut physics = Physics::new(config.gravity);
367    physics.world.integration_parameters.dt = 1.0 / config.hz as f32;
368    world.root_scope_mut().spawn(physics);
369}
370
371/// Step the rapier world once, draining its contacts into
372/// [`PhysicsContacts`].
373///
374/// Returns whether `world` carries a [`PhysicsWorld`] at all. A physics
375/// world's own tick calls this; thread-free callers — deterministic tests,
376/// offline rendering — drive the same simulation on a cadence of their own.
377pub fn step_once(world: &World) -> bool {
378    let mut physics_world_rows = world.root_scope().query::<&mut PhysicsWorld>();
379
380    let Some(physics) = physics_world_rows.iter().next() else {
381        return false;
382    };
383    // The contact events rapier reports this step describe contacts that
384    // began during it, and by the time the step returns the solver has
385    // already turned the approach into a bounce. Sampling the velocities
386    // before the step is what makes the reported speed the approach speed
387    // rather than the separation it left behind.
388    let mut events = physics_events(world);
389    if let Some(events) = events.as_mut() {
390        events.get().sample_velocities(physics);
391    }
392    match events.as_mut() {
393        Some(events) => physics.step_with_events(&(), &events.get().collector),
394        None => physics.step(),
395    }
396    let Some(mut events) = events else {
397        return true;
398    };
399    let Some(mut contacts) = physics_contacts(world) else {
400        return true;
401    };
402    contacts.get().started.clear();
403    contacts.get().stopped.clear();
404    let events = events.get();
405    while let Ok(event) = events.receiver.try_recv() {
406        let (one, two) = match event {
407            CollisionEvent::Started(one, two, _) => (one, two),
408            CollisionEvent::Stopped(one, two, _) => (one, two),
409        };
410        let Some((body_one, body_two)) = body_pair(physics, one, two) else {
411            continue;
412        };
413        match event {
414            CollisionEvent::Started(..) => {
415                let speed = contact_speed(physics, events, one, two);
416                contacts.get().started.push((body_one, body_two, speed));
417            }
418            _ => contacts.get().stopped.push((body_one, body_two)),
419        }
420    }
421    true
422}
423
424/// The speed two colliders close at along their contact normal, from the
425/// velocities sampled before the step that reported the contact.
426///
427/// Rapier reports the normal in the first collider's local frame, so it is
428/// rotated into world space before the relative velocity is projected onto
429/// it. The result is negative while the pair is separating.
430fn contact_speed(
431    physics: &PhysicsWorld,
432    events: &PhysicsEvents,
433    one: ColliderHandle,
434    two: ColliderHandle,
435) -> f32 {
436    // The pair stores its colliders in the broad phase's own order, which
437    // need not match the event's order: the manifold normal belongs to the
438    // pair's first collider, so project the pair's own relative velocity.
439    let Some(pair) = physics.contact_pair(one, two) else {
440        return 0.0;
441    };
442    let (first, second) = (pair.collider1, pair.collider2);
443    let normal = pair
444        .manifolds()
445        .first()
446        .map(|manifold| {
447            let rotation = physics
448                .colliders
449                .get(first)
450                .map_or(Quat::IDENTITY, |collider| collider.rotation());
451            rotation * manifold.local_n1
452        })
453        .unwrap_or(Vec3::ZERO);
454    let velocity = |handle: ColliderHandle| {
455        physics
456            .colliders
457            .get(handle)
458            .and_then(|collider| collider.parent())
459            .map_or(Vec3::ZERO, |body| events.velocity(body))
460    };
461    normal.dot(velocity(first) - velocity(second))
462}
463
464/// The bodies a collider pair belongs to, or nothing when either side is
465/// parentless.
466fn body_pair(
467    physics: &PhysicsWorld,
468    one: ColliderHandle,
469    two: ColliderHandle,
470) -> Option<(RigidBodyHandle, RigidBodyHandle)> {
471    let body = |handle: ColliderHandle| physics.colliders.get(handle)?.parent();
472    Some((body(one)?, body(two)?))
473}