🇬🇧 English
🇬🇧 English
Appearance
🇬🇧 English
🇬🇧 English
Appearance
This page is written for version:
1.21.10
Block entities are a way to store additional data for a block, that is not part of the block state: inventory contents, custom name and so on. Minecraft uses block entities for blocks like chests, furnaces, and command blocks.
As an example, we will create a block that counts how many times it has been right-clicked.
To make Minecraft recognize and load the new block entities, we need to create a block entity type. This is done by extending the BlockEntity class and registering it in a new ModBlockEntities class.
public class CounterBlockEntity extends BlockEntity {
public CounterBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.COUNTER_BLOCK_ENTITY, pos, state);
}
}Registering a BlockEntity yields a BlockEntityType like the COUNTER_BLOCK_ENTITY we've used above:
public static final BlockEntityType<CounterBlockEntity> COUNTER_BLOCK_ENTITY =
register("counter", CounterBlockEntity::new, ModBlocks.COUNTER_BLOCK);
private static <T extends BlockEntity> BlockEntityType<T> register(
String name,
FabricBlockEntityTypeBuilder.Factory<? extends T> entityFactory,
Block... blocks
) {
Identifier id = Identifier.of(ExampleMod.MOD_ID, name);
return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, FabricBlockEntityTypeBuilder.<T>create(entityFactory, blocks).build());
}TIP
Note how the constructor of the CounterBlockEntity takes two parameters, but the BlockEntity constructor takes three: the BlockEntityType, the BlockPos, and the BlockState. If we didn't hard-code the BlockEntityType, the ModBlockEntities class wouldn't compile! This is because the BlockEntityFactory, which is a functional interface, describes a function that only takes two parameters, just like our constructor.
Next, to actually use the block entity, we need a block that implements BlockEntityProvider. Let's create one and call it CounterBlock.
TIP
There's two ways to approach this:
BlockWithEntity and implement the createBlockEntity methodBlockEntityProvider by itself and override the createBlockEntity methodWe'll use the first approach in this example, since BlockWithEntity also provides some nice utilities.
public class CounterBlock extends BlockWithEntity {
public CounterBlock(Settings settings) {
super(settings);
}
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return createCodec(CounterBlock::new);
}
@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new CounterBlockEntity(pos, state);
}
}Using BlockWithEntity as the parent class means we also need to implement the createCodec method, which is rather easy.
Unlike blocks, which are singletons, a new block entity is created for every instance of the block. This is done with the createBlockEntity method, which takes the position and BlockState, and returns a BlockEntity, or null if there shouldn't be one.
Don't forget to register the block in the ModBlocks class, just like in the Creating Your First Block guide:
public static final Block COUNTER_BLOCK = register(
"counter_block",
CounterBlock::new,
AbstractBlock.Settings.create(),
true
);Now that we have a block entity, we can use it to store the number of times the block has been right-clicked. We'll do this by adding a clicks field to the CounterBlockEntity class:
private int clicks = 0;
public int getClicks() {
return clicks;
}
public void incrementClicks() {
clicks++;
markDirty();
}The markDirty method, used in incrementClicks, tells the game that this entity's data has been updated; this will be useful when we add the methods to serialize the counter and load it back from the save file.
Next, we need to increment this field every time the block is right-clicked. This is done by overriding the onUse method in the CounterBlock class:
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
if (!(world.getBlockEntity(pos) instanceof CounterBlockEntity counterBlockEntity)) {
return super.onUse(state, world, pos, player, hit);
}
counterBlockEntity.incrementClicks();
player.sendMessage(Text.literal("You've clicked the block for the " + counterBlockEntity.getClicks() + "th time."), true);
return ActionResult.SUCCESS;
}Since the BlockEntity is not passed into the method, we use world.getBlockEntity(pos), and if the BlockEntity is not valid, return from the method.

Now that we have a functional block, we should make it so that the counter doesn't reset between game restarts. This is done by serializing it into NBT when the game saves, and deserializing when it's loading.
Saving to NBT is done through ReadViews and WriteViews. These views are responsible for storing errors from encoding/decoding, and keeping track of registries throughout the serialization process.
You can read from a ReadView using the read method, passing in a Codec for the desired type. Likewise, you can write to a WriteView by using the put method, passing in a Codec for the type, and the value.
There are also methods for primitives, such as getInt, getShort, getBoolean etc. for reading and putInt, putShort, putBoolean etc. for writing. The View also provides methods for working with lists, nullable types, and nested objects.
Serialization is done with the writeData method:
@Override
protected void writeData(WriteView writeView) {
writeView.putInt("clicks", clicks);
super.writeData(writeView);
}Here, we add the fields that should be saved into the passed WriteView: in the case of the counter block, that's the clicks field.
Reading is similar, you get the values you saved previously from the ReadView, and save them in the BlockEntity's fields:
@Override
protected void readData(ReadView readView) {
super.readData(readView);
clicks = readView.getInt("clicks", 0);
}Now, if we save and reload the game, the counter block should continue from where it left off when saved.
While writeNbt and readNbt handle saving and loading to and from disk, there is still an issue:
clicks value.To fix this, we override toInitialChunkDataNbt:
@Override
public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) {
return createNbt(registryLookup);
}Now, when a player logs in or moves into a chunk where the block exists, they will see the correct counter value right away.
The BlockEntityProvider interface also defines a method called getTicker, which can be used to run code every tick for each instance of the block. We can implement that by creating a static method that will be used as the BlockEntityTicker:
The getTicker method should also check if the passed BlockEntityType is the same as the one we're using, and if it is, return the function that will be called every tick. Thankfully, there is a utility function that does the check in BlockWithEntity:
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return validateTicker(type, ModBlockEntities.COUNTER_BLOCK_ENTITY, CounterBlockEntity::tick);
}CounterBlockEntity::tick is a reference to the static method tick we should create in the CounterBlockEntity class. Structuring it like this is not required, but it's a good practice to keep the code clean and organized.
Let's say we want to make it so that the counter can only be incremented once every 10 ticks (2 times a second). We can do this by adding a ticksSinceLast field to the CounterBlockEntity class, and increasing it every tick:
public static void tick(World world, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) {
entity.ticksSinceLast++;
}Don't forget to serialize and deserialize this field!
Now we can use ticksSinceLast to check if the counter can be increased in incrementClicks:
if (ticksSinceLast < 10) return;
ticksSinceLast = 0;TIP
If the block entity does not seem to tick, try checking the registration code! It should pass the blocks that are valid for this entity into the BlockEntityType.Builder, or else it will give a warning in the console:
[13:27:55] [Server thread/WARN] (Minecraft) Block entity example-mod:counter @ BlockPos{x=-29, y=125, z=18} state Block{example-mod:counter_block} invalid for ticking: