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. @Override
  7. public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
  8. return new BoxBlockEntity(pos, state);
  9. }
  10.  
  11. @Override
  12. public BlockRenderType getRenderType(BlockState state) {
  13. // 从 BlockWithEntity 继承的默认值为 INVISIBLE,所以这里需要进行改变!
  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. // 这里会调用 BlockWithEntity 的 createScreenHandlerFactory 方法,会将返回的方块实体强转为
  21. // 一个 namedScreenHandlerFactory。如果你的方块没有继承 BlockWithEntity,那就需要单独实现 createScreenHandlerFactory。
  22. NamedScreenHandlerFactory screenHandlerFactory = state.createScreenHandlerFactory(world, pos);
  23.  
  24. if (screenHandlerFactory != null) {
  25. // 这个调用会让服务器请求客户端开启合适的 Screenhandler
  26. player.openHandledScreen(screenHandlerFactory);
  27. }
  28. }
  29. return ActionResult.SUCCESS;
  30. }
  31.  
  32.  
  33. // 这个方法能让方块破坏时物品全部掉落
  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. // 更新比较器
  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. }

我们接下来要创建 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. @Override
  36. public void readNbt(NbtCompound nbt) {
  37. super.readNbt(nbt);
  38. Inventories.readNbt(nbt, this.inventory);
  39. }
  40.  
  41. @Override
  42. public NbtCompound writeNbt(NbtCompound nbt) {
  43. super.writeNbt(nbt);
  44. Inventories.writeNbt(nbt, this.inventory);
  45. return nbt;
  46. }
  47. }

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

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. // 我们的大型箱子中不同部分的公共id
  9. public static final Identifier BOX = new Identifier(MOD_ID, "box_block");
  10.  
  11. static {
  12. BOX_BLOCK = Registry.register(Registries.BLOCK, BOX, new BoxBlock(FabricBlockSettings.copyOf(Blocks.CHEST)));
  13. BOX_BLOCK_ITEM = Registry.register(Registries.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. // 1.17 之前
  17. BOX_BLOCK_ENTITY = Registry.register(Registries.BLOCK_ENTITY_TYPE, BOX, BlockEntityType.Builder.create(BoxBlockEntity::new, BOX_BLOCK).build(null));
  18. // 在 1.17 使用 FabricBlockEntityTypeBuilder 而不是 BlockEntityType.Builder
  19. BOX_BLOCK_ENTITY = Registry.register(Registries.BLOCK_ENTITY_TYPE, BOX, FabricBlockEntityTypeBuilder.create(BoxBlockEntity::new, BOX_BLOCK).build(null));
  20. }
  21.  
  22. @Override
  23. public void onInitialize() {
  24.  
  25. }
  26. }

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 = 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.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. drawTexture(matrices, x, y, 0, 0, backgroundWidth, backgroundHeight);
  17. //1.20或者以上的版本,这个方法在DrawContext类里面。
  18. }
  19.  
  20. @Override
  21. public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
  22. renderBackground(matrices);
  23. super.render(matrices, mouseX, mouseY, delta);
  24. drawMouseoverTooltip(matrices, mouseX, mouseY);
  25. }
  26.  
  27. @Override
  28. protected void init() {
  29. super.init();
  30. // 将标题居中
  31. titleX = (backgroundWidth - textRenderer.getWidth(title)) / 2;
  32. }
  33. }

注册 Screen 和 ScreenHandler

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

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

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

/* ... */
  "entrypoints": {
    /* ... */
    "client": [
      "(到这个 ExampleModClient 的路径)"
    ]
  },

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

ExampleMod.java
  1. public class ExampleMod implements ModInitializer {
  2. [...]
  3. public static final ScreenHandlerType<BoxScreenHandler> BOX_SCREEN_HANDLER;
  4. [...]
  5. static {
  6. [...]
  7. // 我们在这里使用 registerSimple 因为实体不是 ExtendedScreenHandlerFactory
  8. // 而是 NamedScreenHandlerFactory.
  9. // 后面的教程中,你将会看到 ExtendedScreenHandlerFactory 能做什么!
  10. BOX_SCREEN_HANDLER = ScreenHandlerRegistry.registerSimple(BOX, BoxScreenHandler::new);
  11. }
  12.  
  13. @Override
  14. public void onInitialize() {
  15.  
  16. }
  17. }

结果

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

延伸阅读

zh_cn/tutorial/screenhandler.txt · Last modified: 2023/09/14 01:42 by wjz_p