PinballRamp
A ramp for a pinball table, built from where it starts, where it ends and how high it goes.
What it does
You give it three things: the mouth on the playfield (from), the top (to), and how high the top is (rise). On its first frame it builds a tilted floor with a rail down each side. Each piece is a box with a static Physics body. The ball rolls up under its own speed, so a weak shot rolls back down.
The slope comes straight from those numbers. Pinball Tower's left ramp runs from [-5.6, 22] to [-6.2, 9] and rises 2.4. That is 13.01 across the table and 2.4 up, a slope of 10.4 degrees, on a floor 13.23 long.
The floor sits half its thickness low, so its top face meets the playfield exactly at the mouth. Put the box's middle on the slope instead and the mouth is a step half a thickness high, and the ball hits it like a kerb.
When the ball gets within madeRadius of the top, it says pinballRamp.made and score.add. That distance is measured in 3D, so a ball rolling under the top of the ramp does not count. It fires once per trip: the ball has to leave the top before it can make the ramp again.
What happens at the top is up to you. Set endStop and a bar across the top sends the ball back down. Or put a WireRail there to carry it away.
Use it in a scene
A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.
Build it from scratch with the bjs CLI:
- 1Scaffold a project
World, renderer, and dev server — ready to run.
- 2Install dependencies
- 3Add PinballRamp
Copies its source into src/ so the scene resolves.
First time? Run bjs login once.
- 4Paste the scene into src/scenes/arcade-room.ts and run
SceneLoader builds the world from the JSON; reload to rebuild.
The ball needs the ball tag. Up the table is -Z, so to usually has the smaller z.
Props
from([x, z], default[0, 4]). The mouth, on the playfield.to([x, z], default[0, -4]). The top.rise(number, default2). How high the top is above the playfield.width(number, default1.8). Floor width between the rails. The ball is 0.84 across.thickness(number, default0.15). Floor thickness.railHeight(number, default0.7). Rail height above the floor.endStop(boolean, defaultfalse). A bar across the top.points(number, default1000). Paid toownerEntityfor each made ramp.ownerEntity(string, default"Player"). TheScorebucket to pay.madeRadius(number, default1.2). How close to the top the ball must get.color([r, g, b], default[0.35, 0.75, 1]). Floor colour.railColor([r, g, b], default[0.85, 0.9, 1]). Rail colour.alpha(number, default0.55). Floor see-through, 0 to 1. A see-through floor casts no shadow.
Events
Emits:
pinballRamp.made,{ entityId, ballId, points }. The ball reached the top.score.add,{ ownerEntity, points }. At the same moment.
Dependencies
MeshPrimitive. The floor, rails and stop are boxes.Physics. Each piece is a static body.Shadow. The rails cast shadows.
Notes
- The pieces are child entities named
<id>_floor,<id>_rail0,<id>_rail1and<id>_stop. Remove the ramp and they go too. - After a made ramp, the ball must get 2.5 ×
madeRadiusfrom the top before the next one counts.












































