Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Movement and collision

Verified against Minecraft 26.3 · Part VI · One tick of a falling zombie: 0.08 of gravity, one swept box against a stone floor, and the four booleans everything downstream reads.

A zombie has been falling for a few ticks and this is the one where it reaches the stone. Its tick builds one delta vector, hands it to Entity.move, gets back the part of it the world allowed, and sets four booleans from the difference — Entity.horizontalCollision, Entity.verticalCollision, Entity.verticalCollisionBelow and Entity.minorHorizontalCollision. Then it has to answer a harder question: what did I just walk through? It does not answer that by sampling the destination. Every segment of the tick’s movement was recorded into a deque, Entity.movementThisTick, and Entity.applyEffectsFromBlocks replays those segments afterwards — in the same axis order the collision used, visiting every block the swept box actually crossed, AABB.collidedAlongVector rather than a static overlap. And the ignition, freezing and extinguishing that replay finds are not applied where they are found: they are queued into an InsideBlockEffectApplier.StepBasedCollector and flushed in InsideBlockEffectType declaration order, so fire and water touched in the same step of the replay always end in the extinguish, whatever order the blocks came in.

The cast

classwhat it decidesthread
Entitythe geometry: clipping, stepping up, bouncing, whether you are on the ground and what you are standing onthe Server thread, the Render thread for whatever is authoritative there, and both for an item, an orb, a falling block or lit TNT, or anything a piston or a shulker pushes
LivingEntitythe physics above it: gravity, drag, friction, swimming, gliding, climbingsame
CollisionGetterwhich blocks are candidates, through BlockCollisions, and which one is holding you upsame
Shapes / VoxelShapethe clipping arithmetic, one axis at a timesame
Entity.Movementone recorded segment — from, to, and, for a segment a move recorded, the pre-collision vector that fixes the replay’s axis ordersame
InsideBlockEffectApplier.StepBasedCollectorwhen an ignition, a freeze or an extinguish happens, and in what ordersame
EntityFluidInteractionthe snapshot of water and lava height, whether the eyes are in and the current, that the tick readssame
ServerEntitywhether this tick’s new position costs a short delta or an absolute syncthe Server thread, in the broadcast phase

Who is allowed to run this at all

The trace below is the authoritative copy’s tick, and which copy that is inverts between a mob and a player: nothing in a client-side mob’s own tick moves it through Entity.move (a dying ender dragon aside), while your own player is simulated on your machine for real. The predicate that decides is Entity.isLocalInstanceAuthoritative rather than a bare “am I the client”, and it is stated in full once, at authority; this page notes each gate where the trace hits it.

The tick

One tick of a falling zombie, from the entity loop to the packet that carries the result. The mob is one object and one lane; the class on each message is which half of it runs, because the page’s two roles — LivingEntity’s physics and Entity’s geometry — alternate all the way down:

sequenceDiagram
    participant SL as ServerLevel
    participant LE as LivingEntity
    participant SE as ServerEntity

    SL->>LE: tick, from ServerLevel's entity loop
    LE->>LE: Entity.baseTick, and the fluid snapshot
    LE->>LE: LivingEntity.aiStep: coast if nothing simulates, then serverAiStep
    LE->>LE: LivingEntity.travel, which picks travelInAir
    LE->>LE: Entity.moveRelative, then Entity.move
    Note over LE: the resolve, and the step-up loop, are the next section
    LE->>LE: Entity.setPos, then the four collision booleans
    LE->>LE: Entity.setOnGround<br/>WithMovement
    LE->>LE: Entity.checkFallDamage
    LE->>LE: the bounce, the step sound, the block speed factor
    LE->>LE: back in travelInAir: 0.08 of gravity, then the drags
    LE->>LE: Entity.applyEffects<br/>FromBlocks
    LE->>LE: LivingEntity.push<br/>Entities, then doPush
    rect rgba(0, 0, 0, 0.04)
    Note over SL,SE: the next tick, the ServerLevel.chunkSource phase
    SL->>SE: sendChanges: a short Pos delta, or an absolute sync
    end

