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.2 · 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 is two blocks above stone with nothing pushing it sideways. 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. 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 effects 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 onserver main, or client main for whoever is authoritative
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 the pre-collision vector that fixes the replay’s axis ordersame
InsideBlockEffectApplier.StepBasedCollectorwhen a block effect actually happens, and in what ordersame
EntityFluidInteractionthe once-per-tick snapshot of water and lava height, eye depth and currentsame
ServerEntitywhether this tick’s new position costs a short delta or an absolute syncserver main, in the broadcast phase

Who is allowed to run this at all

A tracked mob is simulated on the server and merely carried on the client — nothing in its own tick reaches Entity.move, though a piston or a shulker box can still shove it there from a block entity — while a player is the other way up, client-authoritative on both sides, so the client simulates for real and the server re-runs it as a check. The predicate that decides is Entity.isLocalInstanceAuthoritative rather than a bare “am I the client”, and Entity.canSimulateMovement and Entity.isEffectiveAi default to it — though several of the gates below are written as not a client or authoritative, and Player overrides both predicates to exactly that. Which call site reads which, and why a mob and a player invert, is authority; this page notes each gate where the trace hits it.

The tick

sequenceDiagram
    participant SL as ServerLevel
    participant LE as LivingEntity
    participant Entity as Entity
    participant CG as CollisionGetter
    participant Shapes as Shapes
    participant Block as Block
    participant SE as ServerEntity

    SL->>Entity: tickNonPassenger — setOldPosAndRot, bump tickCount, tick
    Entity->>Entity: baseTick — updateFluidInteraction snapshots water and lava, lava halves fallDistance
    LE->>LE: aiStep — coast-or-interpolate, deadzone, applyInput, serverAiStep
    LE->>LE: travel picks travelInAir (not in fluid, not gliding)
    LE->>Entity: moveRelative at 0.02 of flying speed, then move(SELF, deltaMovement)
    Entity->>CG: collide — getEntityCollisions, the world border, getBlockCollisions
    CG->>Shapes: per candidate getCollisionShape, a full cube short-circuits, else joinIsNotEmpty
    Entity->>Shapes: collideWithShapes — axisStepOrder, Y first, then the larger horizontal
    Shapes-->>Entity: the allowed vector, y clipped by the stone floor
    Entity->>Entity: Movement recorded, setPos, then the collision booleans
    Entity->>CG: setOnGroundWithMovement — findSupportingBlock names the block
    Entity->>Block: checkFallDamage — fallOn, then GameEvent.HIT_GROUND
    Entity->>Entity: restitution, step sound, block speed factor
    LE->>LE: back in travelInAir — subtract 0.08 of gravity, then the drags
    LE->>Entity: applyEffectsFromBlocks — replay the deque, flush the collector
    LE->>LE: pushEntities — cramming check, then doPush
    Note over SL,SE: the next tick, in the chunkSource phase, before the entity loop runs again
    SL->>SE: sendChanges — a short Pos delta, or an absolute sync because onGround changed

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: interpolate-or-coast, 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.

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 four block properties (blocks and states):

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

MoverType names who is moving you, in five constants. MoverType.PISTON is the one with real machinery — 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 (input to movement).

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["collect the colliders: every entity box, the world border if you are near it, then BlockCollisions over the swept box"]
    RESOLVE["Entity.collideWithShapes"]
    AXIS["Direction.axisStepOrder — Y first, always, then the larger horizontal axis, then the smaller. Each axis clips the box already displaced by the earlier ones"]
    TEST{"step height above zero, colliding horizontally, and on or hitting the ground?"}
    FLAT["return the flat result"]
    HEIGHTS["Entity.collectCandidateStepUpHeights — every Y face of every candidate shape inside maxUpStep, sorted ascending"]
    RETRY["retry the whole resolve at the next candidate height"]
    MORE{"any more horizontal distance than the flat attempt?"}
    WIN["return that one, minus the drop back to the old floor"]

    COLLIDE --> GATHER --> RESOLVE --> AXIS --> TEST
    TEST -- no --> FLAT
    TEST -- yes --> HEIGHTS --> RETRY --> MORE
    MORE -- "no, try the next candidate" --> RETRY
    MORE -- "no candidates left" --> FLAT
    MORE -- yes --> WIN

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.

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. 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 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: 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 therefore a comparison and not a raycast — it is set from Entity.verticalCollisionBelow, meaning the vertical component was clipped and it was negative. The only geometric probe 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.

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, 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, and the tag 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 lives inside this same step: LivingEntity.handleOnClimbable clamps the fall speed on a BlockTags.CLIMBABLE block, and a separate clamp in LivingEntity.handleRelativeFrictionAndCalculateMovement 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 runs 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 has one exception, and it is a useful one: LivingEntity.checkFallDamage re-runs Entity.updateFluidInteraction from inside Entity.move whenever the entity is not already in water (and ItemEntity.tick re-runs 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 segment is replayed in the same axis order the collision usedDirection.axisStepOrder again, over the segment’s stored pre-collision vector — 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.

Nothing found is applied inline. Each effect 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.

Off it goes

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 one tick in four, the damage 6) and then calls LivingEntity.doPushEntity.push, a horizontal-only impulse scaled by 0.05 and ignored below a hundredth of a block.

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 ticks since the last teleport, not riding, the entity does not demand precision, and Entity.onGround still matches what the last absolute sync recorded. That last condition is the common case, and it is a real cost: every landing and every step off a ledge forces a full ClientboundEntityPositionSyncPacket. Entity.syncPosition forces the next send outright, and ClientboundSetEntityMotionPacket carries the delta separately. On the receiving side, ClientPacketListener.handleEntityPositionSync moves only an entity that is not locally authoritative, and snaps rather than interpolates past 64 blocks of correction — otherwise it feeds InterpolationHandler, three steps by default, which is the coast branch of LivingEntity.aiStep doing its real job.

Where to look

LivingEntity.aiStep · LivingEntity.travel · LivingEntity.travelInAir · LivingEntity.handleRelativeFrictionAndCalculateMovement · LivingEntity.handleOnClimbable · Entity.move · Entity.collide · Entity.collideBoundingBox · Entity.collideWithShapes · Entity.collectCandidateStepUpHeights · CollisionGetter · BlockCollisions · Shapes.collide · Entity.setPos · Entity.setOnGroundWithMovement · Entity.checkSupportingBlock · Entity.checkFallDamage · Entity.restituteMovementAfterCollisions · Entity.applyMovementEmissionAndPlaySound · Entity.updateFluidInteraction · EntityFluidInteraction · Entity.applyEffectsFromBlocks · Entity.checkInsideBlocks · InsideBlockEffectApplier.StepBasedCollector · InsideBlockEffectType · LivingEntity.pushEntities · MoverType · InterpolationHandler · ServerEntity.sendChanges


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