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

ConstantValueMeaning
positionfeet, render spaceX/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.
kHeadHeight49/64 blockHis head: the top of the original's 60-pixel BlupiRect. He fits under a one-block ceiling.
kBodyHalfWidth18/64 blockHalf his width against walls, on X and Z alike, on foot and swimming. A vehicle keeps a single point.
kStepLimit4/64 blockThe only "step" he takes by walking (below).
kMoveSpeed2.5 blocks/sThe original's 8 px per 20 Hz tick, with its four-tick ease-in.
kGravity, kFallLimit25, −12.5 blocks/sFastest 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:

  1. Crates and solid boxes. Inside a crate's unit cube or a SolidBox: solid (no sub-cell mask).
  2. Air and decor. Air, and any block whose registry entry has decor set (passable scenery, including the standalone saw blade): not solid, neither floor nor ceiling.
  3. Lava is solid up to its animated surface height (LavaSurfaceHeight(), below), so he can stand in it, and dies from the contact test.
  4. 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.
  5. Quarter-cell mask. Otherwise, if sub-cell precision is asked for, the bit of the 4 × 4 collisionMask from 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

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

A one-block step stops Blupi. As in the original, he does not walk up steps; he jumps onto them. Until 2026-10-25 the controller let him climb a whole block by walking; that was removed. What remains is 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

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:

  1. 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).
  2. A floor top within the step tolerance above his feet (riding a lift up beside a ledge): he is set on that floor.
  3. 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:

HazardTestShape
LavaBlupiController::IsTouchingLava(), LavaHeight.hppAn 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.
SawBlupiController::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.
FanFanGeometry.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, crusherHost checks in GalaxyEggbertGameUpdate.cppCell-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

Related pages