Collision
How Blupi meets the world: what is solid, how he moves through it, and the shapes of the things that kill him.
All of Blupi's movement and collision is in BlupiController
(src/GalaxyEggbert/Game/BlupiController.{hpp,cpp}). It is engine-independent: it sees the
World, a list of crate cells, a list of SolidBoxes and
plain flags the host computes (vanishing tiles passable this tick, at the water surface, in deep
water, pushing a crate...). That is why tools/VerifyBlupiMovement.cpp can test it without
a window. The rules copy the original game's (Decor::TestPath(),
BlupiAdjust(), the quarter-cell table) and adapt them to a third axis. Described as of
2026-11-10.
Blupi's body
| Constant | Value | Meaning |
|---|---|---|
| position | feet, render space | X/Z cell centres on integers; collision Y is bottom-anchored: cell gy owns [gy, gy + 1), so standing on a block at grid Y = G puts his feet at G + 1. |
kHeadHeight | 49/64 block | His head: the top of the original's 60-pixel BlupiRect. He fits under a one-block ceiling. |
kBodyHalfWidth | 18/64 block | Half his width against walls, on X and Z alike, on foot and swimming. A vehicle keeps a single point. |
kStepLimit | 4/64 block | The only "step" he takes by walking (below). |
kMoveSpeed | 2.5 blocks/s | The original's 8 px per 20 Hz tick, with its four-tick ease-in. |
kGravity, kFallLimit | 25, −12.5 blocks/s | Fastest fall = the original's 40 px a tick. |
What is solid: IsPointSolid()
Every collision query reduces to "is this point solid?". The answer is decided in this order:
- Crates and solid boxes. Inside a crate's unit cube or a
SolidBox: solid (no sub-cell mask). - Air and decor. Air, and any block whose registry entry has
decorset (passable scenery, including the standalone saw blade): not solid, neither floor nor ceiling. - Lava is solid up to its animated surface height (
LavaSurfaceHeight(), below), so he can stand in it, and dies from the contact test. - Special tiles: the vanishing saucer (
Temp) is not solid during its passable ticks; door bars fill their whole cell; a fan is solid only where its base, motor, shaft and hub are (FanBodySolid()); the slime drip and the crusher hang in his own cell and are never solid; a teleporter's top is a floor only for feet coming from above; wind, water and hanging bars are not solid; the flight barrier (214) lets the helicopter and overcraft through. - Quarter-cell mask. Otherwise, if sub-cell precision is asked for, the bit of the 4 × 4
collisionMaskfrom the BlockDefinitionRegistry under the point (columns from X, rows from Y, row 0 at the top; the mask is extruded through Z). Without sub-cell precision any remaining block is solid.
The resolver asks for sub-cell precision horizontally but not vertically. Thin markers such as hub signs therefore do not stop him when he walks, while thin hazard tiles (lava, crusher, saw, lightning) are still ground to stand on, which the hazard checks rely on.
Crates and SolidBoxes
- Crates (object type 12): every tick the host passes the crates' current centres to
SetCrateCells(). Each is a solid unit cube where the crate actually is, also between two cells while it is pushed. He stands on crates and cannot walk through them. - Solid BigDecor:
SetSolidBoxes(worldRuntime_.SolidBigDecorBoxes()), set whenever the world changes. ASolidBox(src/GalaxyEggbert/Game/SolidBox.hpp) is an axis-aligned box in raw grid coordinates. The game measures the BigDecor 3D models when it loads them, one box per half-block slice of height (a palm's trunk is thin, its crown wide); each box is turned by the cell's object-turn record. An icon inkSolidBigDecorIconswithout a measured model is a 1 × 2 × 1 column over its anchor. - Other objects (enemies, pickups, lifts) are not obstacles; they are handled as contacts by InteractionSystem. Lifts carry him through
RideLift().
Moving: one merged sweep
Each Step() computes the whole intended displacement (walk or swim along his facing,
gravity, wind) and resolves it once with ResolveMove(), the 3D equivalent of the
original's Decor::TestPath(). It marches toward the target and stops on whichever axis
meets something solid, reporting blockedX, blockedY, blockedZ
and onGround. One merged resolve treats a wall, a ceiling at a jump's apex and a
helicopter climbing into a roof the same way.
No step-up: he jumps
kStepLimit, a tolerance of 4 of a block's 64
pixels, so that seams between surfaces of almost equal height (a lift's top, a sub-cell floor)
cannot snag him. With his body's width only a real step at the leading face lifts him; a wall stays
a wall.Jumps and ceilings
- Jump speeds follow the original's four magnitudes (
Decor::IsNormalJump()):kJumpSpeed= 12 blocks/s with clear headroom, × 26/16 with the power-up, × 12/16 when the two cells over his head are not clear (HasJumpHeadroom()). Springs, the skateboard and the water exit have their own values. - A fast rise stopped by a ceiling is the "ouch" bump and a hurt landing; a slower one taps the ceiling with its sound.
Ledges
Standing still with his front foot over the void and the back one on the ground, he teeters for 16 ticks and then steps back 4 px a tick for 3 ticks; with the void behind him he steps forward instead. The jeep, tank and skateboard roll on 5 px a tick for 10 ticks the way they face. Foot probes are 0.15 block either side of his middle. Walking off a ledge drops him once his whole box is past it.
Water
Water cells are never solid. The host decides the mode each tick: water in his cell with dry air above is surf (floating with his feet 18/64 below the surface), water above too is swimming. Swimming has no gravity: jump or Right Shift swims up, Left Shift down, easing toward 5 px a tick; without input he drifts up slowly. Breath lasts 100 gauge steps of 0.25 s (about 25 s) and refills when he surfaces. From the surface, a jump with open air above makes the original's weak jump out of the water. A vehicle that enters water is wrecked first.
Wind
Wind tiles at his middle push him sideways (110, 114, turned along Z by a plate-turn record) or up and down (118, 122), except on a lift, in the jeep, tank or skateboard, or while he is busy.
Penetration recovery
After every move (in all modes but ghost) RecoverFromPenetration() checks his box, from
2/64 to 49/64 above his feet, and frees him if it overlaps something solid:
- Inside a
SolidBox(a BigDecor model caught his head while his feet stand on a floor): the nearest free position just outside the box along X or Z, at the same height, or on top of the solid floor if he is half buried. Never downward: a model never pushes him through a floor (the rare jeep-under-a-palm trap, fixed 2026-11-08). - A floor top within the step tolerance above his feet (riding a lift up beside a ledge): he is set on that floor.
- Otherwise the original's
BlupiAdjust(): up to 50 steps of 2 px down (freeing a head in a ceiling), then right, then left. If all fail the overlap is left as the original leaves it.
Placement uses the same machinery: SettleOnGround() runs the resolver with no motion, so
a mission start or a respawn stands on the ground before the first input tick, while a start in open
air still falls.
Hazard contact volumes
Deadly tiles kill on contact with the shape the player sees, not with their voxel:
| Hazard | Test | Shape |
|---|---|---|
| Lava | BlupiController::IsTouchingLava(), LavaHeight.hpp | An animated height field generated from the original Amapi lava models (LavaSurfaceData.hpp, by tools/a3d/lava_surface.py; 5 frames). Five foot points (radius 11/64) are tested against the surface of their cell's current frame. The same field is the lava's collision and its drawn pool, so jumping or flying over a lava cell is safe and falling in kills only on reaching the surface. |
| Saw | BlupiController::IsTouchingSawBlade() | Only the visible upper half of the recovered disk (radius 0.466, thickness 0.20), its flat cut at the cell's bottom. The blade's axis is X or Z according to the cell's turn records. The test sweeps from the previous to the current feet height, so jumping onto the blade kills. A saw has no implied floor; a real block under it collides on its own. |
| Fan | FanGeometry.hpp: FanBodySolid(), FanRotorTouchesBody(); WorldRuntime::TryConsumeFan() | Bounds recovered from the fan model (avatars3d/objects/fan_ventillo-h). The base, motor, shaft and hub are solid and safe to land on; only the swept annulus of the four blades (height 0.225-0.401, radius 0.19-0.50 in the model's frame) is lethal, in all six directions including turned side fans. Touching the blades consumes the fan and its wind column. |
| Spike trap, lightning, drip, crusher | Host checks in GalaxyEggbertGameUpdate.cpp | Cell-based, as in the original: the spike trap under his feet; the lightning in or under his cell on its active ticks (WorldRuntime::IsBlitzActiveAtPhase()); the drip in his cell; the crusher in his cell on its active ticks (IsCrusherActiveAtPhase()). |
Invincibility (shield, Hide, the super-Blupi cheat), vehicle immunity and "focus" (he is not caught
while taking a pickup or teleporting) gate these checks as in the original. His last safe position
(UpdateSafePosition(), GetValidX/Y/Z()) is updated only off hazard tiles,
out of deep water and away from object paths; that is where he reappears after a death.
Tests
tools/VerifyBlupiMovement.cpp(ctestVerifyBlupiMovement): first-tick placement on floors, ledges and crates; the staircase climbed by jumping from step to step (and not by walking); jump apex and ceilings; lava contact (standing in it, jumping over it); the saw disk (walking or jumping into it kills, beside it and on the block below it is safe); bars, Temp tiles, teleporters; crates; penetration recovery for foot, jeep, tank and helicopter head overlaps and a half-buried jeep.tools/VerifyInteractionSystem.cpp: fan body and rotor shapes, hazard timing functions.- The golden movement trace (
tests/golden/golden_trace.txt,--golden-capture-trace) and the autoplay playthroughs catch any change of timing (Testing).