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
| class | what it decides | thread |
|---|---|---|
Entity | the geometry: clipping, stepping up, bouncing, whether you are on the ground and what you are standing on | the 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 |
LivingEntity | the physics above it: gravity, drag, friction, swimming, gliding, climbing | same |
CollisionGetter | which blocks are candidates, through BlockCollisions, and which one is holding you up | same |
Shapes / VoxelShape | the clipping arithmetic, one axis at a time | same |
Entity.Movement | one recorded segment — from, to, and, for a segment a move recorded, the pre-collision vector that fixes the replay’s axis order | same |
InsideBlockEffectApplier.StepBasedCollector | when an ignition, a freeze or an extinguish happens, and in what order | same |
EntityFluidInteraction | the snapshot of water and lava height, whether the eyes are in and the current, that the tick reads | same |
ServerEntity | whether this tick’s new position costs a short delta or an absolute sync | the 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:
| property | default | who changes it |
|---|---|---|
Block.getFriction | 0.6 | 0.98 on ice, packed ice and Blocks.FROSTED_ICE, 0.989 on blue ice, 0.8 on Blocks.SLIME_BLOCK |
Block.getSpeedFactor | 1.0 | 0.4 on soul sand and honey |
Block.getJumpFactor | 1.0 | 0.5 on honey |
Block.getBounceRestitution | 0.0 | 1.0 on Blocks.SLIME_BLOCK, 0.75 on beds and Blocks.SHELF_MUSHROOM |
Block.getFallDistanceReduction | 0.0 | 0.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.