Four lanes built in parallel onto the data-driven spine: - Rules.gd (509) — score with a decaying combo multiplier, 3 balls, ball save, escalating drop-bank bonuses, lit rollover lanes that level up when swept, saucer-lock multiball, two-warning tilt, end-of-ball bonus, per-table high scores in user://pinball_scores.cfg. - Juice.gd (784) — 12 procedural voices synthesized at boot, no assets. The spinner ratchet is timed to Table.gd bleeding spin_left at 6.0/s so the ticking stops exactly when the blade does. Pooled voices/lights/sparks, quadratic-falloff shake, and a damped spring for nudge. Palette-tinted lights with a value floor, because a light the colour of a near-black saucer illuminates nothing. - Hud.gd (935) — score that ROLLS UP rather than snapping, combo, award callouts, plunger meter, tilt banner, ball save, game over panel. - dev/probe_tables.gd (480) — the gate: builds every table and checks parts, part overlaps, playfield bounds, flipper swing, that the ball actually DRAINS, and that everything settles. The probe immediately found three real bugs the flipper autotest could not see: 1. The drain sat 3 cm PAST the playfield lip, so on four of five tables the ball rolled off the end into the void and the trigger never fired. A table you cannot lose a ball on is not a table. Moved inside the slab. 2. Inlane posts were level with the slingshots and interpenetrated them on three tables — on a real table the posts sit below. Moved down-table. 3. THE DIG's counter ramp ran through the crate bank, and a 15 deg yaw over a 28 cm ramp swings its far end 7 cm sideways, which then walked it through the left rail. Less yaw, tucked in, crates slid right. PROBE_TABLES: PASS 5/5. AUTOTEST: ALL TABLES OK. Still open, and it is an ENGINE finding not a table one: godot-box3d does not enforce HingeJoint3D angular limits — flippers swing 147-160 deg against a 62 deg limit. The probe reports it as a NOTE per table. Worth an upstream issue alongside the motor-sign inversion already in the README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| dev | ||
| probe | ||
| scenes | ||
| scripts | ||
| BOX3D_ENGINE_NOTES.md | ||
| godot-box3d.gdextension | ||
| godot-box3d.gdextension.uid | ||
| project.godot | ||
| README.md | ||
| run_probe.sh | ||
Godot physics testbed (Box3D / Jolt / GodotPhysics A/B)
A minimal playable pinball table for Godot 4.7 whose real job is comparing 3D physics backends on the things pinball stresses: fast-small-ball CCD, motorised hinge flippers, and joint limits. The backend is one project setting, so the same table runs under all three engines.
Run it
/Applications/Godot.app/Contents/MacOS/Godot --path godot
Keys: Z left flipper, / right flipper, Space plunger, R reset ball.
The project defaults to Box3D Physics (Extension) via
godot-box3d (MIT). bin/ carries a
macOS arm64 dylib built from source (upstream has no releases; build recipe below).
A/B probe harness
godot/run_probe.sh # GODOT_BIN=... to point at a different binary
Writes override.cfg to switch physics/3d/physics_engine, runs probe/probe.tscn
headless under each backend, and prints per-engine results (also saved to
probe/results/, untracked). Two tests:
- Tunneling — ball fired at a 1 cm static wall at 5–160 m/s, CCD off and on.
- Hinge motor — a motorised HingeJoint3D arm; a flipper is a motorised hinge.
There is also a table-level flipper check:
PINBALL_AUTOTEST=1 godot --headless --path godot flips both real flippers for 0.5 s
and prints the swing angles.
Results (2026-08-06, Godot 4.7.stable, M3 Ultra)
Tunneling — ball contained by the 1 cm wall?
| speed m/s | Godot ccd off | Jolt ccd off | Box3D ccd off | all three, ccd on |
|---|---|---|---|---|
| 5 | tunneled* | tunneled* | contained | contained |
| 10 | tunneled | contained* | contained | contained |
| 20 | contained* | contained* | contained | contained |
| 40 | contained* | contained* | contained | contained |
| 80 | tunneled | tunneled | contained | contained |
| 160 | tunneled | tunneled | contained | contained |
* With a 60 Hz tick, every speed here can step over a 1 cm wall; the discrete-engine
"contained" rows are phase luck (2 m at 20 and 40 m/s lands a tick exactly on the wall
— and both engines produce identical trajectories, so they get lucky identically).
The signal is the pattern, not individual rows: Box3D never tunnels even with the
CCD flag off (speculative contacts are always on), the other two need
continuous_cd = true, and with it all three hold at 160 m/s.
Hinge motor (target 5 rad/s, 1 s):
| engine | reached rad/s | moved | verdict |
|---|---|---|---|
| GodotPhysics3D | −5.0 | 73.5° (wrapped) | works |
| Jolt | −5.0 | 75.1° (wrapped) | works |
| Box3D | +5.0 | 73.5° (wrapped) | works |
Flipper autotest on the real table (motors at ±25 rad/s, limits −5°…+50°):
| engine | left swing | right swing |
|---|---|---|
| Box3D | +51.8° — stops at the 50° limit | −51.8° — stops at limit |
| Jolt | −106.6° — wrong way, blows ~100° past the limit | +104.3° — same |
Two real findings, half a second of headless runtime each:
- Box3D's hinge motor sign is inverted relative to GodotPhysics/Jolt (upstream
drop-in-compatibility bug candidate for godot-box3d). The table's flipper signs are
tuned for Box3D, the project default — flip
FLIP_SPEED/REST_SPEEDsigns inscenes/table.gdto play under Jolt. - With the motor overdriving into them, Box3D enforces the angular limits hard while Jolt lets this setup punch far through them.
Engine notes
Upstream Box3D research — Apple Silicon threading (cap workers at 24, not 32), the benchmark harness and how to re-run it, what godot-box3d does not support, and why this game became the testbed — is in BOX3D_ENGINE_NOTES.md.
Rebuilding the extension
git clone --recurse-submodules https://github.com/bearlikelion/godot-box3d
cd godot-box3d && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
cp bin/libgodot-box3d.dylib <here>/bin/
Known Box3D gaps that don't matter for pinball: no Generic6DOF/ConeTwist joints, no SoftBody3D, Area3D can't detect trimesh/heightmap bodies (the ball is a sphere; fine). Box3D also ignores Godot's hinge BIAS/LIMIT_SOFTNESS/LIMIT_RELAXATION params (warns at load; harmless here).