Skip to content

Block Entities

With Block States you have already created pre-defined and finite data for your blocks.

For example, a redstone torch can be lit or unlit, an oak log can face six different directions and slabs can be waterlogged.

But how can you add data which may have near-endless possibilities, like the text on a sign block or the inventory which is implemented in a chest?

This is where block entities come in.

A BlockEntity is used to store custom data for blocks. Unlike normal blocks, they are bound to a world and have their own instantiated entity for each of their block positions. Data about the block entity is stored as NBT, using readNbt and writeNbt to ensure persistence when the world saves.

Block entities are also used for custom block rendering and can handle ticks on the server side instead of the Block's client-sided random ticks.

Custom BlockEntity class

Create a new TestBlockEntity class which extends from BlockEntity, and implement the constructor.

The constructor of the BlockEntity class comes with three parameters by default: type, pos and state. Make sure to remove the type parameter otherwise the registration of the BlockEntityType later on (with the class constructor reference TestBlockEntity::new) won't work.

java
public class TestBlockEntity extends BlockEntity {
    // make sure the constructor's parameters are correct!
    public TestBlockEntity(BlockPos pos, BlockState state) {
        // We will create this BlockEntityType later on
        super(MyMod.TEST_BLOCK_ENTITY_TYPE, pos, state);
    }
}

Preparing the custom Block class

There are two ways of adding a BlockEntity to a custom Block.

  1. The custom Block class needs to implement the BlockEntityProvider interface.
  2. The custom Block class needs to extend from BlockWithEntity.

Both ways require to @Override the createBlockEntity method. This is the only place where the custom BlockEntity class is instantiated.

java
public class TestBlock extends BlockWithEntity {    
    // Instantiate your custom BlockEntity class
    @Nullable
    @Override
    public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
        return new TestBlockEntity(pos, state);
    }

    // When extending from BlockWithEntity, it would be invisible by default.
    // Make sure to pass in the correct BlockRenderType.
    @Override
    public BlockRenderType getRenderType(BlockState state) {
        return BlockRenderType.MODEL;
    }

    // ...
}

Remember to register your block.

Registering the BlockEntity with a BlockEntityType

Several Blocks can share the same functionality. For example all versions of the bed block behave in the same way. It is possible to register multiple Blocks for a BlockEntity using a single BlockEntityType. Examples of this can be seen in the BlockEntityType class but the Fabric API registers them slightly differently.

Create the registry entry for the BlockEntityType in the MyMod and make a static reference for the BlockEntityType which has the custom TestBlockEntity class as a type parameter. This is used in the TestBlockEntity's constructor.

java
public class MyMod implements ModInitializer {
    // This BlockEntityType is referenced in the TestBlockEntity's constructor
    public static BlockEntityType<TestBlockEntity> TEST_BLOCK_ENTITY_TYPE;

    @Override
    public void onInitialize() {
    // ...
    TEST_BLOCK_ENTITY_TYPE = Registry.register(
                Registries.BLOCK_ENTITY_TYPE, new Identifier("mod_id", "test_block_entity"),
                FabricBlockEntityTypeBuilder.create(TestBlockEntity::new, ModBlocks.TEST_BLOCK)
                        // add as many .addBlock(...) here, as needed to support
                        // additional blocks for the same custom BlockEntity
                        .build()
        );
    }
}

This is considered quite messy, and can get really large when creating multiple block entity types.

Cleaning up the mess

To avoid making the initialize method messy, create a new ModBlockEntities class and add a new method that will register all custom BlockEntityTypes.

java
public class ModBlockEntities {
    public static BlockEntityType<TestBlockEntity> TEST_BLOCK_ENTITY_TYPE;

    public static void registerBlockEntityTypes() {        
        TEST_BLOCK_ENTITY_TYPE = Registry.register(
                Registries.BLOCK_ENTITY_TYPE, new Identifier(MyMod.MOD_ID, "test_block_entity"),
                FabricBlockEntityTypeBuilder.create(TestBlockEntity::new, ModBlocks.TEST_BLOCK).build()
        );        
        // add more BlockEntityTypes here, if needed
    }
}

This way, the ModInitializer implementing entry-point class needs to only have one line to register all custom BlockEntities.

java
public class MyMod implements ModInitializer {
    
    @Override
    public void onInitialize() {
        ModBlockEntities.registerBlockEntityTypes();
        // ...
    }
}

The reference in the TestBlockEntity class' constructor needs to be changed as well.

java
public class TestBlockEntity extends BlockEntity {
    public TestBlockEntity(BlockPos pos, BlockState state) {
        super(ModBlockEntities.TEST_BLOCK_ENTITY_TYPE, pos, state);
    }
    // ...
}

How to use the custom BlockEntity

In this example, the custom Block class will be used to implement the functionality of the Block and it's BlockEntity will be used to keep track of how many times it is landed upon.

