Table of Contents

Creating a Container Block

In this tutorial we will create simple storage block similar to a dispenser, explaining how to build a user interface with the ScreenHandler API from Fabric and Vanilla Minecraft along the way.

Let us first explain some vocabulary:

Screenhandler: A ScreenHandler is a class responsible for synchronizing inventory contents between the client and the server. It can sync additional integer values like furnace progress as well, which will be showed in the next tutorial.

Our subclass will have two constructors here: one will be used on the server side and will contain the real Inventory and another one will be used on the client side to hold the ItemStacks and synchronize them.

Screen: The Screen class only exists on the client and will render the background and other decorations for your ScreenHandler.

Block and BlockEntity classes

First we need to create the Block and its BlockEntity.

BoxBlock.java
  1. public class BoxBlock extends BlockWithEntity {
  2. protected BoxBlock(Settings settings) {
  3. super(settings);
  4. }
  5.  
  6. // This method is required since 1.20.5.
  7. @Override
  8. protected MapCodec<? extends BoxBlock> getCodec() {
  9. return createCodec(BoxBlock::new);
  10. }
  11.  
  12. @Override
  13. public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
  14. return new BoxBlockEntity(pos, state);
  15. }
  16.  
  17. @Override
  18. public BlockRenderType getRenderType(BlockState state) {
  19. // With inheriting from BlockWithEntity this defaults to INVISIBLE, so we need to change that!
  20. return BlockRenderType.MODEL;
  21. }
  22.  
  23. @Override
  24. public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
  25. if (!world.isClient) {
  26. // This will call the createScreenHandlerFactory method from BlockWithEntity, which will return our blockEntity casted to
  27. // a namedScreenHandlerFactory. If your block class does not extend BlockWithEntity, it needs to implement createScreenHandlerFactory.
  28. NamedScreenHandlerFactory screenHandlerFactory = state.createScreenHandlerFactory(world, pos);
  29.  
  30. if (screenHandlerFactory != null) {
  31. // With this call the server will request the client to open the appropriate Screenhandler
  32. player.openHandledScreen(screenHandlerFactory);
  33. }
  34. }
  35. return ActionResult.SUCCESS;
  36. }
  37.  
  38.  
  39. // This method will drop all items onto the ground when the block is broken
  40. @Override
  41. public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
  42. if (state.getBlock() != newState.getBlock()) {
  43. BlockEntity blockEntity = world.getBlockEntity(pos);
  44. if (blockEntity instanceof BoxBlockEntity boxBlockEntity) {
  45. ItemScatterer.spawn(world, pos, boxblockEntity);
  46. // update comparators
  47. world.updateComparators(pos,this);
  48. }
  49. super.onStateReplaced(state, world, pos, newState, moved);
  50. }
  51. }
  52.  
  53. @Override
  54. public boolean hasComparatorOutput(BlockState state) {
  55. return true;
  56. }
  57.  
  58. @Override
  59. public int getComparatorOutput(BlockState state, World world, BlockPos pos) {
  60. return ScreenHandler.calculateComparatorOutput(world.getBlockEntity(pos));
  61. }
  62. }

Now we will create our BlockEntity, it will use the ImplementedInventory interface from the Inventory Tutorial.

BoxBlockEntity.java
  1. public class BoxBlockEntity extends BlockEntity implements NamedScreenHandlerFactory, ImplementedInventory {
  2. private final DefaultedList<ItemStack> inventory = DefaultedList.ofSize(9, ItemStack.EMPTY);
  3.  
  4. public BoxBlockEntity(BlockPos pos, BlockState state) {
  5. super(ExampleMod.BOX_BLOCK_ENTITY, pos, state);
  6. }
  7.  
  8.  
  9. // From the ImplementedInventory Interface
  10.  
  11. @Override
  12. public DefaultedList<ItemStack> getItems() {
  13. return inventory;
  14. }
  15.  
  16. // These Methods are from the NamedScreenHandlerFactory Interface
  17. // createMenu creates the ScreenHandler itself
  18. // `getDisplayName` will Provide its name which is normally shown at the top
  19.  
  20. @Override
  21. public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
  22. // We provide *this* to the screenHandler as our class Implements Inventory
  23. // Only the Server has the Inventory at the start, this will be synced to the client in the ScreenHandler
  24. return new BoxScreenHandler(syncId, playerInventory, this);
  25. }
  26.  
  27. @Override
  28. public Text getDisplayName() {
  29. // for 1.19+
  30. return Text.translatable(getCachedState().getBlock().getTranslationKey());
  31. // for earlier versions
  32. // return new TranslatableText(getCachedState().getBlock().getTranslationKey());
  33. }
  34.  
  35. // For the following two methods, for earlier versions, remove the parameter `registryLookup`.
  36. @Override
  37. public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
  38. super.readNbt(nbt, registryLookup);
  39. Inventories.readNbt(nbt, this.inventory, registryLookup);
  40. }
  41.  
  42. @Override
  43. public NbtCompound writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
  44. super.writeNbt(nbt, registryLookup);
  45. Inventories.writeNbt(nbt, this.inventory, registryLookup);
  46. return nbt;
  47. }
  48. }

