A parcel depot: parcels drop out of a hatch, and you place and turn three wooden boards so they slide through a funnel into the cart. Every fifth parcel is an express that shoots out of a tube faster than a board is thick per frame. This step starts the toolbox's physics suite with the separating axis test for convex shapes, sat_overlap, sat_sweep and sat_move, and a switch shows why the sweep matters.
Play it · Source: collection-spielwerk/26-paketrutsche · Toolbox 5.7.0 or later
The game
A shift is 120 seconds and a parcel drops every 2.5 seconds: a crate, a triangle, a hexagon or a long flat "Brief". The score is how many land in the cart. The boards start where about half get there, so doing nothing already delivers, and a missed parcel just falls out of the bottom. Parcels never turn, only boards do.
- Mouse or touch: drag a board to move it, drag one of its knobs to turn it, or turn the wheel over it. The box "Sweep" switches the sweep on and off.
- Keyboard: Tab picks a board, the arrows move it, Q and E turn it. W is the sweep.
New in the toolbox: SAT for convex polygons
The problem. Step 11 and step 17 used aabb_*: boxes that stay level. A board you can turn isn't a level box, and neither is a funnel's slope. Its bounding box covers a big empty triangle, and a parcel would stop in mid-air above it.
The idea. Two convex shapes are apart exactly when a line fits between them, and that line is always parallel to one of their edges. So look at their shadows on the normal of every edge. If any two shadows don't overlap, the shapes don't either. If they all overlap, the axis with the least overlap is the shortest way out. That's sat_overlap. A moving shape's shadow slides along each axis at its projected speed, so on each axis there is a time it starts and a time it stops overlapping. The shapes touch from the latest start to the earliest stop. That's sat_sweep, the same slab test aabb_sweep does on two axes, and exact for any speed.
// The smallest push that moves a out of b (normal from b towards a), or null.
sat_overlap(a: PolygonI, b: PolygonI): Contact | null
// When mover, moving by delta, first touches a still wall: time 0..1 and the normal of the face they meet on.
sat_sweep(mover: PolygonI, delta: Vector2I, wall: PolygonI): Hit | null
// Moves and slides along what it hits, at most 3 passes; returns how far it went.
sat_move(mover: PolygonI, delta: Vector2I, walls: readonly PolygonI[]): { offset: Vector2I; hits: Hit[] }
They reuse Hit from aabb and Contact from bounce. They are functions, not a world: the game owns its parcels, steps them and decides what a hit means. Here a hit takes the speed into the surface away, leaves a little bounce and a little friction, and lets the rest slide:
velocity.y += GRAVITY * STEP_SECONDS;
const delta = { x: velocity.x * STEP_SECONDS, y: velocity.y * STEP_SECONDS };
if (this.is_swept) {
const moved = sat_move(parcel.polygon, delta, walls);
parcel.x += moved.offset.x;
parcel.y += moved.offset.y;
for (const hit of moved.hits) {
this.land(parcel, hit.normal);
}
} else {
// the naive way: move, then push out of whatever it sits in
parcel.x += delta.x;
parcel.y += delta.y;
for (const wall of walls) {
const contact = sat_overlap(parcel.polygon, wall);
if (contact !== null) {
parcel.x += contact.normal.x * contact.depth;
parcel.y += contact.normal.y * contact.depth;
this.land(parcel, contact.normal);
}
}
}
What the switch shows. An express parcel moves 45 units a step, and a board is 10 thick. Turn "Sweep" off and the parcel is in front of the board one step and behind it the next. The overlap test never sees the two touch, or sees the parcel more than halfway through and pushes it out the far side. The game marks it "durch!". With the sweep on, the same parcel stops at the board's face and slides down it. A slow parcel behaves the same either way; only speed tells them apart.
What using it turned up. sat_move keeps the mover 0.01 units off what it hit (SAT_SKIN), so rounding never leaves it inside. And a parcel that can't turn will balance for ever with its flat bottom on a board's corner: the contact's normal is then the parcel's own face, straight up. Turning would tip it off. The game stands in for that: a parcel that has stood still for a moment and has its middle beyond every point holding it up gets a push to that side.
A test worth copying. No speed may get through a board one unit thick:
it("never ends inside a thin board, however fast", () => {
const thin = board({ x: -1e6, y: 120 }, { x: 1e6, y: 120 }, 1);
for (const speed of [200, 2000, 20000, 200000]) {
const square = box(-5, -5, 10, 10);
const result = sat_move(square, { x: speed / 10, y: speed }, [thin]);
expect(sat_overlap(moved(square, result.offset), thin)).toBeNull();
expect(result.offset.y).toBeLessThan(115);
}
});
The old SweepingSatWorld and PhysicsBody are gone. Despite the name, the world swept bounding boxes and stopped a body dead where it should slide. A physics world that steps bodies itself will come later, as its own step. Path: src/physics/sweeping_sat/Sat.ts. Polygon got translated() and rotated() for the boards and parcels.
From earlier steps
SeededRandomfrom step 01: which parcel drops where.SessionBestfrom step 05: the best shift.reflectandelastic_impactfrom step 16: the bounce off a board, and two parcels that bump.- The shell's
FixedStepClock, moved there from step 16 and step 17: sixty even physics steps a second, however uneven the frames. CheckBoxand theme rules from step 20; the switch that turns a feature off to show it is step 24's.- New in the shell:
on_wheel()for a wheel no widget took. Step 19 listened on the canvas itself to zoom; it uses the hook now too.
The scaffold, start screen, intro and scaling are explained once in Spielwerk 00: The workbench.
Embed it
<script src="https://www.game-object.de/tce-elements/tobidot/dll-tce-assets"></script>
<script src="https://www.game-object.de/tce-elements/tobidot/tce-paketrutsche"></script>
<tce-paketrutsche init-seed="4711"></tce-paketrutsche>
| Attribute | Values | Default |
|---|---|---|
init-seed |
whole number; the first shift, every next shift is seed + 1 | random |
init-intro |
true / false: play the GO logo intro at the first connection |
true |
Next
Previous: Spielwerk 25: Wochenmarkt · Next: Spielwerk 27: Sandkasten · All steps · Where the series is heading: Civilization
Comments