这篇教程会创建一个类似发射器的简单存储方块,并解释如何使用 Fabric 和原版 Minecraft 中的 ScreenHandler API 构建用户界面。
先解释一些词汇:
Screenhandler:
ScreenHandler
是负责在客户端和服务器之间同步物品栏内容的类。它也可以同步额外的数值,比如熔炉烧炼进度,这将在下一个教程中展示。我们的子类会有以下两个构造器:一个将在服务器端使用,并将储存真正的 Inventory
,另外一个将会在客户端运行,用于储存 ItemStack
并且让他们能和服务端同步。
Screen:
Screen
类仅存在于客户端,将为您的 ScreenHandler
呈现背景和其他装饰。
首先我们需要创建 Block
和对应的 BlockEntity
类
public class BoxBlock extends BlockWithEntity { protected BoxBlock(Settings settings) { super(settings); } // 从 1.20.5 开始需要有这个方法。 @Override protected MapCodec<? extends BoxBlock> getCodec() { return createCodec(BoxBlock::new); } @Override public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { return new BoxBlockEntity(pos, state); } @Override public BlockRenderType getRenderType(BlockState state) { // 从 BlockWithEntity 继承的默认值为 INVISIBLE,所以这里需要进行改变! return BlockRenderType.MODEL; } @Override public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { if (!world.isClient) { // 这里会调用 BlockWithEntity 的 createScreenHandlerFactory 方法,会将返回的方块实体强转为 // 一个 namedScreenHandlerFactory。如果你的方块没有继承 BlockWithEntity,那就需要单独实现 createScreenHandlerFactory。 NamedScreenHandlerFactory screenHandlerFactory = state.createScreenHandlerFactory(world, pos); if (screenHandlerFactory != null) { // 这个调用会让服务器请求客户端开启合适的 Screenhandler player.openHandledScreen(screenHandlerFactory); } } return ActionResult.SUCCESS; } // 这个方法能让方块破坏时物品全部掉落 @Override public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { if (state.getBlock() != newState.getBlock()) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof BoxBlockEntity) { ItemScatterer.spawn(world, pos, (BoxBlockEntity)blockEntity); // 更新比较器 world.updateComparators(pos,this); } super.onStateReplaced(state, world, pos, newState, moved); } } @Override public boolean hasComparatorOutput(BlockState state) { return true; } @Override public int getComparatorOutput(BlockState state, World world, BlockPos pos) { return ScreenHandler.calculateComparatorOutput(world.getBlockEntity(pos)); } }
我们接下来要创建 BlockEntity
,并使用物品栏教程中提到的 ImplementedInventory
接口。
public class BoxBlockEntity extends BlockEntity implements NamedScreenHandlerFactory, ImplementedInventory { private final DefaultedList<ItemStack> inventory = DefaultedList.ofSize(9, ItemStack.EMPTY); public BoxBlockEntity(BlockPos pos, BlockState state) { super(ExampleMod.BOX_BLOCK_ENTITY, pos, state); } // 从 ImplementedInventory 接口 @Override public DefaultedList<ItemStack> getItems() { return inventory; } // 这些方法来自 NamedScreenHandlerFactory 接口 // createMenu 会创建 ScreenHandler 自身 // getDisplayName 会提供名称,名称通常显示在顶部 @Override public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) { // 因为我们的类实现 Inventory,所以将*这个*提供给 ScreenHandler // 一开始只有服务器拥有物品栏,然后在 ScreenHandler 中同步给客户端 return new BoxScreenHandler(syncId, playerInventory, this); } @Override public Text getDisplayName() { return Text.translatable(getCachedState().getBlock().getTranslationKey()); // 对于 1.19 之前的版本,请使用: // return new TranslatableText(getCachedState().getBlock().getTranslationKey()); } // 以下两个方法,旧版本请移除参数 `registryLookup`。 @Override public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { super.readNbt(nbt, registryLookup); Inventories.readNbt(nbt, this.inventory, registryLookup); } @Override public NbtCompound writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { super.writeNbt(nbt, registryLookup); Inventories.writeNbt(nbt, this.inventory, registryLookup); return nbt; } }
在这个例子中,方块、方块物品。方块实体直接注册在 ExampleMod
类中,实际开发模组时可能也需要考虑放到单独的类里面(参见 blocks 和 blockentity)。
public class ExampleMod implements ModInitializer { // 对于 1.21 之前的版本,将 `Identifier.of` 替换为 `new Identifier`。 public static final Block BOX_BLOCK = Registry.register(Registries.BLOCK, Identifier.of("tutorial", "box_block"), new BoxBlock(AbstractBlock.Settings.copyOf(Blocks.CHEST))); public static final BlockItem BOX_BLOCK_ITEM = Registry.register(Registries.ITEM, Identifier.of("tutorial", "block"), new BlockItem(BOX_BLOCK, new Item.Settings())); BlockEntityType.Builder.create(BoxBlockEntity::new, BOX_BLOCK).build()); // 在 1.17 使用 FabricBlockEntityTypeBuilder 而不是 BlockEntityType.Builder // public static final BlockEntityType<BoxBlockEntity> BOX_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY_TYPE, new Identifier("tutorial", "box_block"), // FabricBlockEntityTypeBuilder.create(BoxBlockEntity::new, BOX_BLOCK).build(null));; @Override public void onInitialize() { } }
正如前面解释的,我们同时需要 ScreenHandler
和 HandledScreen
来显示并同步 GUI。ScreenHandler
类用来在服务器和客户端之间同步 GUI 状态。HandledScreen
类是完全客户端的,负责绘制 GUI 元素。
public class BoxScreenHandler extends ScreenHandler { private final Inventory inventory; // 服务器想要客户端开启 screenHandler 时,客户端调用这个构造器。 // 如有空的物品栏,客户端会调用其他构造器,screenHandler 将会自动 // 在客户端将空白物品栏同步给物品栏。 public BoxScreenHandler(int syncId, PlayerInventory playerInventory) { this(syncId, playerInventory, new SimpleInventory(9)); } // 这个构造器是在服务器的 BlockEntity 中被调用的,无需先调用其他构造器,服务器知道容器的物品栏 // 并直接将其作为参数传入。然后物品栏在客户端完成同步。 public BoxScreenHandler(int syncId, PlayerInventory playerInventory, Inventory inventory) { super(ExampleMod.BOX_SCREEN_HANDLER, syncId); checkSize(inventory, 9); this.inventory = inventory; // 玩家开启时,一些物品栏有自定义的逻辑。 inventory.onOpen(playerInventory.player); // 这会将槽位放置在 3×3 网格的正确位置中。这些槽位在客户端和服务器中都存在! // 但是这不会渲染槽位的背景,这是 Screens 类的工作 int m; int l; //Our inventory for (m = 0; m < 3; ++m) { for (l = 0; l < 3; ++l) { this.addSlot(new Slot(inventory, l + m * 3, 62 + l * 18, 17 + m * 18)); } } // 玩家物品栏 for (m = 0; m < 3; ++m) { for (l = 0; l < 9; ++l) { this.addSlot(new Slot(playerInventory, l + m * 9 + 9, 8 + l * 18, 84 + m * 18)); } } // 玩家快捷栏 for (m = 0; m < 9; ++m) { this.addSlot(new Slot(playerInventory, m, 8 + m * 18, 142)); } } @Override public boolean canUse(PlayerEntity player) { return this.inventory.canPlayerUse(player); } // Shift + 玩家物品栏槽位 @Override public ItemStack transferSlot(PlayerEntity player, int invSlot) { ItemStack newStack = ItemStack.EMPTY; Slot slot = this.slots.get(invSlot); if (slot != null && slot.hasStack()) { ItemStack originalStack = slot.getStack(); newStack = originalStack.copy(); if (invSlot < this.inventory.size()) { if (!this.insertItem(originalStack, this.inventory.size(), this.slots.size(), true)) { return ItemStack.EMPTY; } } else if (!this.insertItem(originalStack, 0, this.inventory.size(), false)) { return ItemStack.EMPTY; } if (originalStack.isEmpty()) { slot.setStack(ItemStack.EMPTY); } else { slot.markDirty(); } } return newStack; } }
public class BoxScreen extends HandledScreen<ScreenHandler> { // GUI 纹理的路径,本例中使用发射器中的纹理 private static final Identifier TEXTURE = Identifier.ofVanilla("minecraft", "textures/gui/container/dispenser.png"); public BoxScreen(ScreenHandler handler, PlayerInventory inventory, Text title) { super(handler, inventory, title); } @Override protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) { RenderSystem.setShader(GameRenderer::getPositionTexProgram); RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); RenderSystem.setShaderTexture(0, TEXTURE); int x = (width - backgroundWidth) / 2; int y = (height - backgroundHeight) / 2; context.drawTexture(TEXTURE, x, y, 0, 0, backgroundWidth, backgroundHeight); } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { renderBackground(context, mouseX, mouseY, delta); super.render(context, mouseX, mouseY, delta); drawMouseoverTooltip(context, mouseX, mouseY); } @Override protected void init() { super.init(); // 将标题居中 titleX = (backgroundWidth - textRenderer.getWidth(title)) / 2; } }
所有的屏幕都只是仅存于客户端的概念,因此只能在客户端进行注册。
public class ExampleClientMod implements ClientModInitializer { @Override public void onInitializeClient() { HandledScreens.register(ExampleMod.BOX_SCREEN_HANDLER, BoxScreen::new); } }
别忘了在 fabric.mod.json
中注册这个入口点,如果还没有完成的话:
{ /* ... */ "entrypoints": { /* ... */ "client": [ "net.fabricmc.example.ExampleModClient" ] }, /* ... */ }
ScreenHandler
同时在客户端和服务器存在,因此在两者都需要注册。
public class ExampleMod implements ModInitializer { [...] public static final ScreenHandlerType<BoxScreenHandler> BOX_SCREEN_HANDLER = Registry.register(Registries.SCREEN_HANDLER, Identifier.of("tutorial", "box_block"), new ScreenHandlerType<>(BoxScreenHandler::new, FeatureSet.empty())); @Override public void onInitialize() { [...] } }
您现在应该创建了容器方块,可以轻易地改变它以包含更小或者更大的物品栏。也许还需要应用一个纹理!qwq
使用 ScreenHandler
的示例模组:Github 上的示例模组。