User Tools

Site Tools


zh_cn:tutorial:screenhandler

创建容器方块

这篇教程会创建一个类似发射器的简单存储方块,并解释如何使用 Fabric 和原版 Minecraft 中的 ScreenHandler API 构建用户界面。

先解释一些词汇:

Screenhandler: ScreenHandler 是负责在客户端和服务器之间同步物品栏内容的类。它也可以同步额外的数值,比如熔炉烧炼进度,这将在下一个教程中展示。我们的子类会有以下两个构造器:一个将在服务器端使用,并将储存真正的 Inventory,另外一个将会在客户端运行,用于储存 ItemStack 并且让他们能和服务端同步。

Screen: Screen 类仅存在于客户端,将为您的 ScreenHandler 呈现背景和其他装饰。

方块和方块实体类

首先我们需要创建 Block 和对应的 BlockEntity

BoxBlock.java
  1. public class BoxBlock extends BlockWithEntity {
  2. protected BoxBlock(Settings settings) {
  3. super(settings);
  4. }
  5.  
  6. // 从 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. // 从 BlockWithEntity 继承的默认值为 INVISIBLE,所以这里需要进行改变!
  20. return BlockRenderType.MODEL;
  21. }
  22.  
  23. @Override
  24. public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
  25. if (!world.isClient) {
  26. // 这里会调用 BlockWithEntity 的 createScreenHandlerFactory 方法,会将返回的方块实体强转为
  27. // 一个 namedScreenHandlerFactory。如果你的方块没有继承 BlockWithEntity,那就需要单独实现 createScreenHandlerFactory。
  28. NamedScreenHandlerFactory screenHandlerFactory = state.createScreenHandlerFactory(world, pos);
  29.  
  30. if (screenHandlerFactory != null) {
  31. // 这个调用会让服务器请求客户端开启合适的 Screenhandler
  32. player.openHandledScreen(screenHandlerFactory);
  33. }
  34. }
  35. return ActionResult.SUCCESS;
  36. }
  37.  
  38.  
  39. // 这个方法能让方块破坏时物品全部掉落
  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) {
  45. ItemScatterer.spawn(world, pos, (BoxBlockEntity)blockEntity);
  46. // 更新比较器
  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. }

我们接下来要创建 BlockEntity,并使用物品栏教程中提到的 ImplementedInventory 接口。

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. // 从 ImplementedInventory 接口
  10.  
  11. @Override
  12. public DefaultedList<ItemStack> getItems() {
  13. return inventory;
  14.  
  15. }
  16.  
  17. // 这些方法来自 NamedScreenHandlerFactory 接口
  18. // createMenu 会创建 ScreenHandler 自身
  19. // getDisplayName 会提供名称,名称通常显示在顶部
  20.  
  21. @Override
  22. public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
  23. // 因为我们的类实现 Inventory,所以将*这个*提供给 ScreenHandler
  24. // 一开始只有服务器拥有物品栏,然后在 ScreenHandler 中同步给客户端
  25. return new BoxScreenHandler(syncId, playerInventory, this);
  26. }
  27.  
  28. @Override
  29. public Text getDisplayName() {
  30. return Text.translatable(getCachedState().getBlock().getTranslationKey());
  31. // 对于 1.19 之前的版本,请使用:
  32. // return new TranslatableText(getCachedState().getBlock().getTranslationKey());
  33. }
  34.  
  35. // 以下两个方法,旧版本请移除参数 `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. }

注册方块、物品和方块实体

在这个例子中,方块、方块物品。方块实体直接注册在 ExampleMod 类中,实际开发模组时可能也需要考虑放到单独的类里面(参见 blocksblockentity)。

ExampleMod.java
  1. public class ExampleMod implements ModInitializer {
  2.  
  3. // 对于 1.21 之前的版本,将 `Identifier.of` 替换为 `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. // 在 1.17 使用 FabricBlockEntityTypeBuilder 而不是 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 和 Screen

正如前面解释的,我们同时需要 ScreenHandlerHandledScreen 来显示并同步 GUI。ScreenHandler 类用来在服务器和客户端之间同步 GUI 状态。HandledScreen 类是完全客户端的,负责绘制 GUI 元素。

BoxScreenHandler.java
  1. public class BoxScreenHandler extends ScreenHandler {
  2. private final Inventory inventory;
  3.  
  4. // 服务器想要客户端开启 screenHandler 时,客户端调用这个构造器。
  5. // 如有空的物品栏,客户端会调用其他构造器,screenHandler 将会自动
  6. // 在客户端将空白物品栏同步给物品栏。
  7. public BoxScreenHandler(int syncId, PlayerInventory playerInventory) {
  8. this(syncId, playerInventory, new SimpleInventory(9));
  9. }
  10.  
  11. // 这个构造器是在服务器的 BlockEntity 中被调用的,无需先调用其他构造器,服务器知道容器的物品栏
  12. // 并直接将其作为参数传入。然后物品栏在客户端完成同步。
  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. // 玩家开启时,一些物品栏有自定义的逻辑。
  18. inventory.onOpen(playerInventory.player);
  19.  
  20. // 这会将槽位放置在 3×3 网格的正确位置中。这些槽位在客户端和服务器中都存在!
  21. // 但是这不会渲染槽位的背景,这是 Screens 类的工作
  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. // 玩家物品栏
  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. // 玩家快捷栏
  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 + 玩家物品栏槽位
  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. // GUI 纹理的路径,本例中使用发射器中的纹理
  3. private static final Identifier TEXTURE = Identifier.ofVanilla("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(DrawContext context, float delta, int mouseX, int mouseY) {
  11. RenderSystem.setShader(GameRenderer::getPositionTexProgram);
  12. RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
  13. RenderSystem.setShaderTexture(0, TEXTURE);
  14. int x = (width - backgroundWidth) / 2;
  15. int y = (height - backgroundHeight) / 2;
  16. context.drawTexture(TEXTURE, x, y, 0, 0, backgroundWidth, backgroundHeight);
  17. }
  18.  
  19. @Override
  20. public void render(DrawContext context, int mouseX, int mouseY, float delta) {
  21. renderBackground(context, mouseX, mouseY, delta);
  22. super.render(context, mouseX, mouseY, delta);
  23. drawMouseoverTooltip(context, mouseX, mouseY);
  24. }
  25.  
  26. @Override
  27. protected void init() {
  28. super.init();
  29. // 将标题居中
  30. titleX = (backgroundWidth - textRenderer.getWidth(title)) / 2;
  31. }
  32. }

注册 Screen 和 ScreenHandler

所有的屏幕都只是仅存于客户端的概念,因此只能在客户端进行注册。

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. }

别忘了在 fabric.mod.json 中注册这个入口点,如果还没有完成的话:

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

ScreenHandler 同时在客户端和服务器存在,因此在两者都需要注册。

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. }

结果

您现在应该创建了容器方块,可以轻易地改变它以包含更小或者更大的物品栏。也许还需要应用一个纹理!qwq

延伸阅读

zh_cn/tutorial/screenhandler.txt · Last modified: 2024/08/27 04:50 by solidblock