# Physics and movement ## Choosing a body type | Want | Use | Why | | --- | --- | --- | | Responsive player | kinematic capsule mover | direct control, no integrator lag, predictable collision response | | Thrown crate, debris, ragdoll | rigid body | mass, spin, and stacking come free | | Lift, door, moving platform | scripted transform | exact path, no drift | | Vehicle | `Car` block | suspension, wheels and weight transfer already solved | | Bullet | ray or `projectile_ccd` | discrete stepping tunnels through thin geometry | A player on a rigid body feels floaty and snags on seams. A crate on a mover feels weightless. This choice is not a matter of taste. ## Movers ```rust capsule_mover_step(/* world, entity, desired_motion, dt */); apply_mover_contacts(&mut dynamics, &mut entities); apply_sweep_pushes(&mut dynamics, &pushes); ``` The mover resolves against geometry and reports contacts. Grounding: `on_floor`, `ground_y`, `ground_normal`, plus `deck_floor_under(decks, pos, half, feet, climb)` and `box_floor_under(..)` for deck strips and boxes. The `climb` parameter is your step height — the single most feel-relevant constant on a walker. Too low and the character catches on every kerb; too high and it walks up walls. Slopes: use `ground_normal`. Above a walkable angle, slide instead of walk, or the player climbs cliffs. ## Rigid dynamics `RigidDynamics` owns bodies. Useful surface: ```rust dynamics.rigid_body_of(entity_id); // Option dynamics.rigid_impulse(entity_id, dv); // bool dynamics.rigid_spin(entity_id, axis_vel); dynamics.rigid_accel(entity_id); dynamics.sync_baseline(entity_id, pos, orient, vel); dynamics.take_mover_impacts(); // Vec - drive audio & decals from these step_dynamics(&mut dynamics, &mut entities); reconcile(/* ... */); // netplay correction ``` `take_mover_impacts()` is the correct source for impact SFX, `BulletDecal` marks and particle bursts. Detecting collisions a second time in gameplay code produces double sounds and drifting decals. Ragdoll: `activate_ragdoll(..)`, then `ragdoll_active(id)` and `ragdoll_body_poses(id)` to drive the skinned pose. The mover→ragdoll transition must be an explicit event, not an emergent state. ## Queries ```rust cast_ray(&dynamics, from, dir, max); // Option projectile_ccd(/* ... */); raycast_filter(); // sensible defaults - start here projectile_filter(); ``` Filter deliberately. A raycast that hits the shooter's own capsule is the classic "my gun does nothing" bug; a ground check that hits a trigger volume is the classic "player is permanently airborne" bug. ## Terrain, water, voxels, nav - `terrain` + `landform::LandKind` — heightfield ground. 65×65 with 100 movers and 50 bodies costs 0.038 ms/tick, so terrain resolution is a *visual* budget question, not a sim one. - `water` — `WaterVolume`, `WaterWave` (`MAX_WAVES`), `WaterState`, `BuoyancyApplied`, `dispersion_speed`. Buoyancy is applied to rigid bodies, so a floating crate is a rigid body, not a mover. - `voxel` — destructible/blocky volumes, with their own body count (`voxel_body_count`). - `nav` — `NavMap`, `NavAgent`, `FlowField`. Use a `FlowField` when many agents share one target (crowds, waves); use per-agent paths when targets differ. Building N paths to one goal is the standard performance mistake in wave games. - `LosProvider` / `NavProvider` — inject visibility and navigation into behaviour code instead of reaching into the world from a `Brain`. ## Tuning tables Starting values for a human-scale character (player capsule ~1.8 m tall). Tune from here; do not invent from zero. | Constant | Start | Note | | --- | --- | --- | | walk speed | 4.5 m/s | | | run speed | 8.0 m/s | ≤ ~2× walk or animation desyncs | | ground accel | 40 m/s² | reaches walk speed in ~0.11 s | | air accel | 8 m/s² | low, but non-zero — zero air control feels broken | | jump apex | 1.2 m | derive impulse from apex, not the reverse | | gravity | −22 m/s² | heavier than real; real gravity feels floaty | | fall multiplier | 1.8× after apex | the single biggest jump-feel improvement | | coyote time | 0.10 s | | | jump buffer | 0.12 s | | | step height (`climb`) | 0.4 m | | | max walkable slope | 45° | | Vehicles: tune `CarConfig`, not raw forces. Aircraft: tune `PlaneConfig`. ## Determinism Non-negotiable for netplay and replay: - Fixed `TICK_DT`. Accumulate leftover frame time; never step by a variable delta. - `makepad-game-math` transcendentals, not `std`, for anything crossing the wire. - Seeded `rng` from `makepad-game-gen`, never thread RNG. - Iterate deterministically ordered containers. A `HashMap` iteration order in the step function is a desync waiting for a player count above one. ## Testing All of this is Cx-free, so it is ordinary `cargo test`. Assert on observable behaviour: ```rust #[test] fn mover_clears_step_but_not_wall() { let mut w = world_with_step(0.4); walk_forward(&mut w, 1.0); assert!(player(&w).pos.y > 0.35, "should have climbed the 0.4 m step"); let mut w = world_with_step(1.5); walk_forward(&mut w, 1.0); assert!(player(&w).pos.y < 0.1, "must not climb a 1.5 m wall"); } ``` When the claim is about composition — "a walker can enter a real generated room" — test the composition. Two half-tests in two crates both pass while the walker floats above the floor; that has actually happened in this codebase, which is why `blocks` dev-depends on `gen` and `render`.