Table of Contents
创建容器方块
这篇教程会创建一个类似发射器的简单存储方块,并解释如何使用 Fabric 和原版 Minecraft 中的 ScreenHandler API 构建用户界面。
先解释一些词汇:
Screenhandler:
ScreenHandler
是负责在客户端和服务器之间同步物品栏内容的类。它也可以同步额外的数值,比如熔炉烧炼进度,这将在下一个教程中展示。我们的子类会有以下两个构造器:一个将在服务器端使用,并将储存真正的 Inventory
,另外一个将会在客户端运行,用于储存 ItemStack
并且让他们能和服务端同步。
Screen:
Screen
类仅存在于客户端,将为您的 ScreenHandler
呈现背景和其他装饰。
方块和方块实体类
首先我们需要创建 Block
和对应的 BlockEntity
类
- BoxBlock.java
- 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
接口。
- BoxBlockEntity.java
- 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)。
- ExampleMod.java
- 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 和 Screen
正如前面解释的,我们同时需要 ScreenHandler
和 HandledScreen
来显示并同步 GUI。ScreenHandler
类用来在服务器和客户端之间同步 GUI 状态。HandledScreen
类是完全客户端的,负责绘制 GUI 元素。
- BoxScreenHandler.java
- 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;
- }
- }
- BoxScreen.java
- 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;
- }
- }
注册 Screen 和 ScreenHandler
所有的屏幕都只是仅存于客户端的概念,因此只能在客户端进行注册。
- ExampleModClient.java
- public class ExampleClientMod implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- HandledScreens.register(ExampleMod.BOX_SCREEN_HANDLER, BoxScreen::new);
- }
- }
别忘了在 fabric.mod.json
中注册这个入口点,如果还没有完成的话:
- src/main/resources/fabric.mod.json
{ /* ... */ "entrypoints": { /* ... */ "client": [ "net.fabricmc.example.ExampleModClient" ] }, /* ... */ }
ScreenHandler
同时在客户端和服务器存在,因此在两者都需要注册。
- ExampleMod.java
- 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 上的示例模组。