User Tools

Site Tools


tutorial:screenhandler

This is an old revision of the document!


Creating a Container Block (DRAFT) (NEW)

In this tutorial we will create simple Storage Block similar to a dispenser. Explaining 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 integer values like furnace progress aswell, which will be showed in the next Tutorial.

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

Tutorials based on this: - Syncing Custom Data with Extended ScreenHandlers when screen is opened

- Syncing Integers continuously with PropertyDelegates

Further Reading

This and the following Tutorials are heavily inspired by the ScreenHandlerAPI ExampleMod. It can be found here: ExampleMod on Github

The documentation for the Fabric ScreenHandlerAPI can be found here: ##javadoc link here##

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. @Override
  7. public BlockEntity createBlockEntity(BlockView world) {
  8. return new BoxBlockEntity();
  9. }
  10.  
  11. @Override
  12. public BlockRenderType getRenderType(BlockState state) {
  13. //With inheriting from BlockWithEntity this defaults to INVISIBLE, so we need to change that!
  14. return BlockRenderType.MODEL;
  15. }
  16.  
  17. @Override
  18. public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
  19. if (!world.isClient) {
  20. //This will call the createScreenHandlerFactory method from blockWithEntity, which will return our blockEntity casted
  21. //to a namedScreenHandlerFactory
  22. NamedScreenHandlerFactory screenHandlerFactory = state.createScreenHandlerFactory(world, pos);
  23.  
  24. if (screenHandlerFactory != null) {
  25. //With this call the server will request the client to open the appropriate Screenhandler
  26. player.openHandledScreen(screenHandlerFactory);
  27. }
  28. }
  29. return ActionResult.SUCCESS;
  30. }
  31.  
  32.  
  33. //This method will drop all items onto the ground when the block is broken
  34. @Override
  35. public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
  36. if (state.getBlock() != newState.getBlock()) {
  37. BlockEntity blockEntity = world.getBlockEntity(pos);
  38. if (blockEntity instanceof BoxBlockEntity) {
  39. ItemScatterer.spawn(world, pos, (BoxBlockEntity)blockEntity);
  40. // update comparators
  41. world.updateComparators(pos,this);
  42. }
  43. super.onStateReplaced(state, world, pos, newState, moved);
  44. }
  45. }
  46.  
  47. @Override
  48. public boolean hasComparatorOutput(BlockState state) {
  49. return true;
  50. }
  51.  
  52. @Override
  53. public int getComparatorOutput(BlockState state, World world, BlockPos pos) {
  54. return ScreenHandler.calculateComparatorOutput(world.getBlockEntity(pos))
  55. }
  56. }

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() {
  5. super(ExampleMod.BOX_BLOCK_ENTITY);
  6. }
  7.  
  8.  
  9. //From the ImplementedInventory Interface
  10.  
  11. @Override
  12. public DefaultedList<ItemStack> getItems() {
  13. return inventory;
  14.  
  15. }
  16.  
  17. //These Methods are from the NamedScreenHandlerFactory Interface
  18. //createMenu creates the ScreenHandler itself
  19. //getDisplayName will Provide its name which is normally shown at the top
  20.  
  21. @Override
  22. public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
  23. //We provide *this* to the screenHandler as our class Implements Inventory
  24. //Only the Server has the Inventory at the start, this will be synced to the client in the ScreenHandler
  25. return new BoxScreenHandler(syncId, playerInventory, this);
  26. }
  27.  
  28. @Override
  29. public Text getDisplayName() {
  30. return new TranslatableText(getCachedState().getBlock().getTranslationKey());
  31. }
  32.  
  33. @Override
  34. public void fromTag(BlockState state, CompoundTag tag) {
  35. super.fromTag(state, tag);
  36. inventory = DefaultedList.ofSize(invsize, ItemStack.EMPTY);
  37. Inventories.fromTag(tag, this.inventory);
  38. }
  39.  
  40. @Override
  41. public CompoundTag toTag(CompoundTag tag) {
  42. super.toTag(tag);
  43. Inventories.toTag(tag, this.inventory);
  44. return tag;
  45. }
  46. }

Registering Block, BlockItem and BlockEntity

ExampleMod.java
  1. public class ExampleMod implements ModInitializer {
  2.  
  3. public static final Block BOX_BLOCK;
  4. public static final BlockItem BOX_BLOCK_ITEM;
  5. public static final BlockEntityType<BoxBlockEntity> BOX_BLOCK_ENTITY;
  6.  
  7. public static final String MOD_ID = "testmod";
  8. // a public identifier for multiple parts of our bigger chest
  9. public static final Identifier BOX = new Identifier(MOD_ID, "box_block");
  10.  
  11. static {
  12. BOX_BLOCK = Registry.register(Registry.BLOCK, BOX, new BoxBlock(FabricBlockSettings.copyOf(Blocks.CHEST)));
  13. BOX_BLOCK_ITEM = Registry.register(Registry.ITEM, BOX, new BlockItem(BOX_BLOCK, new Item.Settings().group(ItemGroup.MISC)));
  14.  
  15. //The parameter of build at the very end is always null, do not worry about it
  16. BOX_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY_TYPE, BOX, BlockEntityType.Builder.create(BoxBlockEntity::new, BOX_BLOCK).build(null));
  17. }
  18.  
  19. @Override
  20. public void onInitialize() {
  21.  
  22. }
  23. }

ScreenHandler and Screen

We need a ScreenHandler Class and a HandledScreen Class 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 transferSlot(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<ScreenHandler> {
  2. //A path to the gui texture. In this example we use the texture from the dispenser
  3. private static final Identifier TEXTURE = new Identifier("minecraft", "textures/gui/container/dispenser.png");
  4.  
  5. public BoxScreen(ScreenHandler handler, PlayerInventory inventory, Text title) {
  6. super(handler, inventory, title);
  7. }
  8.  
  9. @Override
  10. protected void drawBackground(MatrixStack matrices, float delta, int mouseX, int mouseY) {
  11. RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
  12. client.getTextureManager().bindTexture(TEXTURE);
  13. int x = (width - backgroundWidth) / 2;
  14. int y = (height - backgroundHeight) / 2;
  15. drawTexture(matrices, x, y, 0, 0, backgroundWidth, backgroundHeight);
  16. }
  17.  
  18. @Override
  19. public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
  20. renderBackground(matrices);
  21. super.render(matrices, mouseX, mouseY, delta);
  22. drawMouseoverTooltip(matrices, mouseX, mouseY);
  23. }
  24.  
  25. @Override
  26. protected void init() {
  27. super.init();
  28. // Center the title
  29. titleX = (backgroundWidth - textRenderer.getWidth(title)) / 2;
  30. }
  31. }

Registering our Screen and ScreenHandler

As Screens are are client only concept, we can only register them on the client

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

ScreenHandlers exist both on 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;
  4. [...]
  5. static {
  6. [...]
  7. //We use registerSimple here because our Entity is not an ExtendedScreenHandlerFactory
  8. //but a NamedScreenHandlerFactory.
  9. //In a later Tutorial you will see what ExtendedScreenHandlerFactory can do!
  10. BOX_SCREEN_HANDLER = ScreenHandlerRegistry.registerSimple(BOX, BoxScreenHandler::new);
  11. }
  12.  
  13. @Override
  14. public void onInitialize() {
  15.  
  16. }
  17. }

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

tutorial/screenhandler.1597425929.txt.gz · Last modified: 2020/08/14 17:25 by manymoney2