Registering Block, BlockItem and BlockEntity

In this example, the block, block item and block entity are directly registered in ExampleMod class. You may also need to consider placing them into separate classes (see blocks and blockentity) when actually developing mods.

ExampleMod.java
  1. public class ExampleMod implements ModInitializer {
  2.  
  3. // For versions before 1.21, replace `Identifier.of` with `new Identifier`.
  4. public static final Block BOX_BLOCK = Registry.register(Registries.BLOCK, Identifier.of("tutorial", "box_block"),
  5. new BoxBlock(AbstractBlock.Settings.copyOf(Blocks.CHEST)));
  6.  
  7. public static final BlockItem BOX_BLOCK_ITEM = Registry.register(Registries.ITEM, Identifier.of("tutorial", "block"),
  8. new BlockItem(BOX_BLOCK, new Item.Settings()));
  9.  
  10. public static final BlockEntityType<BoxBlockEntity> BOX_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY_TYPE, Identifier.of("tutorial", "box_block"),
  11. BlockEntityType.Builder.create(BoxBlockEntity::new, BOX_BLOCK).build());
  12. // In 1.17 use FabricBlockEntityTypeBuilder instead of BlockEntityType.Builder
  13. // public static final BlockEntityType<BoxBlockEntity> BOX_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY_TYPE, new Identifier("tutorial", "box_block"),
  14. // FabricBlockEntityTypeBuilder.create(BoxBlockEntity::new, BOX_BLOCK).build(null));;
  15.  
  16. @Override
  17. public void onInitialize() {
  18. }
  19. }

ScreenHandler and Screen

As explained earlier, we need both a ScreenHandler and a HandledScreen to display and sync the GUI. ScreenHandler classes are used to synchronize GUI state between the server and the client. HandledScreen classes are fully client-sided and are responsible for drawing GUI elements.

BoxScreenHandler.java
  1. public class BoxScreenHandler extends ScreenHandler {
  2. private final Inventory inventory;
  3.  
  4. // This constructor gets called on the client when the server wants it to open the screenHandler,
  5. // The client will call the other constructor with an empty Inventory and the screenHandler will automatically
  6. // sync this empty inventory with the inventory on the server.
  7. public BoxScreenHandler(int syncId, PlayerInventory playerInventory) {
  8. this(syncId, playerInventory, new SimpleInventory(9));
  9. }
  10.  
  11. // This constructor gets called from the BlockEntity on the server without calling the other constructor first, the server knows the inventory of the container
  12. // and can therefore directly provide it as an argument. This inventory will then be synced to the client.
  13. public BoxScreenHandler(int syncId, PlayerInventory playerInventory, Inventory inventory) {
  14. super(ExampleMod.BOX_SCREEN_HANDLER, syncId);
  15. checkSize(inventory, 9);
  16. this.inventory = inventory;
  17. // some inventories do custom logic when a player opens it.
  18. inventory.onOpen(playerInventory.player);
  19.  
  20. // This will place the slot in the correct locations for a 3x3 Grid. The slots exist on both server and client!
  21. // This will not render the background of the slots however, this is the Screens job
  22. int m;
  23. int l;
  24. // Our inventory
  25. for (m = 0; m < 3; ++m) {
  26. for (l = 0; l < 3; ++l) {
  27. this.addSlot(new Slot(inventory, l + m * 3, 62 + l * 18, 17 + m * 18));
  28. }
  29. }
  30. // The player inventory
  31. for (m = 0; m < 3; ++m) {
  32. for (l = 0; l < 9; ++l) {
  33. this.addSlot(new Slot(playerInventory, l + m * 9 + 9, 8 + l * 18, 84 + m * 18));
  34. }
  35. }
  36. // The player Hotbar
  37. for (m = 0; m < 9; ++m) {
  38. this.addSlot(new Slot(playerInventory, m, 8 + m * 18, 142));
  39. }
  40.  
  41. }
  42.  
  43. @Override
  44. public boolean canUse(PlayerEntity player) {
  45. return this.inventory.canPlayerUse(player);
  46. }
  47.  
  48. // Shift + Player Inv Slot
  49. @Override
  50. public ItemStack quickMove(PlayerEntity player, int invSlot) {
  51. ItemStack newStack = ItemStack.EMPTY;
  52. Slot slot = this.slots.get(invSlot);
  53. if (slot != null && slot.hasStack()) {
  54. ItemStack originalStack = slot.getStack();
  55. newStack = originalStack.copy();
  56. if (invSlot < this.inventory.size()) {
  57. if (!this.insertItem(originalStack, this.inventory.size(), this.slots.size(), true)) {
  58. return ItemStack.EMPTY;
  59. }
  60. } else if (!this.insertItem(originalStack, 0, this.inventory.size(), false)) {
  61. return ItemStack.EMPTY;
  62. }
  63.  
  64. if (originalStack.isEmpty()) {
  65. slot.setStack(ItemStack.EMPTY);
  66. } else {
  67. slot.markDirty();
  68. }
  69. }
  70.  
  71. return newStack;
  72. }
  73. }
