04 Block Entities
1 What Are Block Entities
Blocks store data using a limited set of predefined BlockStates, render models through default rendering behavior, and execute logic via update and scheduling systems.
However, this approach has limitations and cannot directly support certain block behaviors required for game interactions.
Block entities solve this problem. They give ordinary blocks three key capabilities:
- Store data using NBT
- Custom rendering behavior
- Update every tick
1.1 Block Entity Contents
Block entities vary by block type, but a basic block entity typically contains:
- type
- world (the world it exists in)
- pos (position)
- removed (whether it has been removed)
- cachedState (cached state of the corresponding block)
1.2 Block Entities and Blocks
Block entities are bound to blocks in the world. Each block position can only have one block entity instance.
Only blocks that are specifically declared can have block entities attached.
Block entities are added and removed alongside their blocks. However, through techniques like update suppression, you can retain a block entity even after its block is removed.
2 Notable Blocks with Block Entities
2.1 Hopper
In Minecraft, the hopper is a block entity that handles automated item transfer.
2.1.1 Hopper Block Entity Data
A hopper block entity stores the following information:
inventorystorage space (5 slots)transferCooldowncooldown (default is -1)lastTickTimeworld time of the last tickfacingdirection
2.1.2 Hopper Block Entity Functions
- Transfer items to target container
- Extract items from container
- Attempt to pick up item entities
2.1.3 Hopper Block Entity Workflow
Every game tick, the hopper executes these steps:
-
Decrease cooldown (cd)
- If cd is still greater than 0, the hopper skips all pull and output operations and stops processing.
- If cd reaches zero, the hopper begins attempting output and pull operations.
-
Execute item transfer logic
- First attempt output (place items into target container)
- Then attempt pull (extract items from container above)
- If either succeeds, the hopper resets the cooldown and syncs its data.
Now, let's look at the detailed logic for pulling and outputting.
When the hopper is ready to operate, it first attempts to output to the container it's pointing at. Then it tries to extract items from above. Finally, if either operation succeeds, it sets the cooldown and syncs its data.
Let's look at output and pull separately.
First, output...
The output operation uses the insert() method, which transfers items from the hopper to the target container and returns a boolean indicating success. If the transfer succeeds, the hopper enters cooldown.
Basic output logic:
-
Check if the target container is available
- If the target container is full, output fails.
- If the target container has available slots, attempt to transfer items.
-
Attempt to output items
- Iterate through the hopper's item slots, attempting to transfer items to the target container one by one.
- If any transfer succeeds, return "success"; otherwise return "failure".
When calling transfer() to move items, it returns an item stack:
- An empty return value means the items were fully transferred, so output succeeds.
- A non-empty return value means the items could not be fully transferred, so output fails.
That covers output. Now let's look at pulling.
The pull logic is similar to output, but in the opposite direction:
- During output: the hopper
transfer()s items to the target container. - During pull: a container
transfer()s items to the hopper.
Since we'll cover transfer() in detail next, we won't repeat it here.
transfer() is the core method that handles item transfer in hoppers. It takes the following parameters:
- from: item source (can be a hopper or other container)
- to: transfer target (can be a container or another hopper)
- itemStack: the item stack to transfer
- slot: target slot in the target container
Here's how it works:
-
Check the target slot
- If the slot is empty, place
itemStackdirectly into it and clearitemStack. - If the slot already has items of the same type with remaining space, merge the items and clear
itemStack.
- If the slot is empty, place
-
Determine if the transfer succeeded
- After transfer, if
itemStackis empty, the transfer succeeded; otherwise it failed.
- After transfer, if
-
Special timing for hopper-to-hopper transfers
- If the target is another hopper, the target hopper receives
8gtof cooldown. - If the target hopper
tohas already ticked before the source hopperfrom, the target hoppertoonly receives7gtof cooldown.Note that this only occurs when the target hopper is empty.
Additionally, even iffromticks aftertoand applies8gtof cooldown,towill immediately subtract 1 when it ticks next, leaving only7gtof cooldown.
- If the target is another hopper, the target hopper receives
That covers the basic hopper mechanism. This foundation will help with the chapters ahead.
2.1.4 Moving Piston
Another important block is moving_piston, commonly known as B36.
2.1.5 Moving Piston Block Entity Data
A moving piston block entity stores the following information:
pushedBlockthe block being pushedfacingdirectionextendingwhether the piston is extending (determines animation direction)sourcewhether this is the piston head retractingprogresscurrent movement progresssavedWorldTimeworld time when logic was last executed
2.1.6 Moving Piston Block Entity Functions
- Handle piston pushing entities
- Update animation progress
- If
progress>=1, replace MOVING_PISTON with the block stored inpushedBlock - Render animation on the client
2.1.7 Moving Piston Block Entity Workflow
For details, see 5.7.
3 Timing Between Block Entities
Block entity ticking is managed by two lists in LevelChunk:
pendingBlockEntityTickers: block entities added during the current tick cycle.blockEntityTickers: block entities scheduled for ticking.
Both lists are ArrayLists, so traversal order depends on insertion order. Here's the process:
tickBlockEntities()first appendspendingBlockEntityTickerstoblockEntityTickers, then clearspendingBlockEntityTickers.- When iterating through
blockEntityTickers, removed entries (isRemoved()returnstrue) are deleted; others have theirtick()method called.
New block entities are added via addBlockEntityTicker(). If ticking is in progress (tickingBlockEntities == true), they go to pendingBlockEntityTickers; otherwise to blockEntityTickers.
When unloading, block entities in the same chunk are stored in a Map<BlockPos, BlockEntity>. On reload, the map is iterated and each entity is added to blockEntityTickers via addBlockEntityTicker(), so execution order follows BlockPos.hashCode() order.
Across chunks, block entity order depends on which chunk loads first.
TL;DR:
Before unloading, block entity tick order matches placement order. After reloading, order within a chunk depends on position hash; across chunks, it depends on chunk load order.
Here's the relevant code (with some irrelevant content removed):





