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.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-pinball |
| Version | 0.1.0 |
| Entry point | jengine_pinball::PinballPackage |
| Script namespace | Pinball |
| Listed by | projects/arcade-classics |
Using it
Section titled “Using it”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.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
pin_table | PinTable | package only: needs a v2 scene |
Script commands
Section titled “Script commands”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.
| Call | Flat 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 |
Scenes that use it
Section titled “Scenes that use it”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.
What this one is for
Section titled “What this one is for”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).
How it drives the physics
Section titled “How it drives the physics”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 toPinball.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.
Component ("version": 2 scenes)
Section titled “Component ("version": 2 scenes)”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]}Script commands
Section titled “Script commands”| Call | Does |
|---|---|
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() |