容器菜单 26.1.2
讲解如何为容器方块创建简单菜单的指南。
前置条件
你 应该首先阅读方块容器以熟悉如何创建容器方块实体。
打开容器(例如箱子)时,主要需要两样东西才能显示其中的内容:
- 一个负责将内容和背景渲染到显示器上的
Screen。 - 一个处理 Shift 点击逻辑以及服务器和客户端之间同步的
Menu。
在本指南中,我们将创建一个泥土箱子,其中包含一个 3x3 的容器,可以通过右键单击并打开屏幕来访问容器。
创建方块
首先,我们要创建一个方块和方块实体;请阅读方块容器指南进一步了解。
java
public class DirtChestBlock extends BaseEntityBlock {
@Override
public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new DirtChestBlockEntity(pos, state);
}
// ...
}1
2
3
4
5
6
7
2
3
4
5
6
7
java
public class DirtChestBlockEntity extends BlockEntity implements ImplementedContainer {
public static final int CONTAINER_SIZE = 3 * 3;
private final NonNullList<ItemStack> items = NonNullList.withSize(CONTAINER_SIZE, ItemStack.EMPTY);
// ...
}1
2
3
4
5
6
2
3
4
5
6
打开菜单
我们希望能够以某种方式打开菜单,因此我们将在 useWithoutItem 方法中处理这个问题:
java
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
if (!level.isClientSide() && level.getBlockEntity(pos) instanceof DirtChestBlockEntity dirtChest) {
player.openMenu(dirtChest);
}
return InteractionResult.SUCCESS;
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
实现 MenuProvider
为了添加菜单功能,我们现在需要在方块实体中实现 MenuProvider:
java
public class DirtChestBlockEntity extends BlockEntity implements ImplementedContainer, MenuProvider {
@Override
@NonNull
public Component getDisplayName() {
return Component.translatable("block.example-mod.dirt_chest");
}
@Override
public @Nullable AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) {
return null;
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
getDisplayName 方法返回方块的名称,将显示在屏幕顶部。
创建菜单
createMenu 要求我们返回一个菜单,但我们还没有为我们的方块创建菜单。 为此,我们将创建一个继承自 AbstractContainerMenu 的 DirtChestMenu 类:
java
public class DirtChestMenu extends AbstractContainerMenu {
private final Container container;
// Client-side constructor
public DirtChestMenu(final int containerId, final Inventory inventory) {
this(containerId, inventory, new SimpleContainer(DirtChestBlockEntity.CONTAINER_SIZE));
}
// Server-side constructor
public DirtChestMenu(final int containerId, final Inventory inventory, final Container container) {
super(ModMenuType.DIRT_CHEST, containerId);
checkContainerSize(container, DirtChestBlockEntity.CONTAINER_SIZE);
this.container = container;
// Some containers do custom logic when opened by a player.
// TODO: is this intended to use this. ?
container.startOpen(inventory.player);
int rows = 3;
int columns = 3;
// Add the slots for our container in a 3x3 grid.
for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) {
int slot = x + y * 3;
this.addSlot(new Slot(container, slot, 62 + x * 18, 17 + y * 18));
}
}
// Add the player inventory slots.
this.addStandardInventorySlots(inventory, 8, 84);
}
@Override
public ItemStack quickMoveStack(Player player, int slotIndex) {
Slot slot = this.slots.get(slotIndex);
if (!slot.hasItem()) {
return ItemStack.EMPTY;
}
ItemStack stack = slot.getItem();
ItemStack clicked = stack.copy();
if (slotIndex < this.container.getContainerSize()) {
if (!this.moveItemStackTo(stack, this.container.getContainerSize(), this.slots.size(), true)) {
return ItemStack.EMPTY;
}
} else if (!this.moveItemStackTo(stack, 0, this.container.getContainerSize(), false)) {
return ItemStack.EMPTY;
}
if (stack.isEmpty()) {
slot.setByPlayer(ItemStack.EMPTY);
} else {
slot.setChanged();
}
return clicked;
}
@Override
public boolean stillValid(Player player) {
return this.container.stillValid(player);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
当服务器想要打开菜单时,客户端构造函数就会被调用。 它会创建一个空容器,然后该容器会自动与服务器上的实际容器同步。
服务端构造函数在服务器上被调用,因为它知道容器的内容,所以可以直接将其作为参数传递。
quickMoveStack 处理菜单中按住 Shift 键点击的物品。 这个示例复现了原版菜单(如箱子和发射器)的行为。
然后我们需要在一个新的 ModMenuType 类中注册菜单:
java
public class ModMenuType {
public static final MenuType<DirtChestMenu> DIRT_CHEST = register("dirt_chest", DirtChestMenu::new);
public static <T extends AbstractContainerMenu> MenuType<T> register(
String name,
MenuType.MenuSupplier<T> constructor
) {
return Registry.register(BuiltInRegistries.MENU, name, new MenuType<>(constructor, FeatureFlagSet.of()));
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
现在我们可以将方块实体中的 createMenu 的返回值设置为使用我们的菜单:
java
@Override
public @Nullable AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) {
return new DirtChestMenu(containerId, inventory, this);
}1
2
3
4
2
3
4
INFO
createMenu 方法只会在服务端调用,所以我们调用服务端构造函数,并将 this(方块实体)作为容器参数传递。
创建屏幕
为了在客户端实际显示容器的内容,我们还需要为菜单创建一个屏幕。 我们将创建一个继承自 AbstractContainerScreen 的新类:
java
public class DirtChestScreen extends AbstractContainerScreen<DirtChestMenu> {
private static final Identifier CONTAINER_TEXTURE = Identifier.withDefaultNamespace("textures/gui/container/dispenser.png");
public DirtChestScreen(DirtChestMenu abstractContainerMenu, Inventory inventory, Component component) {
super(abstractContainerMenu, inventory, component);
// Center the title
this.titleLabelX = (this.imageWidth - this.font.width(this.title)) / 2;
}
@Override
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) {
super.extractBackground(graphics, mouseX, mouseY, delta);
graphics.blit(RenderPipelines.GUI_TEXTURED, CONTAINER_TEXTURE, this.leftPos, this.topPos, 0.0F, 0.0F, this.imageWidth, this.imageHeight, BACKGROUND_TEXTURE_WIDTH, BACKGROUND_TEXTURE_HEIGHT);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对于这个屏幕的背景,我们直接使用了默认的发射器屏幕纹理,因为我们的泥土箱子使用相同的槽位布局。 或者,你 也可以为 CONTAINER_TEXTURE 提供自己的纹理。
因为这是一个菜单屏幕,所以我们还需要使用 MenuScreens#register() 方法在客户端注册它:
java
public class ExampleModScreens implements ClientModInitializer {
@Override
public void onInitializeClient() {
MenuScreens.register(ModMenuType.DIRT_CHEST, DirtChestScreen::new);
}
}1
2
3
4
5
6
2
3
4
5
6
游戏加载完毕后,你应该会看到一个泥土箱子,右键点击即可打开菜单并将物品存放其中。






