User Tools

Site Tools


tutorial:blockentity

Adding a BlockEntity

Introduction

A BlockEntity is primarily used to store data within blocks. Before creating one, you will need a Block. This tutorial will cover the creation of your BlockEntity class, and its registration.

Creating a BlockEntity

The simplest Block Entity simply extends BlockEntity, and uses the default constructor. This is perfectly valid, but will not grant any special functionality to your block.

public class DemoBlockEntity extends BlockEntity {
    public DemoBlockEntity(BlockPos pos, BlockState state) {
        super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state);
    }
}

Please ensure that the constructor only takes the two parameters, otherwise the method reference DemoBlockEntity::new that we write later will be invalid. The ExampleMod.DEMO_BLOCK_ENTITY field will be created later.

Block entities support a variety of methods to enable functionality such as serialization to and deserialization from NBT, ticking, providing inventories, and more. This tutorial covers the most common implementations of block entity functionality.

Registering your BlockEntity

Once you have created the BlockEntity class, you will need to register it for it to function. The first step of this process is to create a BlockEntityType, which links your Block and BlockEntity together. Assuming your Block has been created and saved to the static final field DEMO_BLOCK, you would create the matching BlockEntityType with the line below. In this tutorial, the ID of the block entity is tutorial:demo_block_entity.

The BlockEntityType can be registered in the initialization of class or in your onInitialize method. This is to ensure it gets registered at the correct time.

    public static final BlockEntityType<DemoBlockEntity> DEMO_BLOCK_ENTITY = Registry.register(
        Registries.BLOCK_ENTITY_TYPE,
        new Identifier("tutorial", "demo_block_entity"),
        FabricBlockEntityTypeBuilder.create(DemoBlockEntity::new, DEMO_BLOCK).build()
    );

The block entity type defines that only the DEMO_BLOCK can have this block entity type. If you want the block entity type to support more blocks, just add them in the parameters of FabricBlockEntityTypeBuilder.create. If the method reference DemoBlockEntity::new does not parse, check if the constructor of DemoBlockEntity has the correct parameters.

Connecting a Block Entity to a Block

Once your BlockEntityType has been created and registered, you'll need a block that is associated with it. You can do this by implementing BlockEntityProvider and overriding createBlockEntity. Each time your block is placed, your Block Entity will spawn alongside it.

public class DemoBlock extends Block implements BlockEntityProvider {
 
    [...]
 
    @Override
    public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
        return new DemoBlockEntity(pos, state);
    }
}

Serializing Data

If you want to store any data in your BlockEntity, you will need to save and load it, or it will only be held while the BlockEntity is loaded, and the data will reset whenever you come back to it. Luckily, saving and loading is quite simple - you only need to override writeNbt() and readNbt().

writeNbt() modifies the parameter nbt, which should contain all of the data in your block entity. It usually does not modify the block entity object itself. The NBT is saved to the disk, and if you need to sync your block entity data with clients, also sent through packets. It is very important to call super.writeNbt, which saves the position and id of the block entity to the nbt. Without this, any further data you try and save will be lost as it is not associated with a position and BlockEntityType.

Knowing this, the example below demonstrates saving an integer from your BlockEntity to the nbt. In the example, the integer is saved under the key “number” - you can replace this with any string, but you can only have one entry for each key in your nbt, and you will need to remember the key in order to read the data later.

public class DemoBlockEntity extends BlockEntity {
 
    // Store the current value of the number
    private int number = 7;
 
    public DemoBlockEntity(BlockPos pos, BlockState state) {
        super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state);
    }
 
    // Serialize the BlockEntity
    @Override
    public void writeNbt(NbtCompound nbt) {
        // Save the current value of the number to the nbt
        nbt.putInt("number", number);
 
        super.writeNbt(nbt);
    }
}

In order to read the data, you will also need to override readNbt. This method is the opposite of writeNbt - instead of saving your data to a NBTCompound, you are given the nbt data which you saved earlier, enabling you to retrieve any data that you need. It modifies the block entity object itself, instead of the nbt. As with writeNbt, it is essential that you call super.readNbt, and you will need to use the same keys to retrieve data that you saved. To read, the number we saved earlier in the nbt, see the example below.

// Deserialize the BlockEntity
@Override
public void readNbt(NbtCompound nbt) {
    super.readNbt(nbt);
 
    number = nbt.getInt("number");
}

Once you have implemented the writeNbt and readNbt methods, you simply need to ensure that they are called when needed. Whenever your block entity is modified and needs to be saved, call markDirty(). This will force the writeNbt method to be called when the world is next saved by marking the chunk in which your block is as dirty. As a general rule of thumb, simply call markDirty() whenever you modify any custom variable in your BlockEntity class, otherwise after you exit and re-enter the world, the block entity appears as if the modification had not been done.

Sync data from server to client

The data is read in the server world usually. Most data are not needed by the client, for example, your client does not need to know what's in the chest or furnace, until you open the GUI. But for some block entities, such as signs and banners, you have to inform the client of the data of the block entity, for example, for rendering.

For version 1.17.1 and below, implement BlockEntityClientSerializable from the Fabric API. This class provides the fromClientTag and toClientTag methods, which work much the same as the previously discussed readNbt and writeNbt methods, except that they are used specifically for sending to and receiving data on the client. You may simply call readNbt and writeNbt in the fromClientTag and toClientTag methods.

For version 1.18 and above, override toUpdatePacket and toInitialChunkDataNbt:

  @Nullable
  @Override
  public Packet<ClientPlayPacketListener> toUpdatePacket() {
    return BlockEntityUpdateS2CPacket.create(this);
  }
 
  @Override
  public NbtCompound toInitialChunkDataNbt() {
    return createNbt();
  }

Warning: Need to call world.updateListeners(pos, state, state, Block.NOTIFY_LISTENERS); to trigger the update, otherwise the client does not know that the block entity has been changed.

Block Entity Ticking

1.17 has added static ticking, where before you'd implement the Tickable interface. For your block to tick, you would normally use getTicker in Block, linking back to a BlockEntity. See below for the common implementation of ticking.

In your Block class:

public class DemoBlock extends BlockWithEntity {
    [...]
    @Override
    public BlockRenderType getRenderType(BlockState state) {
        // With inheriting from BlockWithEntity this defaults to INVISIBLE, so we need to change that!
        return BlockRenderType.MODEL;
    }
    @Override
    public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
        return checkType(type, ExampleMod.DEMO_BLOCK_ENTITY, (world1, pos, state1, be) -> DemoBlockEntity.tick(world1, pos, state1, be));
    }
}

And in your BlockEntity class:

public class DemoBlockEntity extends BlockEntity {
    public DemoBlockEntity(BlockPos pos, BlockState state) {
        super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state);
    }
    public static void tick(World world, BlockPos pos, BlockState state, DemoBlockEntity be) {
        [...]
    }
}

Overview

You should now have your very own BlockEntity, which you can expand in various ways to suit your needs. You registered a BlockEntityType, and used it to connect your Block and BlockEntity classes together. Then, you implemented BlockEntityProvider in your Block class, and used the interface to provide an instance of your new BlockEntity. You also learned how to save data to your BlockEntity, how to retrieve for use later, and finally, you learned how to add ticking to it.

tutorial/blockentity.txt · Last modified: 2023/09/20 19:18 by haykam