The line to look for is the fourth from the bottom. Gravity is subtracted after the move, not before it, so the delta a tick moves the mob by was built by the tick before — and the shaded band at the foot is the next tick again, which is when the wire hears about any of it.

Building the delta

Entity.baseTick clears the cached block state, records whether the eyes were in water, and calls Entity.updateFluidInteraction — one sweep that fills the water and lava trackers of EntityFluidInteraction with their heights and accumulated current. Everything downstream — Entity.isInWater, Entity.isInLava, Entity.getFluidHeight, Entity.isEyeInFluid — reads that snapshot and never the live world. Fire ticks after it, lava halves Entity.fallDistance rather than clearing it, and Entity.checkBelowWorld discards anything 64 below the world floor — except a LivingEntity, which overrides the hook and takes four points of fell out of the world damage a tick instead.

LivingEntity.aiStep is the order of every mob’s tick and worth memorising: the coast when nothing interpolates or simulates, head turn, equipment, a deadzone that zeroes any delta component under 0.003 (a squared-horizontal test instead, for players), LivingEntity.applyInput, Mob.serverAiStep — the goal selector and the movement control, which set LivingEntity.xxa and LivingEntity.zza (AI, pathfinding) — the jump branch, gliding, the travel branch, Entity.applyEffectsFromBlocks, animation, freezing, LivingEntity.pushEntities. Our zombie’s jump branch is skipped before Entity.onGround is ever consulted, because the branch is gated on LivingEntity.jumping and a falling zombie is not asking to jump.

The travel branch is a fork, not a call. If the controlling passenger is a Player and the mob is alive it is LivingEntity.travelRidden — the path every horse, pig and happy ghast takes, and the reason a ridden mob’s input comes from LivingEntity.getRiddenInput rather than its own AI. Otherwise, and only if both Entity.canSimulateMovement and Entity.isEffectiveAi hold, it is LivingEntity.travel, which picks one of three: LivingEntity.travelInFluid (splitting again into LivingEntity.travelInWater and LivingEntity.travelInLava), LivingEntity.travelFallFlying — the elytra model, lift from the square of the pitch cosine — or LivingEntity.travelInAir. LivingEntity.shouldTravelInFluid picks the first, and note what it reads: the cached in-water and in-lava flags, with the live FluidState at the block position used only for LivingEntity.canStandOnFluid, which is how a strider walks on lava.

The knobs on the entity and the knobs on the block

LivingEntity.travelInAir probes the block below through Entity.getBlockPosBelowThatAffectsMyMovement — 0.500001 down — for its friction: airborne, 1.0, and on stone Block.getFriction’s 0.6 through LivingEntity.computeModifiedFriction. For an airborne entity LivingEntity.getFrictionInfluencedSpeed then returns LivingEntity.getFlyingSpeed: 0.02 for a mob nobody is riding, which is why you have almost no air control — a literal, not Attributes.FLYING_SPEED, which is not even in the base living attribute set. Entity.moveRelative rotates the input by the yaw and adds it to Entity.deltaMovement through Entity.setDeltaMovement, which silently discards the whole vector if it is not finite, so NaN never enters the physics state.

Every knob on the entity’s side is a syncable attribute (attributes): Attributes.GRAVITY (0.08), Attributes.STEP_HEIGHT (0.6), Attributes.MOVEMENT_SPEED (0.7), Attributes.JUMP_STRENGTH (0.42), Attributes.SAFE_FALL_DISTANCE (3.0), Attributes.FALL_DAMAGE_MULTIPLIER, Attributes.MOVEMENT_EFFICIENCY, Attributes.WATER_MOVEMENT_EFFICIENCY, Attributes.AIR_DRAG_MODIFIER, Attributes.FRICTION_MODIFIER, Attributes.BOUNCINESS. The world’s half is five values each block copies out of its BlockBehaviour.Properties (blocks and states) and returns from a getter:

