Skip to content

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:

java
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 ID
  • CustomBlockEntity::new is the block entity factory method
  • validBlock() 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:

java
// 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:

java
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:

java
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:

java
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

java
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

java
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:

java
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

java
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

java
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

java
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():

java
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:

java
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

java
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:

java
// 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

  1. Data Saving

    • Always override saveAdditional() and loadAdditional() to save/load data
    • Call setChanged() after data changes to ensure data is saved
  2. Client Synchronization

    • Only sync data that the client needs to reduce network overhead
    • Use getUpdateTag() and getUpdatePacket() for synchronization
  3. Performance Optimization

    • Avoid time-consuming operations in tick methods
    • Use delayed processing or batch processing for large amounts of data
  4. Capability System

    • Use NeoForge's Capabilities system to expose item/fluid/energy handlers
    • Support different handlers for different directions
  5. Timely Registration

    • Ensure you call the register() method in the mod main class constructor

Released under the CC-BY-NC-SA 4.0 License.