BoxScreen.java
  1. public class BoxScreen extends HandledScreen<BoxScreenHandler> {
  2. // A path to the gui texture. In this example we use the texture from the dispenser
  3.  
  4. private static final Identifier TEXTURE = Identifier.ofVanilla("textures/gui/container/dispenser.png");
  5. // For versions before 1.21:
  6. // private static final Identifier TEXTURE = new Identifier("minecraft", "textures/gui/container/dispenser.png");
  7.  
  8. public BoxScreen(BoxScreenHandler handler, PlayerInventory inventory, Text title) {
  9. super(handler, inventory, title);
  10. }
  11.  
  12. @Override
  13. protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
  14. RenderSystem.setShader(GameRenderer::getPositionTexProgram);
  15. RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
  16. RenderSystem.setShaderTexture(0, TEXTURE);
  17. int x = (width - backgroundWidth) / 2;
  18. int y = (height - backgroundHeight) / 2;
  19. context.drawTexture(TEXTURE, x, y, 0, 0, backgroundWidth, backgroundHeight);
  20. }
  21.  
  22. @Override
  23. public void render(DrawContext context, int mouseX, int mouseY, float delta) {
  24. renderBackground(context, mouseX, mouseY, delta);
  25. super.render(context, mouseX, mouseY, delta);
  26. drawMouseoverTooltip(context, mouseX, mouseY);
  27. }
  28.  
  29. @Override
  30. protected void init() {
  31. super.init();
  32. // Center the title
  33. titleX = (backgroundWidth - textRenderer.getWidth(title)) / 2;
  34. }
  35. }

Registering our Screen and ScreenHandler

As screens are a client-only concept, we can only register them on the client.

ExampleModClient.java
  1. @Environment(EnvType.CLIENT)
  2. public class ExampleClientMod implements ClientModInitializer {
  3. @Override
  4. public void onInitializeClient() {
  5. HandledScreens.register(ExampleMod.BOX_SCREEN_HANDLER, BoxScreen::new);
  6. }
  7. }

Don't forget to register this entrypoint in fabric.mod.json if you haven't done it yet:

src/main/resources/fabric.mod.json
{
   /* ... */
  "entrypoints": {
    /* ... */
    "client": [
      "net.fabricmc.example.ExampleModClient"
    ]
  },
  /* ... */
}

ScreenHandlers exist both on the client and on the server and therefore have to be registered on both.

ExampleMod.java
  1. public class ExampleMod implements ModInitializer {
  2. [...]
  3. public static final ScreenHandlerType<BoxScreenHandler> BOX_SCREEN_HANDLER = Registry.register(Registries.SCREEN_HANDLER, Identifier.of("tutorial", "box_block"), new ScreenHandlerType<>(BoxScreenHandler::new, FeatureSet.empty()));
  4.  
  5. @Override
  6. public void onInitialize() {
  7. [...]
  8. }
  9. }

Result

You have now created your own container Block, you could easily change it to contain a smaller or bigger Inventory. Maybe even apply a texture :-P

Further Reading

An example mod using the ScreenHandler API: ExampleMod on Github.