propertydefaultwho changes it
Block.getFriction0.60.98 on ice, packed ice and Blocks.FROSTED_ICE, 0.989 on blue ice, 0.8 on Blocks.SLIME_BLOCK
Block.getSpeedFactor1.00.4 on soul sand and honey
Block.getJumpFactor1.00.5 on honey
Block.getBounceRestitution0.01.0 on Blocks.SLIME_BLOCK, 0.75 on beds and Blocks.SHELF_MUSHROOM
Block.getFallDistanceReduction0.00.5 on beds and Blocks.SHELF_MUSHROOM

Who else can move you

MoverType names who is moving you, in five constants. MoverType.PISTON is the one with collision rules of its own — Entity.limitPistonMovement collapses the vector to a single axis, Entity.applyPistonMovementRestriction clamps it to ±0.51 per game tick, and that path alone is exempt from the multiply by Entity.stuckSpeedMultiplier — it still clears the field (pistons). MoverType.SHULKER_BOX makes a Shulker teleport rather than move, and MoverType.SELF and MoverType.PLAYER are read together by Player.maybeBackOffFromEdge, which is what stops a crouching player walking off a ledge.

Resolving one axis at a time

Entity.move opens with two things that are easy to miss. Entity.stuckSpeedMultiplier is applied to the delta and cleared in the same breath, zeroing Entity.deltaMovement with it — that pair is the whole cobweb, berry-bush and powder-snow model — and Entity.noPhysics is an escape hatch above even it: an entity with it set skips collision entirely and has all four booleans cleared.

flowchart TD
    COLLIDE["Entity.collide"]
    GATHER["gather the colliders over the swept box"]
    RESOLVE["Entity.collideWithShapes"]
    AXIS["Direction.axisStepOrder: Y, then the larger horizontal, then the smaller"]
    TEST{"step height above zero, colliding horizontally, on the ground?"}
    FLAT["return the flat result"]
    HEIGHTS["Entity.collectCandidateStepUpHeights, ascending, less the one already tried"]
    NEXT{"a candidate height left?"}
    RETRY["retry the whole resolve at that height"]
    MORE{"more horizontal distance than the flat attempt?"}
    WIN["return that one, with the tick's fall added back"]

    COLLIDE --> GATHER --> RESOLVE --> AXIS --> TEST
    TEST -- "no" --> FLAT
    TEST -- "yes" --> HEIGHTS --> NEXT
    NEXT -- "no" --> FLAT
    NEXT -- "yes" --> RETRY --> MORE
    MORE -- "no" --> NEXT
    MORE -- "yes" --> WIN

Three diamonds: the first decides whether a step-up is attempted at all, the second walks the candidate heights in ascending order, and the third is asked once per height. The loop leaves with the flat result when the heights run out, and with the stepped one at the first height that gains ground.

Two things in the gathering stage surprise people. The first is that collision is against shapes, not blocks: a candidate contributes whatever BlockBehaviour.BlockStateBase.getCollisionShape says, which for a fence is 1.5 blocks tall — to walk into and to stand on. The 1.0 you see outlined is BlockBehaviour.BlockStateBase.getShape, the selection box, and CrossCollisionBlock builds the two from different heights. The mover only ever asks for the first. The second is that most entities are not colliders at all: EntityGetter.getEntityCollisions wraps with Shapes.create the box of every entity that answers Entity.canBeCollidedWith, and the base class answers false — so the mob standing next to you contributes nothing, while boats, living shulkers and happy ghasts do. (Pushing is a different predicate, Entity.isPushable, and belongs to the crowding pass below.) BlockCollisions walks the box with a Cursor3D, reads chunks through CollisionGetter.getChunkForCollisions — a full chunk if it is already there, null otherwise, and a missing chunk is simply stepped past, so an entity at the edge of loaded space falls through empty space rather than blocking the tick. A full cube short-circuits to a box intersection; anything else goes through Shapes.joinIsNotEmpty.

