A block entity (formerly called a tile entity) attaches persistent data and optional ticking logic to a block. Furnaces, chests, and command blocks all use them. This tutorial creates a simple ticking block entity that counts elapsed seconds and persists that value across world saves and server restarts.
Access Wideners & Transformers
BlockEntityType.BlockEntitySupplier, the functional interface that BlockEntityType.Builder.of() accepts, is package-private in Minecraft's source. That means any reference to it from mod code (including lambdas and method references) triggers an access error at compile time. Each loader has its own mechanism for widening visibility before the build runs: NeoForge uses an access transformer and Fabric uses an access widener. Both files live in the common project so the same declaration covers both loaders.
Create src/main/resources/META-INF/accesstransformer.cfg:
Create src/main/resources/examplemod.accesswidener:
./gradlew --refresh-dependencies) so the widening takes effect before your next build. Your Fabric subproject's fabric.mod.json also needs "accessWidener": "examplemod.accesswidener". If your template already includes that line, no change is needed there.Block Entity Registry
Create a new class called BlockEntityRegistry in your common registry package:
The build(null) argument is the DataFixer type, which is null for all modded block entities that do not use the vanilla data-fixing system. Each valid block listed in Builder.of is one that this block entity type can be attached to.
The Block Entity Class
Create ExampleBlockEntity in a new blockentity package in your common project:
The static tick method matches the signature required by BlockEntityTicker. Calling setChanged() marks the chunk dirty so the data is included in the next world save. Calling level.sendBlockUpdated queues a sync packet to nearby clients.
The Block Class
Blocks that own a block entity must extend BaseEntityBlock (which implements the EntityBlock interface). In 1.21.1, BaseEntityBlock declares codec() as abstract, so every subclass must provide a MapCodec. You also need to override getRenderShape to restore solid model rendering, since BaseEntityBlock returns RenderShape.INVISIBLE by default:
simpleCodec helper (from Block) builds the required MapCodec from any constructor that takes only a Properties argument. If your block constructor takes additional parameters you will need to write the codec manually using RecordCodecBuilder. The getRenderShape override is also required. BaseEntityBlock marks it @Deprecated in 1.21.1 but still returns RenderShape.INVISIBLE by default, which hides the block model.Saving and Loading Data
Override saveAdditional and loadAdditional in ExampleBlockEntity to persist custom fields to NBT:
Always call super first so the base class can save and load its own fields such as the block entity type and position. Use unique NBT key names to avoid collisions if you extend this class later.
Syncing to the Client
Add getUpdateTag and getUpdatePacket so the block entity state reaches connected clients when a chunk loads or when sendBlockUpdated is called:
saveWithoutMetadata serialises all of the block entity's custom data without including the type ID or position, keeping the packet small. The client calls loadAdditional when it receives the packet, so no extra handling is needed.
Wiring Up
Register the block entity block in BlockRegistry:
Then call BlockEntityRegistry.init() from CommonClass.init() after the block registry so all blocks exist before the block entity type is built:
Datagen and Testing
Add the new block to your existing datagen providers:
- BlockStateProvider:
simpleBlockWithItem(BlockRegistry.EXAMPLE_BE_BLOCK.get(), cubeAll(...)) - BlockLootTableProvider:
dropSelf(BlockRegistry.EXAMPLE_BE_BLOCK.get()) - BlockTagsProvider: add to
MINEABLE_WITH_PICKAXE - Language file: add a
block.examplemod.example_be_blockentry
Place the block in a creative-mode world and switch to survival. Right-click the block to check that nothing crashes. Use /data get block ~ ~ ~ SecondsAlive at the block's position to confirm the counter is incrementing and persisting correctly. Break and replace the block to verify the counter resets to zero, confirming that the block entity is being recreated on placement.
Container (or extend BaseContainerBlockEntity) and pair it with a MenuType registration and a screen class. That pattern is covered in a future tutorial on custom menus.You can find the source for this tutorial here:
View Source on GitHubConfig Files (MultiLoader 1.21+)
Define a common config interface, implement it with NeoForge ModConfigSpec and a Fabric Gson JSON file, and read config values from shared code without loader-specific imports.
Continue →