Skip to content

Getting Started with GUIs

Juuz edited this page Jul 29, 2024 · 15 revisions

Prepare the Block and BlockEntity for a GUI

If you prefer to watch a video tutorial, there is one available on YouTube for Minecraft 1.15: https://www.youtube.com/watch?v=Ca769FY4pOg

Required Interfaces:

  • If your block has an inventory, InventoryProvider on the Block and/or BlockEntity (More information on how to add an inventory in the fabric docs)
  • If you want numeric fields, PropertyDelegateHolder on the Block and/or BlockEntity
  • NamedScreenHandlerFactory on the BlockEntity
    • An implementation of Block.createScreenHandlerFactory in your block class for accessing the block entity. Vanilla's BlockWithEntity implements this for you if you extend it.

Later in the process we'll implement the onUse method on the Block.

Creating the container GUI

SyncedGuiDescription is the abstract superclass for shared gui state. Subclass this and add whatever widgets you'll need to your root panel:

public class ExampleGuiDescription extends SyncedGuiDescription {
    private static final int INVENTORY_SIZE = 1;

    public ExampleGuiDescription(int syncId, PlayerInventory playerInventory, ScreenHandlerContext context) {
        super(MyMod.SCREEN_HANDLER_TYPE, syncId, playerInventory, getBlockInventory(context, INVENTORY_SIZE), getBlockPropertyDelegate(context));

        WGridPanel root = new WGridPanel();
        setRootPanel(root);
        root.setSize(300, 200);
        root.setInsets(Insets.ROOT_PANEL);

        WItemSlot itemSlot = WItemSlot.of(blockInventory, 0);
        root.add(itemSlot, 4, 1);

        root.add(this.createPlayerInventoryPanel(), 0, 3);

        root.validate(this);
    }
}

The last line in your constructor should be validating the root panel. This finds the right size for your GUI and registers all the itemslots with vanilla so that they sync and hover properly.

public class ExampleBlockScreen extends CottonInventoryScreen<ExampleGuiDescription> {
    public ExampleBlockScreen(ExampleGuiDescription gui, PlayerEntity player, Text title) {
        super(gui, player, title);
    }
}

Subclassing CottonInventoryScreen is optional but highly recommended. You could use the class as provided, but many GUI addons identify a gui by its Screen class, so creating a per-gui subclass helps sync up things like "recipes" buttons and helps other modders interact with your code.

Register the GUI

Somewhere in a "main" ModInitializer so that it runs on both client and server:

SCREEN_HANDLER_TYPE = Registry.register(Registries.SCREEN_HANDLER, ExampleBlock.ID,
    new ScreenHandlerType<>((syncId, inventory) -> new ExampleGuiDescription(syncId, inventory, ScreenHandlerContext.EMPTY),
        FeatureFlags.VANILLA_FEATURES));

Somewhere in a "client" ModInitializer so that it runs only on client startup:

HandledScreens.<ExampleGuiDescription, ExampleBlockScreen>register(MyMod.SCREEN_HANDLER_TYPE, (gui, inventory, title) -> new ExampleBlockScreen(gui, inventory.player, title));

Note that you in most cases have to have the manual generics specified for the HandledScreens.register call; the compiler has trouble with calling that method.

This is the same thing, but for the client. Again, the other side of this is coming right up. To configure the initializers, you will see an entrypoints object in the fabric.mod.json file. Make sure you have the entrypoints "main" and "client" configured to your wishes.

Implement NamedScreenHandlerFactory on your BlockEntity

Vanilla has an implementation in LootableContainerBlockEntity that most container block entities should extend. Other block entities can do it themselves like this:

	@Override
	public Text getDisplayName() {
		// Using the block name as the screen title
		return Text.translatable(getCachedState().getBlock().getTranslationKey());
	}

	@Override
	public ScreenHandler createMenu(int syncId, PlayerInventory inventory, PlayerEntity player) {
		return new ExampleGuiDescription(syncId, inventory, ScreenHandlerContext.create(world, pos));
	}

Implement the onUse method on your Block

	@Override
	public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
		// You need a Block.createScreenHandlerFactory implementation that delegates to the block entity,
		// such as the one from BlockWithEntity
		player.openHandledScreen(state.createScreenHandlerFactory(world, pos));
		return ActionResult.SUCCESS;
	}

Test your GUI

At this point, you should be up and running. Try it out!