You can access the BlockEntity's object with world.getBlockEntity() in your custom Block class.

java
public class TestBlock extends BlockWithEntity {
    // ...
    
    // Executed when entities fall on the custom Block
    @Override
    public void onLandedUpon(World world, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
        // Check for the custom BlockEntity
        if (world.getBlockEntity(pos) instanceof TestBlockEntity testBlockEntity && !world.isClient()) {
            // Using the Accessor of the custom BlockEntity
            int count = testBlockEntity.getInteractionCount();
            // Using the Mutator of the custom BlockEntity
            testBlockEntity.setInteractionCount(count + 1);            
            
            // Value output for the player in the chat
            if (entity instanceof PlayerEntity) {
                entity.sendMessage(Text.literal("You've stepped on this block " + testBlockEntity.getInteractionCount() + " times!"));
            }
        }

        // ...

        super.onLandedUpon(world, state, pos, entity, fallDistance);
    }    
}

The custom BlockEntity will get additional methods and variables to interact with, and save the incoming data from the custom Block class.

java
public class TestBlockEntity extends BlockEntity {
    // Creating a field for the object's custom data
    private int interactionCount = 0;

    // A static String variable to avoid typos when refering to the NBT data
    private static final String INTERACT_NBT_KEY = "test_block.interaction";

    public TestBlockEntity(BlockPos pos, BlockState state) {
        super(ModBlockEntities.TEST_BLOCK_ENTITY_TYPE, pos, state);
    }

    // "Accessor" (getter) method
    // To access this value from outside of this class
    public int getInteractionCount() {
        return this.interactionCount;
    }
    // "Mutator" (setter) method
    // To change this value from outside of this class
    public void setInteractionCount(int newCount) {
        this.interactionCount = newCount;        
        // When changing values for the BlockEntity, make sure to mark it as dirty.
        // Otherwise, the new values won't be saved properly in the NBT data
        this.markDirty();
    }

    // Store data in the NBT of the BlockEntity
    @Override
    protected void writeNbt(NbtCompound nbt) {
        nbt.putInt(INTERACT_NBT_KEY, this.interactionCount);
        super.writeNbt(nbt);
    }
    // Read data from the NBT of the BlockEntity
    @Override
    public void readNbt(NbtCompound nbt) {
        super.readNbt(nbt);
        this.interactionCount = nbt.getInt(INTERACT_NBT_KEY);
    }
}

Now you have a custom Block which will interact with the player and save its data in the NBT using the custom BlockEntity and its BlockEntityType for registration.

Implementing BlockEntity ticking

Create a new method, that will be called for every tick inside of the custom BlockEntity class, and implement your tick behavior. In this case, every 200 ticks (~ 10 sec) the SoundEvents.BLOCK_ANVIL_USE sound will be played.

In addition, create a new field to keep track of the ticks for the BlockEntity instance.

java
public class TestBlockEntity extends BlockEntity {
    // The new field for the ticking.
    // If the value is important to keep, consider saving this field into the NBT values too.
    private int tickCounter = 0;

    // ...

    public TestBlockEntity(BlockPos pos, BlockState state) {
        super(ModBlockEntities.TEST_BLOCK_ENTITY_TYPE, pos, state);
    }

    // ...

    public static void tick(World world, BlockPos pos, BlockState blockState, BlockEntity blockEntity) {
        // Check if the blockEntity from the parameters is actualy this custom BlockEntity
        // You can also just work with a cast instead - we will check for the correct BlockEntity when calling this method anyways
        if (blockEntity instanceof TestBlockEntity testBlockEntity) {
            // Play the sound every 200 ticks with the use of the modulo operator
            if (testBlockEntity.tickCounter % 200 == 0) {
                world.playSound(null, pos, SoundEvents.BLOCK_ANVIL_USE, SoundCategory.BLOCKS, 1f, 1f);
            }
            // Increment the tick attribute of the instance
            testBlockEntity.tickCounter++;
        }
    }
}

Now @Override the getTicker method in the custom Block class to call the BlockEntity's tick method.

java
public class TestBlock extends BlockWithEntity {
    // ...

    @Nullable
    @Override
    public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
        // Calls the custom BlockEntity's tick method if the getTicker method parameter's BlockEntityType and the specified BlockEntityType match up
        return checkType(type, ModBlockEntities.TEST_BLOCK_ENTITY_TYPE, TestBlockEntity::tick);
                
        // To pass over different parameters, use a Lambda Expression instead of the method reference (TestBlockEntity::tick)
        // and change the parameters in the BlockEntity's definition of this method to your liking
    }
}

Keep in mind that ticking BlockEntities in large quantities can cause massive client lag (FPS) and/or server lag (TPS). This is one of the reasons why players place composters on top of their hoppers in large redstone contraptions.

Not affiliated with Mojang Studios or the Fabric Project.