Block Entities
Block entities allow the storage of data on blocks in cases where block states are not suitable. This is especially the case for data with a non-finite amount of options, such as inventories. Block entities are stationary and bound to a block, but otherwise share many similarities with entities, hence the name.
If you have a finite and reasonably small amount (= a few hundred at most) of possible states for your block, you might want to consider using block states instead.
Creating and Registering Block Entities
Like entities and unlike blocks, the BlockEntity class represents the block entity instance, not the registered singleton object. The singleton is expressed through the BlockEntityType<?> class instead. We will need both to create a new block entity.
Let's begin by creating our block entity class:
public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(type, pos, state);
}
}
As you may have noticed, we pass an undefined variable type to the super constructor. Let's leave that undefined variable there for a moment and instead move to registration.
Registration happens in a similar fashion to entities. We create an instance of the associated singleton class BlockEntityType<?> and register it to the block entity type registry, like so:
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, ExampleMod.MOD_ID);
public static final Supplier<BlockEntityType<MyBlockEntity>> MY_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register(
"my_block_entity",
// The block entity type.
() -> new BlockEntityType<>(
// The supplier to use for constructing the block entity instances.
MyBlockEntity::new,
// A vararg of blocks that can have this block entity.
// This assumes the existence of the referenced blocks as DeferredBlock<Block>s.
MyBlocks.MY_BLOCK_1.get(), MyBlocks.MY_BLOCK_2.get()
)
);
Remember that the DeferredRegister must be registered to the mod event bus!
Now that we have our block entity type, we can use it in place of the type variable we left earlier:
public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}
}
The reason for this rather confusing setup process is that BlockEntityType expects a BlockEntityType.BlockEntitySupplier<T extends BlockEntity>, which is basically a BiFunction<BlockPos, BlockState, T extends BlockEntity>. As such, having a constructor we can directly reference using ::new is highly beneficial. However, we also need to provide the constructed block entity type to the default and only constructor of BlockEntity, so we need to pass references around a bit.
Finally, we need to modify the block class associated with the block entity. This means that we will not be able to attach block entities to simple instances of Block, instead, we need a subclass:
// The important part is implementing the EntityBlock interface and overriding the #newBlockEntity method.
public class MyEntityBlock extends Block implements EntityBlock {
// Constructor deferring to super.
public MyEntityBlock(BlockBehaviour.Properties properties) {
super(properties);
}
// Return a new instance of our block entity here.
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new MyBlockEntity(pos, state);
}
}
And then, you of course need to use this class as the type in your block registration:
public static final DeferredBlock<MyEntityBlock> MY_BLOCK_1 =
BLOCKS.register("my_block_1", () -> new MyEntityBlock( /* ... */ ));
public static final DeferredBlock<MyEntityBlock> MY_BLOCK_2 =
BLOCKS.register("my_block_2", () -> new MyEntityBlock( /* ... */ ));
Storing Data
One of the main purposes of BlockEntitys is to store data. Data storage on block entities can happen in two ways: directly reading and writing NBT, or using data attachments. This section will cover reading and writing NBT directly; for data attachments, please refer to the linked article.
The main purpose of data attachments is, as the name suggests, attaching data to existing block entities, such as those provided by vanilla or other mods. For your own mod's block entities, saving and loading directly to and from NBT is preferred.
Data can be read from and written to a CompoundTag using the #loadAdditional and #saveAdditional methods, respectively. These methods are called when the block entity is synced to disk or over the network.
public class MyBlockEntity extends BlockEntity {
// This can be any value of any type you want, so long as you can somehow serialize it to NBT.
// We will use an int for the sake of example.
private int value;
public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}
// Read values from the passed CompoundTag here.
@Override
public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
// Will default to 0 if absent. See the NBT article for more information.
this.value = tag.getInt("value");
}
// Save values into the passed CompoundTag here.
@Override
public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("value", this.value);
}
}
In both methods, it is important that you call super, as that adds basic information such as the position. The tag names id, x, y, z, NeoForgeData and neoforge:attachments are reserved by the super methods, and as such, you should not use them yourself.
Of course, you will want to set other values and not just work with defaults. You can do so freely, like with any other field. However, if you want the game to save those changes, you must call #setChanged() afterward, which marks the block entity's chunk as dirty (= in need of being saved). If you do not call that method, the block entity might get skipped during saving, as Minecraft's saving system only saves chunks that have been marked as dirty.