Entity.collideBoundingBox is the flat attempt itself — the public static that gathers the colliders and hands them to Entity.collideWithShapes, with no step-up in it at all — and Entity.collide calls it once before deciding whether stepping up is worth trying. The step-up loop in the figure is the part worth slowing down for. It does not guess a height and it does not pick the best one. It harvests the Y coordinates of the candidate shapes that lie above the entity’s feet and within Entity.maxUpStep, skipping the height the flat attempt already tried, sorts them ascending, and retries the whole resolve at each until one yields any more horizontal distance than the flat attempt — and returns that one with this tick’s fall added back in, because the box it stepped from was the one already lowered by this tick’s vertical movement. It is the lowest step that helps, which is also why an entity can step onto a shape’s internal ledge and not only its top face. Entity.maxUpStep is zero on the base class and LivingEntity.maxUpStep reads Attributes.STEP_HEIGHT, raised to at least 1.0 when a Player is riding — which is how a ridden horse climbs a full block.

What the move reports back

Before committing, one clip: if Entity.fallDistance is non-zero and the allowed movement is at least a block long, Entity.move casts a ray up to eight blocks along it for BlockTags.FALL_DAMAGE_RESETTING or water and resets the fall distance on any hit. Then an Entity.Movement record — from, to, and the pre-collision delta — goes onto Entity.movementThisTick, and Entity.setPos moves the point and the bounding box together.

The four booleans are then computed by comparing what was asked with what was allowed. Entity.horizontalCollision is true when either horizontal component was clipped; Entity.verticalCollision when the Y component was, and Entity.verticalCollisionBelow when that clip was downward. The fourth, Entity.minorHorizontalCollision, is the odd one: it asks Entity.isHorizontalCollisionMinor, which is false on the base class and overridden by exactly one class in the game, LocalPlayer, which is also its only reader — a scrape too small to be a wall is the difference between stopping a sprint and not. The comparison is Mth.equal on the two horizontals but exact inequality on Y, and the whole vertical block only runs if the entity moved vertically at all or is authoritative. Entity.onGround is not a fifth boolean of the same kind: it is set from Entity.verticalCollisionBelow, so it too is a comparison and not a raycast. The probe for what holds you up is CollisionGetter.findSupportingBlock, reached through Entity.setOnGroundWithMovement and Entity.checkSupportingBlock, and it answers which block is holding you (for sounds and the speed factor), not whether — probing a paper-thin slab under the box, retrying with the box shifted back along the movement if that finds nothing, and setting Entity.onGroundNoBlocks when it still does.

Landing, and the fall distance that resets from many places

Entity.checkFallDamage runs next, only when this instance is authoritative. It adds the downward movement to Entity.fallDistance and, on landing, calls Block.fallOn, posts GameEvent.HIT_GROUND and resets the distance. Block.fallOn is what calls LivingEntity.causeFallDamage, with the distance scaled down by the block’s Block.getFallDistanceReduction; LivingEntity.calculateFallPower subtracts Attributes.SAFE_FALL_DISTANCE and LivingEntity.calculateFallDamage multiplies by Attributes.FALL_DAMAGE_MULTIPLIER and checks EntityTypeTags.FALL_DAMAGE_IMMUNE (damage). Our two-block fall is 2 − 3 < 0, so the power is zero and nothing at all happens: the landing particles are gated on a positive power and the fall sound on positive damage, so only the game event and the reset fire. That reset is reached from more places than you would guess — landing, entering water in Entity.updateFluidInteraction, climbing in LivingEntity.handleOnClimbable, every LivingEntity.rideTick, under MobEffects.SLOW_FALLING or MobEffects.LEVITATION at the top of the travel branch, Entity.makeStuckInBlock, a bubble column, a honey block’s slide, a mace’s smash, and the tag-or-water clip above. Lava halves it instead.

