Block Entity Development
Block Entities are special objects attached to blocks, used to store data and execute logic. Common block entities include chests, furnaces, hoppers, etc.
Block Entity Registration Basics
Use the REGISTRUM.blockEntity() method to register block entities:
public static final BlockEntityEntry<CustomBlockEntity> CUSTOM_BLOCK_ENTITY = REGISTRUM
.blockEntity("custom_block_entity", CustomBlockEntity::new)
.validBlock(ModBlocks.CUSTOM_BLOCK)
.register();Where:
"custom_block_entity"is the block entity IDCustomBlockEntity::newis the block entity factory methodvalidBlock()specifies which blocks this block entity can be attached to
This chapter will detail how to use REGISTRUM.blockEntity()
BlockEntityBuilder.validBlock() / validBlocks()
Specify which blocks the block entity can be attached to:
// Single block
public static final BlockEntityEntry<CustomBlockEntity> CUSTOM = REGISTRUM
.blockEntity("custom", CustomBlockEntity::new)
.validBlock(ModBlocks.CUSTOM_BLOCK)
.register();
// Multiple blocks
public static final BlockEntityEntry<PowerConverterBlockEntity> POWER_CONVERTER = REGISTRUM
.blockEntity("power_converter", PowerConverterBlockEntity::createBlockEntity)
.validBlocks(
ModBlocks.POWER_CONVERTER_SMALL,
ModBlocks.POWER_CONVERTER_MIDDLE,
ModBlocks.POWER_CONVERTER_BIG
)
.register();BlockEntityBuilder.renderer()
Register the client-side renderer for the block entity:
public static final BlockEntityEntry<FluidTankBlockEntity> FLUID_TANK = REGISTRUM
.blockEntity("fluid_tank", FluidTankBlockEntity::new)
.validBlocks(ModBlocks.FLUID_TANK)
.renderer(() -> FluidTankBlockEntityRenderer::new)
.register();BlockEntityBuilder.registerCapability()
Register capabilities for the block entity:
public static final BlockEntityEntry<CustomBlockEntity> CUSTOM = REGISTRUM
.blockEntity("custom", CustomBlockEntity::new)
.validBlock(ModBlocks.CUSTOM_BLOCK)
.registerCapability(event -> {
event.registerBlockEntity(
Capabilities.ItemHandler.BLOCK,
ModBlockEntities.CUSTOM.get(),
(blockEntity, direction) -> blockEntity.getItemHandler(direction)
);
})
.register();BlockEntityBuilder.onRegister()
Execute additional logic during registration:
public static final BlockEntityEntry<ChuteBlockEntity> CHUTE = REGISTRUM
.blockEntity("chute", ChuteBlockEntity::createBlockEntity)
.onRegister(ChuteBlockEntity::onBlockEntityRegister)
.validBlock(ModBlocks.CHUTE)
.register();Custom Block Entity Classes
Basic Block Entity
public class CustomBlockEntity extends BlockEntity {
// Stored data
private int counter = 0;
public CustomBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}
// Factory method for registration
public static CustomBlockEntity createBlockEntity(
BlockEntityType<CustomBlockEntity> type,
BlockPos pos,
BlockState state) {
return new CustomBlockEntity(type, pos, state);
}
// Save data to NBT
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("counter", counter);
}
// Load data from NBT
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
counter = tag.getInt("counter");
}
// Get the update tag for syncing to client
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = super.getUpdateTag(registries);
saveAdditional(tag, registries);
return tag;
}
// Handle update packet from server
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
}Block Entity with Tick Logic
public class TickingBlockEntity extends BlockEntity {
public TickingBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}
// Server tick
public static void serverTick(Level level, BlockPos pos, BlockState state,
TickingBlockEntity blockEntity) {
// Server-side logic executed every tick
blockEntity.doWork();
}
// Client tick (optional)
public static void clientTick(Level level, BlockPos pos, BlockState state,
TickingBlockEntity blockEntity) {
// Client-side logic executed every tick (e.g., animations)
}
private void doWork() {
// Implement your logic
setChanged(); // Mark data as changed, needs saving
}
}Register the Ticker in the block:
public class CustomBlock extends BaseEntityBlock {
public CustomBlock(Properties properties) {
super(properties);
}
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new TickingBlockEntity(ModBlockEntities.TICKING.get(), pos, state);
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(
Level level, BlockState state, BlockEntityType<T> type) {
if (level.isClientSide) {
return createTickerHelper(type, ModBlockEntities.TICKING.get(),
TickingBlockEntity::clientTick);
} else {
return createTickerHelper(type, ModBlockEntities.TICKING.get(),
TickingBlockEntity::serverTick);
}
}
}Block Entity with Item Storage
public class StorageBlockEntity extends BlockEntity {
private final ItemStackHandler inventory = new ItemStackHandler(9) {
@Override
protected void onContentsChanged(int slot) {
setChanged();
if (level != null && !level.isClientSide) {
level.sendBlockUpdated(worldPosition, getBlockState(),
getBlockState(), Block.UPDATE_ALL);
}
}
};
public StorageBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}
public IItemHandler getItemHandler(@Nullable Direction direction) {
return inventory;
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.put("inventory", inventory.serializeNBT(registries));
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
inventory.deserializeNBT(registries, tag.getCompound("inventory"));
}
}Block Entity with Fluid Storage
public class FluidStorageBlockEntity extends BlockEntity {
private final FluidTank fluidTank = new FluidTank(16000) {
@Override
protected void onContentsChanged() {
setChanged();
}
};
public FluidStorageBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}
public IFluidHandler getFluidHandler(@Nullable Direction direction) {
return fluidTank;
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.put("fluid", fluidTank.writeToNBT(registries, new CompoundTag()));
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
fluidTank.readFromNBT(registries, tag.getCompound("fluid"));
}
}Block Entity Renderer
public class CustomBlockEntityRenderer
implements BlockEntityRenderer<CustomBlockEntity> {
public CustomBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
// Initialize renderer
}
@Override
public void render(CustomBlockEntity blockEntity, float partialTick,
PoseStack poseStack, MultiBufferSource buffer,
int packedLight, int packedOverlay) {
// Custom rendering logic
poseStack.pushPose();
// Render content...
poseStack.popPose();
}
}Using with Blocks
When creating a block entity within a block, it's recommended to use BlockBuilder.simpleBlockEntity():
public static final BlockEntry<CustomBlock> CUSTOM_BLOCK = REGISTRUM
.block("custom_block", CustomBlock::new)
.initialProperties(() -> Blocks.IRON_BLOCK)
.simpleBlockEntity(CustomBlockEntity::new) // Automatically create block entity
.simpleItem()
.register();Or use the blockEntity() method for more configuration:
public static final BlockEntry<CustomBlock> CUSTOM_BLOCK = REGISTRUM
.block("custom_block", CustomBlock::new)
.initialProperties(() -> Blocks.IRON_BLOCK)
.blockEntity(CustomBlockEntity::new)
.renderer(() -> CustomBlockEntityRenderer::new)
.build()
.simpleItem()
.register();Data Synchronization
Client-Server Data Synchronization
public class SyncedBlockEntity extends BlockEntity {
private int syncedValue = 0;
// Call this method to sync to client when data changes
public void syncToClient() {
if (level != null && !level.isClientSide) {
level.sendBlockUpdated(worldPosition, getBlockState(),
getBlockState(), Block.UPDATE_CLIENTS);
}
}
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = super.getUpdateTag(registries);
tag.putInt("syncedValue", syncedValue);
return tag;
}
@Override
public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) {
super.handleUpdateTag(tag, registries);
syncedValue = tag.getInt("syncedValue");
}
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
}Complete Example
Here is a complete block entity registration example:
// ModBlockEntities.java
public class ModBlockEntities {
public static final BlockEntityEntry<ProcessorBlockEntity> PROCESSOR = REGISTRUM
.blockEntity("processor", ProcessorBlockEntity::createBlockEntity)
.validBlock(ModBlocks.PROCESSOR)
.renderer(() -> ProcessorBlockEntityRenderer::new)
.register();
public static void register() {
// Ensure the class is loaded
}
}
// ProcessorBlockEntity.java
public class ProcessorBlockEntity extends BlockEntity {
private int progress = 0;
private static final int MAX_PROGRESS = 200;
private final ItemStackHandler inputSlot = new ItemStackHandler(1);
private final ItemStackHandler outputSlot = new ItemStackHandler(1);
public ProcessorBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}
public static ProcessorBlockEntity createBlockEntity(
BlockEntityType<ProcessorBlockEntity> type,
BlockPos pos, BlockState state) {
return new ProcessorBlockEntity(type, pos, state);
}
public static void serverTick(Level level, BlockPos pos, BlockState state,
ProcessorBlockEntity blockEntity) {
if (blockEntity.canProcess()) {
blockEntity.progress++;
if (blockEntity.progress >= MAX_PROGRESS) {
blockEntity.processItem();
blockEntity.progress = 0;
}
blockEntity.setChanged();
}
}
private boolean canProcess() {
return !inputSlot.getStackInSlot(0).isEmpty();
}
private void processItem() {
// Item processing logic
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("progress", progress);
tag.put("input", inputSlot.serializeNBT(registries));
tag.put("output", outputSlot.serializeNBT(registries));
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
progress = tag.getInt("progress");
inputSlot.deserializeNBT(registries, tag.getCompound("input"));
outputSlot.deserializeNBT(registries, tag.getCompound("output"));
}
}Best Practices
Data Saving
- Always override
saveAdditional()andloadAdditional()to save/load data - Call
setChanged()after data changes to ensure data is saved
- Always override
Client Synchronization
- Only sync data that the client needs to reduce network overhead
- Use
getUpdateTag()andgetUpdatePacket()for synchronization
Performance Optimization
- Avoid time-consuming operations in tick methods
- Use delayed processing or batch processing for large amounts of data
Capability System
- Use NeoForge's Capabilities system to expose item/fluid/energy handlers
- Support different handlers for different directions
Timely Registration
- Ensure you call the
register()method in the mod main class constructor
- Ensure you call the