| name | machin-game-demo-physics |
| description | Build, run, and modify machin-game-demo-physics — a 3D verlet physics sandbox with pure-MFL position-based dynamics, distance constraints, sphere-ground and sphere-sphere collision, and a chain pendulum. Use when working on this repo or as the reference example of verlet physics in machin (value-semantic particle arrays, constraint relaxation, O(n²) collision). |
machin-game-demo-physics
A 3D verlet physics sandbox — spheres fall, collide, stack, and a chain pendulum swings through them. Written in machin (MFL) via raylib's C FFI. This is the pure-MFL physics layer that the Tier 4 combat sim needs — no C library, no FFI callbacks, just the Vec3 module and a constraint solver.
Shared game-dev setup, the FFI surface, and cross-cutting gotchas live in the canonical machin-gamedev skill. This file covers the specifics of this demo.
Build & run
./build.sh
./machin-game-demo-physics
Needs machin v0.48.0+, a C compiler, raylib, and a display. build.sh prefers a system raylib, else vendors the prebuilt static release into vendor/ (no root).
Architecture
math3d module (inline)
The Vec3 type with 10 ops — identical to the solar demo's module. Every physics calculation (position, velocity, constraint projection, collision normal) flows through it.
Verlet particle
type Particle struct {
pos Vec3 // current position
old Vec3 // previous position (velocity = pos - old)
radius float // for collision and rendering
mass float // not used for verlet integration (equal for all), for future weighting
pinned int // 1 = fixed in place
}
Physics world
type Physics struct {
particles []Particle
constraints []Constraint
gravity Vec3
damping float
substeps int
iters int // constraint relaxation iterations per substep
}
Simulation loop (fixed timestep)
accumulator += frame_dt
while accumulator >= FIXED_DT:
for each substep (6):
phys_integrate(w, dt_sub) // verlet step: pos += (pos-old)*damping + g*dt²
for each iteration (3):
phys_solve_constraints(w) // project overlapping particles
phys_collide_ground(w, 0.0) // clamp pos.y ≥ radius
phys_collide_spheres(w) // O(n²) sphere-sphere push-apart
accumulator -= FIXED_DT
if steps > 8: break // spiral-of-death cap
Rendering
- Particles:
DrawSphere(pos, radius, color) — raylib draws smooth filled spheres. Color is a velocity heat map: cold blue (still) → cyan → yellow → hot red (fast).
- Constraints:
DrawLine3D between particle centers. Color indicates tension: stretched → red, compressed → green, relaxed → grey-blue.
- Ground:
DrawGrid(40, 1.5) for a reference plane.
Scene
The demo spawns:
- A chain pendulum — 20 particles, top one pinned, connected by distance constraints. Hangs from
(-3, 13, 0) and swings under gravity.
- 64 random particles — scattered in a box
(-5..5, 4..14, -5..5) with random radii (0.25–0.5). They fall, collide with the ground and each other, and form a pile.
- 2 heavy spheres — large mass, at
(3, 10, 2) and (-2, 8, -3), radius ~1.0. They push through the light particles.
Patterns worth copying
- Verlet avoids velocity storage.
old_pos is the implicit velocity store. The integration is a one-liner. No Euler/Verlet distinction — just new = pos + (pos-old)*damping + a*dt².
- Parallel slices for particle arrays. Cstruct types like
Model can't be fields of MFL type structs, but Particle is a pure-MFL struct so it's fine in a []Particle. For the physics module, everything is pure MFL types.
- Constraint relaxation. Don't solve constraints exactly — iterate 3–5 times per substep. The half-step
*0.5 distributes the correction equally between paired particles.
- Color by speed for live diagnostics. One
speed_color() function turns a scalar into a heat map without any branch-per-pixel overhead.
- Separate substeps from frame rate. The physics tick is always
1/60; multiple substeps (6) maintain stability. The accumulator pattern from solar re-used verbatim.
Modifying
- Scene density: change the number of particles in
main() (the 64‑particle while loop range, build calls, or build_grid call — build_grid is available but commented out).
- Gravity:
phys_new(0.0, -15.0, 0.0, ...) — the Y value can be adjusted or negated for antigravity.
- Damping: the
0.995 factor in phys_new — higher = more elastic, lower = more damped.
- Substeps: 6 is stable for fast-falling particles. Lower for performance, higher for stability.
- Constraint iterations: 3 is a good balance. More = stiffer constraints at higher CPU cost.
Future directions
- Soft body cubes — particles at cube corners + edge constraints + a center constraint → a deformable block that squishes and bounces.
- Cloth simulation —
build_grid() is already written; uncomment and the demo shows a pinned grid of particles acting as hanging cloth.
- Mouse picking — raycast from camera → find nearest particle → drag it with a temporary constraint.
- Friction / restitution — per-particle bounce coefficient, Coulomb friction at ground contact.
- Floor rendering — a
DrawCube ground plane instead of just a grid, optionally with a checkerboard texture.
- Extract physics module —
physics.src as a vendored module for reuse.