Skip to content

jengine-pinball

A pinball table: the ball is a real rapier body with continuous collision, the flippers are kinematic transforms, and the table’s outline is checked closed before a ball is ever released.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-pinball
Version0.1.0
Entry pointjengine_pinball::PinballPackage
Script namespacePinball
Listed byprojects/arcade-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-pinball", "source": { "type": "path", "path": "../../packages/jengine-pinball" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Pinball == nil then return end.

Scene keyRust type
pin_tablePinTablepackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Pinball.new_game(e)pin_new_game
Pinball.flip(e, left, down)pin_flip
Pinball.plunge(e, power)pin_plunge
Pinball.contact(e, a, b, impulse)pin_contact
Pinball.nudge(e, dx)pin_nudge
  • projects/arcade-classics/assets/scenes/tilt.scene

From packages/jengine-pinball/src/lib.rs.

A pinball table for jEngine.

  • [table]: the table as geometry, and whether it holds a ball in.
  • [rules]: scoring, banks, tilt and the ball count. Pure.
  • [content]: tables and balls as data tables.

Every other physics game in the arcade tests physics at rest. Tumble Bay stacks crates and knocks them over; Pebble Peak walks a character controller along a floor. None of them go fast.

A pinball goes fast. It is small, the flippers hit hard, and the only thing between it and infinity is a wall a few units thick - so this is the game that finds out whether the engine’s physics survives speed. It did not: rapier integrates velocity and then looks for overlaps, so the ball went straight through the table. The engine had always constructed a CCDSolver and handed it to every step, but rapier sweeps only bodies that opt in and no jEngine body ever did, so the solver had run on an empty set since physics was added. RigidBody2D.ccd exists because of this game (BUGS #143).

Not by reaching into it. Every runtime drives rapier on its own hardcoded path and deliberately does not share PhysicsWorld as a package resource, so a package cannot step, impulse or query it. That is the documented design, not an oversight - and the first version of this package was written against the resource anyway, ran nothing at all, and reported phase=nil for thirty-six seconds.

So it works the way jengine-launcher does, which is the established pattern for a physics game here:

  • Pop is physical. A bumper’s collider has a restitution above 1, so it returns the ball with more energy than it arrived with. No impulse, no API, just the number rapier already reads. This is a better answer than an impulse anyway: it scales with how hard the ball hit, which is what a real bumper does.
  • Contacts come back through the script. The game reads Physics.get_contacts() and forwards each pair to Pinball.contact, which matches the ids against the parts this package spawned.
  • The plunger and the nudge are impulses, and those are the only two things a script has to do by hand. The package publishes the ball’s entity id and says how hard, in an event.

Flippers are kinematic bodies whose Transform.rotation this package writes each tick. PhysicsWorld::sync_kinematic_from_ecs turns that into set_next_kinematic_position, and rapier derives the implied velocity from the move - so a flipper imparts momentum without a joint, a motor, or any tuning of either.

pin_table on one entity.

"pin_table": {
"tables": { "tables": "data/pin_tables", "balls": "data/pin_balls" },
"ball": "steel", "which": 0, "sheet": "atlases/tilt.png",
"origin": [-200.0, -350.0]
}
CallDoes
Pinball.new_game(e)rack up three balls
Pinball.flip(e, left, down)a flipper button, up or down
Pinball.plunge(e, power)launch the ball in the lane, 0..1
Pinball.nudge(e, dx)shove the table; too much and it tilts
Pinball.contact(e, a, b, impulse)one row of Physics.get_contacts()