🇬🇧 English
🇬🇧 English
Appearance
🇬🇧 English
🇬🇧 English
Appearance
This page is written for version:
1.21
This page is written for version:
1.21
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,
BlockEntityType.BlockEntityFactory<? extends T> entityFactory,
Block... blocks) {
Identifier id = Identifier.of(FabricDocsReference.MOD_ID, name);
return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, BlockEntityType.Builder.<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
method (and the getRenderType
method, since BlockWithEntity
makes it invisible by default)BlockEntityProvider
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);
}
@Override
protected BlockRenderType getRenderType(BlockState state) {
return BlockRenderType.MODEL;
}
@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(
new CounterBlock(AbstractBlock.Settings.create()), "counter_block", 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.
Serialization is done with the writeNbt
method:
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
nbt.putInt("clicks", clicks);
super.writeNbt(nbt, registryLookup);
}
Here, we add the fields that should be saved into the passed NbtCompound
: in the case of the counter block, that's the clicks
field.
Reading is similar, but instead of saving to the NbtCompound
you get the values you saved previously, and save them in the BlockEntity's fields:
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
clicks = nbt.getInt("clicks");
}
Now, if we save and reload the game, the counter block should continue from where it left off when saved.
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 fabric-docs-reference:counter @ BlockPos{x=-29, y=125, z=18} state Block{fabric-docs-reference:counter_block} invalid for ticking: