Skip to content

jengine-paddleball

Paddle-and-ball arcade physics: swept ball collision, paddle deflection, input and AI paddles.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-paddleball
Version0.1.0
Entry pointjengine_paddleball::PaddleBallPackage
Script namespacePaddleball
Listed byprojects/arcade-classics, projects/engine-test-lab

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.

Scene keyRust type
ballBallpackage only: needs a v2 scene
ball_colliderBallColliderpackage only: needs a v2 scene
paddlePaddlepackage only: needs a v2 scene
brickBrickpackage 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
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
  • projects/arcade-classics/assets/scenes/breakout.scene
  • projects/arcade-classics/assets/scenes/pong.scene
  • projects/arcade-classics/assets/scenes/tilt.scene
  • projects/car-classics/assets/scenes/soccer.scene
  • projects/car-classics/assets/scenes/soccer_online.scene
  • projects/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.

  • 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_angle from the ends. Every paddle hit adds speed_gain, up to max_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 brick takes hits hits, then breaks: it stops colliding and its sprite goes transparent. Each hit reports its points. Broken bricks are not despawned, so Paddleball.bricks_reset() brings back a whole wall, and a second wall is a reset rather than a respawn.
  • Holding. Paddleball.ball_hold rests the ball on a paddle (Breakout’s serve) until Paddleball.ball_serve launches it.
  • Paddles, vertical or horizontal, clamped to min..max, driven by:
    • Input: the engine’s input_axis component 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.

Registered through register_script_commands, so they exist natively and in the editor viewport:

CallDoes
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

Every frame the package rewrites each ball’s BallState custom component:

FieldMeaning
eventsthis 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, vythe ball’s motion
rallypaddle 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) end
end

events 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.

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 --nocapture

It plays the real pong.scene against a perfect opponent and reports how many returns each setting makes per miss.

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 a PackageHost. 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).