容器

容器

方块实体的常见用途是保存物品。Minecraft 中许多重要方块,例如熔炉和箱子,都是如此。Minecraft 使用 Container 在对象上保存物品。

Container 接口定义了 #getItem、#setItem、#removeItem 等查询和更新方法。但接口本身没有底层列表或其他数据结构,需要由实现者决定。

因此,不仅方块实体,任何类都能实现 Container,典型例子还有实体物品栏,以及 Mod 中常见的背包等物品。

警告 NeoForge 提供 ItemStacksResourceHandler,可在许多地方代替 Container。应尽可能优先使用它,以便更清晰地与其他 Container 或 ItemStacksResourceHandler 交互。

本文主要用于理解原版代码,或供同时适配多个加载器的开发者参考。自己的代码应尽可能使用 ItemStacksResourceHandler,相关文档仍在编写中。

基础容器实现

与其他 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);
    }

    // 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();
    }

    // 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() {

    }

    // 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 是带少量附加功能的基础实现。不需要特殊行为时可以直接使用。

BaseContainerBlockEntity

BaseContainerBlockEntity 是许多重要方块实体的基类,包括箱子和类似箱子的方块、各种熔炉、漏斗、发射器、投掷器、酿造台等。

除 Container 外,它还实现 MenuProvider 和 Nameable:

  • Nameable 定义与自定义名称相关的方法,除许多方块实体外,Entity 等类也实现它,使用 Component 系统。
  • MenuProvider 定义 #createMenu,用于根据容器构造 AbstractContainerMenu。因此,唱片机这类不需要关联图形界面的容器,不适合使用这个类。

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。让方块实体继承它,即可获得预先实现容器功能的方块实体。

提示 实现 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:

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。

容器使用者

能够访问容器的生物实体实现 ContainerUser,定义是否正在打开容器,以及可与容器交互的最大方块距离。与容器交互时,例如右键箱子,会向 Container#startOpen 传入使用者;关闭容器时,例如退出箱子菜单,则调用 stopOpen。

这些方法通常通过 ContainerOpenersCounter 统计当前打开容器的生物实体数量,用于某些实体 AI 和渲染。

物品堆上的容器

前面主要讨论方块实体上的容器,也可以通过 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),即可用于菜单或其他场景。

警告 注意,直接操作容器的菜单,在修改 ItemStack 时必须先调用 #copy(),否则会破坏数据组件的不可变约定。NeoForge 为此提供 StackCopySlot。

实体上的容器

Entity 上的容器比较复杂:没有统一方式判断实体是否具有容器,具体取决于实体类型,可能需要大量特殊处理。

自己的实体可以直接实现 Container,但不能再继承 SimpleContainer 等类,因为父类位置已由 Entity 占用。

生物上的容器

Mob 不实现 Container,但实现了 EquipmentUser 等接口。其中提供 #setItemSlot(EquipmentSlot, ItemStack)、#getItemBySlot(EquipmentSlot)、#setDropChance(EquipmentSlot, float)。代码上虽不属于 Container,功能却类似:将装备槽与物品堆关联。

与容器最明显的差别是没有列表式顺序,尽管 Mob 底层使用 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 获取,可以像普通容器一样操作。

物品栏内容保存在两处:

  • NonNullList<ItemStack> items 保存 36 个主物品栏槽位,包括索引 0–8 的九个快捷栏槽位。
  • EntityEquipment equipment 映射按顺序保存 FEET、LEGS、CHEST、HEAD 护甲槽,以及 OFFHAND、BODY 和 SADDLE。

遍历内容时,建议先遍历 items,再按照 Inventory#EQUIPMENT_SLOT_MAPPING 的索引遍历 equipment。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。