Then Entity.restituteMovementAfterCollisions, gated on Entity.canSimulateMovement: a real restitution model, not slime-block code. It reflects the horizontal components, and for a downward hit combines Attributes.BOUNCINESS with Block.getBounceRestitution, damped to 80% for non-living entities, gated on the impact being at least one tick of gravity — which is why nothing jitters at rest on a slime block — and opted out of by BlockTags.SUPPRESSES_BOUNCE or by crouching. A bounce posts GameEvent.BOUNCE (game events) and sets Entity.syncPosition. Entity.applyMovementEmissionAndPlaySound follows, gated on not client-side or authoritative: it accumulates Entity.moveDist and fires Entity.playStepSound plus GameEvent.STEP when it passes Entity.nextStep. Last, the horizontal components are multiplied by Entity.getBlockSpeedFactor — soul sand’s 0.4, lerped towards 1 by Attributes.MOVEMENT_EFFICIENCY.

And then gravity

Control returns to LivingEntity.travelInAir, after the move, and only now is gravity subtracted: Entity.getEffectiveGravity, 0.08, or capped at 0.01 while falling with MobEffects.SLOW_FALLING. MobEffects.LEVITATION replaces that step entirely rather than modifying it, and a client-side entity standing over an unloaded chunk gets a hard-coded −0.1. The horizontals are then multiplied by block friction times a 0.91 scaled by Attributes.AIR_DRAG_MODIFIER, the vertical by a 0.98 scaled by the same — Attributes.FRICTION_MODIFIER touches only the block-friction term, and block friction is 1.0 unless Entity.onGround. The whole drag step is skipped when LivingEntity.shouldDiscardFriction is set. Climbing straddles the move: LivingEntity.handleOnClimbable clamps the fall speed on a climbable block before it, and a separate clamp in LivingEntity.handleRelativeFrictionAndCalculateMovement after it sets the vertical component to 0.2 when a climbing or powder-snow entity is either colliding horizontally or jumping — which is the whole of “you go up a ladder by pressing into it”.

So the delta Entity.move consumes carries the previous tick’s gravity. That is one of two conventions in the codebase, and the other is Entity.applyGravity, which most of its callers run before the move. An ItemEntity does it the other way, and the contrast is the clearest way to see both. ItemEntity.tick applies gravity (a default of 0.04) before Entity.move and drag after it, reverses any downward velocity on landing at half strength — items bounce, they do not merely damp — and skips the move entirely when it is resting still on the ground and the tick count says it is not this item’s turn, calling Entity.applyEffectsFromBlocksForLastMovements on the previous tick’s segments instead. Its Entity.getMovementEmission is Entity.MovementEmission.NONE, so it makes no step sounds. Neither convention is wrong.

The fluid snapshot is not taken only once a tick, and usefully so: LivingEntity.checkFallDamage re-runs Entity.updateFluidInteraction from inside Entity.move whenever the entity is not already in water (and ItemEntity.tick, PrimedTnt.tick and AbstractMinecart.tick re-run it too), which is exactly why falling into water cancels the fall damage in the same tick that entered it.

What did I pass through

Entity.applyEffectsFromBlocks runs on the same gate as the step sound — not client-side, or authoritative. It drains Entity.movementThisTick into Entity.finalMovementsThisTick first — substituting a single old-position-to- position segment when the deque is empty, and appending a final segment when the entity ended somewhere the last recorded one did not — and only then runs the replay, which opens by calling Block.stepOn for the block underfoot, gated on Entity.onGround.

Each recorded segment is replayed in the same axis order the collision used — Direction.axisStepOrder again, over the segment’s stored pre-collision vector, while a substituted, appended or merged one is swept straight — and Entity.checkInsideBlocks walks each leg with BlockGetter.forEachBlockIntersectedBetween, testing each block with AABB.collidedAlongVector (through Entity.collidedWithShapeMovingFrom) rather than a static overlap at the destination, and calling BlockBehaviour.BlockStateBase.entityInside, Entity.onInsideBlock and FluidState.entityInside on what it finds. Entity.visitedBlocks is the deduplicator: a block is visited at most once across the whole replay, however many segments cross it. Two budgets bound the work — sixteen sweep steps per segment, which is not sixteen blocks, because every block the box covers at one end of the sweep shares a single step index; and Entity.movementThisTick merges its two oldest entries once it reaches a hundred, buying bounded memory with a little precision. A segment that exhausts its steps gets one last zero-length visit at the destination, which covers every block the box ends up inside.

