容器
Minecraft 中的方块实体、实体等许多系统都会存储物品。为此,Minecraft 使用 Container 实现。
Container 接口定义了 #getItem、#setItem、#removeItem 等查询和更新方法。接口本身不包含底层列表或其他数据结构,存储方式由实现者决定。
因此,不仅方块实体,其他任意类也可以实现 Container,典型例子包括实体物品栏,以及背包等常见 Mod 物品。
警告 NeoForge 在许多场景下提供
ItemStackHandler替代Container,应尽可能优先使用它,因为它与其他Container/ItemStackHandler的交互更清晰。本篇主要供阅读原版代码或开发跨加载器 Mod 时参考。自己的代码应尽可能使用
ItemStackHandler,其文档仍在编写中。
基础容器实现
与 Java 其他接口一样,只要满足接口方法要求,就可以按任意方式实现容器。常见方式是使用固定长度的 NonNullList<ItemStack> 作为底层结构。单槽容器也可以直接使用一个 ItemStack 字段。
例如,具有 27 个槽位(一个箱子大小)的基础实现如下:
public class MyContainer implements Container {
private final NonNullList<ItemStack> items = NonNullList.withSize(
// The size of the list, i.e. the amount of slots in our container.
27,
// The default value to be used in place of where you'd use null in normal lists.
ItemStack.EMPTY
);
// The amount of slots in our container.
@Override
public int getContainerSize() {
return 27;
}
// Whether the container is considered empty.
@Override
public boolean isEmpty() {
return this.items.stream().allMatch(ItemStack::isEmpty);
}
// Return the item stack in the specified slot.
@Override
public ItemStack getItem(int slot) {
return this.items.get(slot);
}
// Call this when changes are done to the container, i.e. when item stacks are added, modified, or removed.
// For example, you could call BlockEntity#setChanged here.
@Override
public void setChanged() {
}
// Remove the specified amount of items from the given slot, returning the stack that was just removed.
// We defer to ContainerHelper here, which does this as expected for us.
// However, we must call #setChanged manually.
@Override
public ItemStack removeItem(int slot, int amount) {
ItemStack stack = ContainerHelper.removeItem(this.items, slot, amount);
this.setChanged();
return stack;
}
// Remove all items from the specified slot, returning the stack that was just removed.
// We again defer to ContainerHelper here, and we again have to call #setChanged manually.
@Override
public ItemStack removeItemNoUpdate(int slot) {
ItemStack stack = ContainerHelper.takeItem(this.items, slot);
this.setChanged();
return stack;
}
// Set the given item stack in the given slot. Limit to the max stack size of the container first.
@Override
public void setItem(int slot, ItemStack stack) {
stack.limitSize(this.getMaxStackSize(stack));
this.items.set(slot, stack);
this.setChanged();
}
// Whether the container is considered "still valid" for the given player. For example, chests and
// similar blocks check if the player is still within a given distance of the block here.
@Override
public boolean stillValid(Player player) {
return true;
}
// Clear the internal storage, setting all slots to empty again.
@Override
public void clearContent() {
items.clear();
this.setChanged();
}
}
SimpleContainer
SimpleContainer 是带少量额外功能的基础容器实现,例如可添加 ContainerListener。没有特殊要求时可以使用它。
BaseContainerBlockEntity
BaseContainerBlockEntity 是 Minecraft 许多重要方块实体的基类,例如箱子及类似方块、各种熔炉、漏斗、发射器、投掷器和酿造台等。
除了 Container,它还实现 MenuProvider 和 Nameable:
Nameable定义设置名称和自定义名称的相关方法。除了许多方块实体,Entity等类也实现它。它使用Component系统。MenuProvider定义#createMenu,允许根据容器构造AbstractContainerMenu。因此,如果需要唱片机那样没有关联 GUI 的容器,就不宜使用此类。
BaseContainerBlockEntity 通过 #getItems 和 #setItems 两个方法,集中处理通常直接对 NonNullList<ItemStack> 的调用,显著减少样板代码。例如:
public class MyBlockEntity extends BaseContainerBlockEntity {
// The container size. This can of course be any value you want.
public static final int SIZE = 9;
// Our item stack list. This is not final due to #setItems existing.
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
// The constructor, like before.
public MyBlockEntity(BlockPos pos, BlockState blockState) {
super(MY_BLOCK_ENTITY.get(), pos, blockState);
}
// The container size, like before.
@Override
public int getContainerSize() {
return SIZE;
}
// The getter for our item stack list.
@Override
protected NonNullList<ItemStack> getItems() {
return items;
}
// The setter for our item stack list.
@Override
protected void setItems(NonNullList<ItemStack> items) {
this.items = items;
}
// The display name of the menu. Don't forget to add a translation!
@Override
protected Component getDefaultName() {
return Component.translatable("container.examplemod.myblockentity");
}
// The menu to create from this container. See below for what to return here.
@Override
protected AbstractContainerMenu createMenu(int containerId, Inventory inventory) {
return null;
}
}
该类同时是 BlockEntity 和 Container,因此继承它就能得到带预实现容器功能的方块实体。
WorldlyContainer
WorldlyContainer 是 Container 的子接口,允许按 Direction 访问槽位,主要用于只向特定方向开放部分容器的方块实体。例如,机器可向一侧输出,从其他方向输入,或反过来。简单实现如下:
// See BaseContainerBlockEntity methods above. You can of course extend BlockEntity directly
// and implement Container yourself if needed.
public class MyBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer {
// other stuff here
// Assume that slot 0 is our output and slots 1-8 are our inputs.
// Further assume that we output to the top and take inputs from all other sides.
private static final int[] OUTPUTS = new int[]{0};
private static final int[] INPUTS = new int[]{1, 2, 3, 4, 5, 6, 7, 8};
// Return an array of exposed slot indices based on the passed Direction.
@Override
public int[] getSlotsForFace(Direction side) {
return side == Direction.UP ? OUTPUTS : INPUTS;
}
// Whether items can be placed through the given side at the given slot.
// For our example, we return true only if we're not inputing from above and are in the index range [1, 8].
@Override
public boolean canPlaceItemThroughFace(int index, ItemStack itemStack, @Nullable Direction direction) {
return direction != Direction.UP && index > 0 && index < 9;
}
// Whether items can be taken from the given side and the given slot.
// For our example, we return true only if we're pulling from above and from slot index 0.
@Override
public boolean canTakeItemThroughFace(int index, ItemStack stack, Direction direction) {
return direction == Direction.UP && index == 0;
}
}
使用容器
创建容器后,就可以使用它。
Container 与 BlockEntity 有较大重叠,因此在可行时,最好将方块实体转换为 Container 来取得容器:
if (blockEntity instanceof Container container) {
// do something with the container
}
接着可以调用前述方法,例如:
// Get the first item in the container.
ItemStack stack = container.getItem(0);
// Set the first item in the container to dirt.
container.setItem(0, new ItemStack(Items.DIRT));
// Removes a quantity of (up to) 16 from the third slot.
container.removeItem(2, 16);
警告 访问超出容器大小的槽位时,容器可能抛出异常,也可能像
SimpleContainer那样返回ItemStack.EMPTY。
ItemStack 上的容器
前文主要介绍 BlockEntity 上的容器,但也可以通过 minecraft:container 数据组件,在 ItemStack 上实现容器:
// We use SimpleContainer as the superclass here so we don't have to reimplement the item handling logic ourselves.
// Due to implementation details of SimpleContainer, this may lead to race conditions if multiple parties
// can access the container at the same time, so we're just going to assume our mod doesn't allow that.
// You may of course use a different implementation of Container (or implement Container yourself) if needed.
public class MyBackpackContainer extends SimpleContainer {
// The item stack this container is for. Passed into and set in the constructor.
private final ItemStack stack;
public MyBackpackContainer(ItemStack stack) {
// We call super with our desired container size.
super(27);
// Setting the stack field.
this.stack = stack;
// We load the container contents from the data component (if present), which is represented
// by the ItemContainerContents class. If absent, we use ItemContainerContents.EMPTY.
ItemContainerContents contents = stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY);
// Copy the data component contents into our item stack list.
contents.copyInto(this.getItems());
}
// When the contents are changed, we save the data component on the stack.
@Override
public void setChanged() {
super.setChanged();
this.stack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(this.getItems()));
}
}
这样就创建了以物品为载体的容器。调用 new MyBackpackContainer(stack) 即可创建用于菜单或其他场景的容器。
警告 直接与
Container交互的菜单,在修改物品堆时必须调用#copy(),否则会违反数据组件的不可变约定。NeoForge 为此提供了StackCopySlot类。
Entity 上的容器
实体上的容器比较棘手:无法统一判断实体是否有容器,具体取决于所处理的实体,因此往往需要许多特殊分支。
创建自有实体时,可以直接实现 Container。但因为父类已经是 Entity,无法再继承 SimpleContainer 等容器类。
Mob 上的容器
Mob 不实现 Container,但实现了 EquipmentUser 等接口。该接口定义 #setItemSlot(EquipmentSlot, ItemStack)、#getItemBySlot(EquipmentSlot) 和 #setDropChance(EquipmentSlot, float)。虽然代码上与 Container 无关,但功能相似:将槽位(这里是装备槽位)与物品堆关联。
与 Container 最明显的区别是没有类似列表的顺序,尽管内部仍使用 NonNullList<ItemStack>。访问不通过槽位索引,而是使用七个 EquipmentSlot 枚举值:MAINHAND、OFFHAND、FEET、LEGS、CHEST、HEAD 和 BODY,其中 BODY 用于马和狼的盔甲。
与生物“槽位”交互的示例:
// Get the item stack in the HEAD (helmet) slot.
ItemStack helmet = mob.getItemBySlot(EquipmentSlot.HEAD);
// Put bedrock into the mob's FEET (boots) slot.
mob.setItemSlot(EquipmentSlot.FEET, new ItemStack(Items.BEDROCK));
// Enable that bedrock to always drop if the mob is killed.
mob.setDropChance(EquipmentSlot.FEET, 1f);
InventoryCarrier
村民等部分活体实体实现 InventoryCarrier。它声明 #getInventory 方法,返回 SimpleContainer。该接口用于需要实际物品栏、而不只是 EquipmentUser 装备槽位的非玩家实体。
玩家容器(玩家物品栏)
玩家物品栏由 Inventory 实现,它同时实现 Container 和前述 Nameable。Player 将其实例保存在 inventory 字段中,通过 Player#getInventory 访问,使用方式与其他容器一致。
内容保存在三个 public final NonNullList<ItemStack> 中:
items:36 个主物品栏槽位,其中包含九个快捷栏槽位(索引 0–8)。armor:长度为 4,依次存放FEET、LEGS、CHEST、HEAD的盔甲。该列表与Mob类似,使用EquipmentSlot访问。offhand:只包含副手槽位,长度为 1。
遍历物品栏时,建议先遍历 items,再遍历 armor,最后遍历 offhand,与原版行为保持一致。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。