macrosoft3dpinball/godot
m3ultra 7347ec4db7 Pinball: Rules, Juice, HUD, a table probe — and the three bugs it found
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>
2026-08-09 18:18:15 +10:00
..
dev Pinball: Rules, Juice, HUD, a table probe — and the three bugs it found 2026-08-09 18:18:15 +10:00
probe Add Godot 4.7 physics testbed: Box3D/Jolt/GodotPhysics A/B pinball table 2026-08-06 05:33:07 +10:00
scenes Pinball: data-driven table spine (3 tables, 11 part kinds) 2026-08-09 17:24:07 +10:00
scripts Pinball: Rules, Juice, HUD, a table probe — and the three bugs it found 2026-08-09 18:18:15 +10:00
BOX3D_ENGINE_NOTES.md Add Box3D upstream research notes; note godot-box3d is single-threaded 2026-08-06 09:03:34 +10:00
godot-box3d.gdextension Add Godot 4.7 physics testbed: Box3D/Jolt/GodotPhysics A/B pinball table 2026-08-06 05:33:07 +10:00
godot-box3d.gdextension.uid Add Godot 4.7 physics testbed: Box3D/Jolt/GodotPhysics A/B pinball table 2026-08-06 05:33:07 +10:00
project.godot Pinball: data-driven table spine (3 tables, 11 part kinds) 2026-08-09 17:24:07 +10:00
README.md Add Box3D upstream research notes for the testbed 2026-08-06 08:54:25 +10:00
run_probe.sh Add Godot 4.7 physics testbed: Box3D/Jolt/GodotPhysics A/B pinball table 2026-08-06 05:33:07 +10:00

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:

  1. Tunneling — ball fired at a 1 cm static wall at 5160 m/s, CCD off and on.
  2. 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:

  1. 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_SPEED signs in scenes/table.gd to play under Jolt.
  2. 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).