jengine-paddleball
Paddle-and-ball arcade physics: swept ball collision, paddle deflection, input and AI paddles.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-paddleball |
| Version | 0.1.0 |
| Entry point | jengine_paddleball::PaddleBallPackage |
| Script namespace | Paddleball |
| Listed by | projects/arcade-classics, projects/engine-test-lab |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-paddleball", "source": { "type": "path", "path": "../../packages/jengine-paddleball" } } ]}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 Paddleball == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
ball | Ball | package only: needs a v2 scene |
ball_collider | BallCollider | package only: needs a v2 scene |
paddle | Paddle | package only: needs a v2 scene |
brick | Brick | 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 |
|---|---|
Paddleball.ball_serve(e, dir_x, dir_y) | ball_serve |
Paddleball.ball_reset(e, x, y) | ball_reset |
Paddleball.paddle_set_control(e, mode) | paddle_set_control |
Paddleball.paddle_set_ai(e, speed, error, predict, reaction) | paddle_set_ai |
Paddleball.paddle_resize(e, half_length, min, max) | paddle_resize |
Paddleball.ball_hold(e, holder, offset_x, offset_y) | ball_hold |
Paddleball.ball_set_speed(e, speed) | ball_set_speed |
Paddleball.bricks_reset() | bricks_reset |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/breakout.sceneprojects/arcade-classics/assets/scenes/pong.sceneprojects/arcade-classics/assets/scenes/tilt.sceneprojects/car-classics/assets/scenes/soccer.sceneprojects/car-classics/assets/scenes/soccer_online.sceneprojects/engine-test-lab/assets/scenes/69-paddleball.scene
From packages/jengine-paddleball/README.md.
Paddle-and-ball arcade physics: the reusable core of Pong, Breakout, Arkanoid and air hockey. A game built on it writes scoring, serving and presentation in Lua. The collision it doesn’t write, because that’s the part those games all get wrong the same way: a fast ball goes straight through a thin paddle.
Used by projects/arcade-classics (Pong and Breakout) and Engine Test Lab scene 69.
What it does
Section titled “What it does”- Swept collision. The ball is a box, and each frame it’s swept against every collider box, so it can’t tunnel at any speed. The test pins 6000 units/s through a 4-unit paddle on a 0.1 s frame. It resolves up to four bounces a frame, so corners work.
- Paddle deflection. A paddle sends the ball back at an angle set by where
it hit: straight back from the centre, up to
max_bounce_anglefrom the ends. Every paddle hit addsspeed_gain, up tomax_speed. A hit on a paddle’s end cap reflects like a wall. - Goals. Touching a goal stops the ball and reports the goal. What a goal means (a point, a lost life) is up to the game.
- Bricks. A
bricktakeshitshits, then breaks: it stops colliding and its sprite goes transparent. Each hit reports itspoints. Broken bricks are not despawned, soPaddleball.bricks_reset()brings back a whole wall, and a second wall is a reset rather than a respawn. - Holding.
Paddleball.ball_holdrests the ball on a paddle (Breakout’s serve) untilPaddleball.ball_servelaunches it. - Paddles, vertical or horizontal, clamped to
min..max, driven by:Input: the engine’sinput_axiscomponent on the same entity, so they can be rebound and a gamepad works through the same actions.Ai: tracks the nearest incoming ball, with a top speed, an aim error re-rolled for each approach, a reaction delay, and optional bounce prediction.None: stays where scripts put it.
Components (scenes using them must be "version": 2)
Section titled “Components (scenes using them must be "version": 2)”"ball": { "half_extents": [8, 8], "base_speed": 470, "speed_gain": 26, "max_speed": 1150, "max_bounce_angle": 50 },"ball_collider": { "half_extents": [8, 48], "kind": "Paddle" },"paddle": { "axis": "Vertical", "speed": 560, "min": -280, "max": 280, "control": "Ai", "ai_speed": 320, "ai_error": 50, "ai_predict": false, "ai_reaction": 0.25 },"brick": { "hits": 1, "points": 7 }A brick also needs a ball_collider (normally kind: Wall).
kind is Wall, Paddle or Goal. Collision ignores rotation and scale, so
set half_extents to match the sprite.
Script commands
Section titled “Script commands”Registered through register_script_commands, so they exist natively and in
the editor viewport:
| Call | Does |
|---|---|
Paddleball.ball_serve(e, dir_x, dir_y) | launch at base_speed along the direction; resets the rally |
Paddleball.ball_reset(e, x, y) | stop the ball and place it |
Paddleball.paddle_set_control(e, mode) | "input", "ai" or "none" |
Paddleball.paddle_set_ai(e, speed, error, predict, reaction) | tune the CPU |
Paddleball.paddle_resize(e, half_length, min, max) | change a paddle’s length and travel (Breakout’s shrinking paddle) |
Paddleball.ball_hold(e, holder, offset_x, offset_y) | rest the ball on holder until served |
Paddleball.ball_set_speed(e, speed) | change speed, keeping direction |
Paddleball.bricks_reset() | un-break every brick |
What scripts read
Section titled “What scripts read”Every frame the package rewrites each ball’s BallState custom component:
| Field | Meaning |
|---|---|
events | this frame’s touches: { kind, other, other_id, x, y, speed }. kind is wall, paddle, goal or brick; a brick event adds points, destroyed and hits_left |
moving, held, speed, vx, vy | the ball’s motion |
rally | paddle hits since the last serve |
for _, ev in ipairs(Entity.get_custom(ball, "BallState", "events") or {}) do if ev.kind == "goal" then point_for(ev.other) endendevents is replaced each frame, not appended to, so nothing needs
clearing and each touch is seen exactly once. That only holds if the script
reading it runs every frame: declare script = { tick_rate = 60 } (the base
rate, meaning every frame) or no rate at all, never anything lower.
Tuning a CPU
Section titled “Tuning a CPU”ai_predict makes a CPU nearly perfect at any speed, because it starts moving
to the right spot a whole court-crossing early. A CPU without prediction but
with a reaction delay misses more as the ball speeds up, which is how Pong is
supposed to feel. To measure settings instead of guessing them:
cargo test -p jengine-paddleball --test balance -- --ignored --nocaptureIt plays the real pong.scene against a perfect opponent and reports how many
returns each setting makes per miss.
Where it runs
Section titled “Where it runs”Its one system runs at ScriptExec, after the frame’s script commands (so a
serve takes effect the frame it’s issued). Like every package, it runs natively
and in a project-built editor viewport (./build-project-viewport.ps1), and
not in web builds, which host no packages yet.
src/sim.rs: the rules, on plain numbers (sweep, deflection, tunnelling, corners, AI aim, reaction, prediction).tests/pong_scene.rs: the arcade’s real scene through aPackageHost. It plays a minute of attract mode, scores an undefended goal, and runs every command.tests/breakout_scene.rs: the arcade’s Breakout scene. It checks the classic 112-brick wall, lets the CPU break bricks for two minutes, and resets the wall.tests/bricks_and_holding.rs: bricks, multi-hit bricks, reset, holding, speed changes and paddle resizing.tests/test_lab_scene.rs: test-lab scene 69, checking that both paddle axes return the ball.tests/balance.rs: the tuning report (ignored by default).