Ignition, freezing and extinguishing are not applied inline, though most of what a block does is (a cobweb’s slowdown, a cactus’s or a campfire’s damage). Each of those is queued into the InsideBlockEffectApplier.StepBasedCollector, which flushes a step’s worth at a time in InsideBlockEffectType declaration order — InsideBlockEffectType.FREEZE, InsideBlockEffectType.CLEAR_FREEZE, InsideBlockEffectType.FIRE_IGNITE, InsideBlockEffectType.LAVA_IGNITE, InsideBlockEffectType.EXTINGUISH — so fire and water touched in the same step always end in the extinguish. The reordering is strictly per step: InsideBlockEffectApplier.StepBasedCollector.advanceStep flushes as the replay advances and InsideBlockEffectApplier.StepBasedCollector.applyAndClear runs the accumulated list at the end, so across steps the order stays chronological and fire in a later step than water still burns you.

The crowding pass closes the tick

LivingEntity.pushEntities closes the tick. It collects pushable neighbours through Level.getPushableEntities — a different predicate from the collision one, and on the client ClientLevel.getPushableEntities returns at most the local player, never the crowd. On a server it applies GameRules.MAX_ENTITY_CRAMMING (default 24, checked on a one-in-four roll each tick, the damage 6) and then calls LivingEntity.doPush → Entity.push, a horizontal-only impulse scaled by 0.05 and ignored below a hundredth of a block.

And the tick after: what this one costs on the wire

Nothing has crossed the network yet. ServerEntity.sendChanges runs in the chunk-source phase of ServerLevel.tick, which comes before the entity loop (the level tick) — so this tick’s movement is broadcast at the start of the next one. It becomes a short delta, ClientboundMoveEntityPacket.Pos, only when it can: not too big for a short, no more than 400 gated evaluations since the last absolute sync — ServerEntity.teleportDelay is incremented inside the gate, so for an entity tracked every tick on the default interval that is about 1,200 ticks, fewer when its data or a push opens the gate, and more when the tracker is not called at all — not riding, the entity does not demand precision, and Entity.onGround still matches what the last absolute sync recorded. The last condition is the one that fails most often, and it is a real cost: a landing or a step off a ledge that a gated call sees forces a full ClientboundEntityPositionSyncPacket. The bounce’s Entity.syncPosition flag re-phases the tracker’s own counter to the next interval boundary for an entity with no stepped interpolation, so its send happens at the next evaluation rather than up to an interval late; for a living entity like this zombie (every one but a shulker), its SteppedInterpolationTracker takes the flag first and records the bounce as a step instead. ClientboundSetEntityMotionPacket carries the delta separately (what the client is told owns the choice between the two shapes). On the receiving side, ClientPacketListener.handleEntityPositionSync moves only an entity that is not locally authoritative (authority), and snaps rather than interpolates past 64 blocks of correction — otherwise it feeds the entity’s InterpolationHandler, for a living entity other than a shulker a stepped one whose step count is its update interval, three by default, which Entity.commonTick steps just before the entity’s tick; the coast branch LivingEntity.aiStep opens with is what runs when nothing is interpolating.

Where to look

Two methods hold almost all of this and are worth reading end to end in this order: LivingEntity.aiStep, which is every mob’s tick in one list, and Entity.move, which is the geometry. Under the second, Entity.collide and Entity.collideWithShapes are the axis-at-a-time resolve, with the step-up loop in Entity.collide and Entity.collectCandidateStepUpHeights harvesting its heights; BlockCollisions is what supplies them with candidates, and CollisionGetter.findSupportingBlock is the probe for what holds you up. For the replay, read Entity.applyEffectsFromBlocks and then Entity.checkInsideBlocks, with InsideBlockEffectType open beside them for the order the collector flushes in. LivingEntity.travelInAir is where gravity and drag are spent, after the move rather than before it. Two doors: ItemEntity.tick, the other convention run end to end in one method, and InterpolationHandler, which is what moves an entity when nothing on that side simulates it.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.