====== 创建容器方块 ======
这篇教程会创建一个类似发射器的简单存储方块,并解释如何使用 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'',并使用[[inventory|物品栏教程]]中提到的 ''ImplementedInventory'' 接口。
public class BoxBlockEntity extends BlockEntity implements NamedScreenHandlerFactory, ImplementedInventory {
private final DefaultedList inventory = DefaultedList.ofSize(9, ItemStack.EMPTY);
public BoxBlockEntity(BlockPos pos, BlockState state) {
super(ExampleMod.BOX_BLOCK_ENTITY, pos, state);
}
// 从 ImplementedInventory 接口
@Override
public DefaultedList 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()));
public static final BlockEntityType BOX_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY_TYPE, Identifier.of("tutorial", "box_block"),
BlockEntityType.Builder.create(BoxBlockEntity::new, BOX_BLOCK).build());
// 在 1.17 使用 FabricBlockEntityTypeBuilder 而不是 BlockEntityType.Builder
// public static final BlockEntityType 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 元素。
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 {
// 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 =====
所有的屏幕都只是仅存于客户端的概念,因此只能在客户端进行注册。
@Environment(EnvType.CLIENT)
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 BOX_SCREEN_HANDLER = Registry.register(Registries.SCREEN_HANDLER, Identifier.of("tutorial", "box_block"), new ScreenHandlerType<>(BoxScreenHandler::new, FeatureSet.empty()));
@Override
public void onInitialize() {
[...]
}
}
===== 结果 =====
您现在应该创建了容器方块,可以轻易地改变它以包含更小或者更大的物品栏。也许还需要应用一个纹理!qwq
{{:tutorial:bildschirmfoto_vom_2020-08-14_18-32-07.png?nolink&400|}}
===== 延伸阅读 =====
* [[extendedscreenhandler|屏幕开启时,使用 Extended ScreenHandlers 同步自定义的数据]]
* [[propertydelegates|使用 PropertyDelegates 持续同步整数]]
使用 ''ScreenHandler'' 的示例模组:[[https://github.com/FabricMC/fabric/tree/1.16/fabric-screen-handler-api-v1/src/testmod|Github 上的示例模组]]。