Container

在上一节中,我们已经学习了如何创建一个非常简单的GUI,但是那个GUI显然有些功能是实现不了的。在这节中,我们将要来学习GUI中一个非常重要的类,Container。

在开始之前我们先要回答Container是什么,已经它的存在是为了什么。Container和Screen的关系有点像是,Block和TileEntity的关系,Container本身不会被渲染,它存在的意义就是用来临时的储存数据(Container没法把数据保存到硬盘上)以及数据同步。但是很可惜的是Container所能同步的数据非常有限,不过就是Inventory和IntArray而已,如果你需要更加复杂的数据同步,那只能自己手动发包了。

guistructure

这里这张图是GUI操作中数据流向图,实线和虚线代表了数据流动的方向。所以跨过中间的箭头都需要进行发包。

当你在Screen上点击按钮或者进行什么别的什么操作,最后修改的都应该是服务端的方块实体或者是Container里面的值,然后在通过它们的同步机制,同步回客户端。切记不能直接修改客户端的方块实体或者Container里的值。请注意,在客户端Container和方块实体之间是没有数据同步的。

希望你大脑里对这个过程有些印象,接下来我们就开始一步一步的实现一个非常简单的箱子。这里涉及到的类挺多的,建议大家在IntelliJ中打开源代码,自己试着理清整个调用关系。

在这节中,我们要创建一个非常简单的箱子。

首先我们先来看方块相关的代码

public class ObsidianFirstContainerBlock extends Block {
    public ObsidianFirstContainerBlock() {
        super(Properties.create(Material.ROCK).hardnessAndResistance(5));
    }

    @Override
    public boolean hasTileEntity(BlockState state) {
        return true;
    }

    @Nullable
    @Override
    public TileEntity createTileEntity(BlockState state, IBlockReader world) {
        return new ObsidianFirstContainerTileEntity();
    }

    @Override
    public ActionResultType onBlockActivated(BlockState state, World worldIn, BlockPos pos, PlayerEntity player, Hand handIn, BlockRayTraceResult hit) {
        if (!worldIn.isRemote && handIn == Hand.MAIN_HAND) {
            ObsidianFirstContainerTileEntity obsidianFirstContainerTileEntity = (ObsidianFirstContainerTileEntity) worldIn.getTileEntity(pos);
            NetworkHooks.openGui((ServerPlayerEntity) player, obsidianFirstContainerTileEntity, (PacketBuffer packerBuffer) -> {
                packerBuffer.writeBlockPos(obsidianFirstContainerTileEntity.getPos());
            });
        }
        return ActionResultType.SUCCESS;
    }
}

我们来看最为重要的方法,

@Override
public ActionResultType onBlockActivated(BlockState state, World worldIn, BlockPos pos, PlayerEntity player, Hand handIn, BlockRayTraceResult hit) {
  if (!worldIn.isRemote && handIn == Hand.MAIN_HAND) {
    ObsidianFirstContainerTileEntity obsidianFirstContainerTileEntity = (ObsidianFirstContainerTileEntity) worldIn.getTileEntity(pos);
    NetworkHooks.openGui((ServerPlayerEntity) player, obsidianFirstContainerTileEntity, (PacketBuffer packerBuffer) -> {
      packerBuffer.writeBlockPos(obsidianFirstContainerTileEntity.getPos());
    });
  }
  return ActionResultType.SUCCESS;
}

大部分的内容没什么好说的,想想大家以及可以读的懂了,这里最为关键的只有一句话。

NetworkHooks.openGui((ServerPlayerEntity) player, obsidianFirstContainerTileEntity, (PacketBuffer packerBuffer) -> {
  packerBuffer.writeBlockPos(obsidianFirstContainerTileEntity.getPos());
});

这句话就是打开一个包含有Container的GUI的语句。

当你执行这条命令时,Minecraft会同时在客户端和服务端各自创建一个Container,并且创建的流程不一样。我们来讲一下这个创建的流程时什么样子的。

  • 服务端:这个函数的第二个参数是是一个INamedContainerProvider接口,这里我们让我们的方块实体实现了这个接口(大部分时候你都应该这么做)。这个接口下有个叫做createMenu的方法,在服务端最终会调用这个方法来创建Container。

  • 客户端:客户端的操作首先会进行一次发包处理,NetworkHooks.openGui方法的第三个参数让你可以在发送的数据包中额外的附加数据(为了方便展示,我没有使用内置的封装好的函数),这里添加数据的方法,相信已经会自定义网络包的你,理解起来不难。

    当客户端接受到发送来的包之后,客户的会调用你在注册时设置的构造方法创建,具体的内容我们留到注册部分再说

接下来我们开看看我们的方块实体

public class ObsidianFirstContainerTileEntity extends TileEntity implements ITickableTileEntity, INamedContainerProvider {

    private Inventory inventory = new Inventory(1);
    private ObsidianFirstContainerItemNumber itemNumber = new ObsidianFirstContainerItemNumber();

    public ObsidianFirstContainerTileEntity() {
        super(TileEntityTypeRegistry.obsidianFirstContainerTileEntity.get());
    }

    @Override
    public ITextComponent getDisplayName() {
        return new TranslationTextComponent("gui." + Utils.MOD_ID + ".first_container");
    }

    @Nullable
    @Override
    public Container createMenu(int sycID, PlayerInventory inventory, PlayerEntity player) {
        return new ObsidianFirstContainer(sycID, inventory, this.pos, this.world, itemNumber);
    }

    @Override
    public void read(BlockState state, CompoundNBT nbt) {
        this.inventory.addItem(ItemStack.read(nbt.getCompound("item")));
        super.read(state, nbt);
    }

    @Override
    public CompoundNBT write(CompoundNBT compound) {
        ItemStack itemStack = this.inventory.getStackInSlot(0).copy();
        compound.put("item", itemStack.serializeNBT());
        return super.write(compound);
    }

    public Inventory getInventory() {
        return inventory;
    }

    @Override
    public void tick() {
        if (!world.isRemote) {
            this.itemNumber.set(0, this.inventory.getStackInSlot(0).getCount());
        }
    }
}

这些就是方块实体的代码,我们细细的来讲。

首先就是。

@Override
public ITextComponent getDisplayName() {
 return new TranslationTextComponent("gui." + Utils.MOD_ID + ".first_container");
}

@Nullable
@Override
public Container createMenu(int sycID, PlayerInventory inventory, PlayerEntity player) {
 return new ObsidianFirstContainer(sycID, inventory, this.pos, this.world, itemNumber);
}

这两个方法是INamedContainerProvider规定的方法,可以看到我们在createMenu方法中创建了我们的Container。

接下来,我们看一个变量

private Inventory inventory = new Inventory(1);

这个变量的代表了一个只有1个「Slot(槽位)」的「Inventory(物品栏)」。请注意这里的Slot和Inventory也是只是一个抽象上的概念。

Inventoryandslot

因为我们希望的我们要创建的是一个箱子,自然需要存放物品,所以需要创建一个Inventory,这个是原版已经实现好的IInvetory接口的一个类,如果需要你也可以自己实现这个接口。

至于数据的保存和恢复这里也就不在多提,大家可以自行查看。

接下来是

private ObsidianFirstContainerItemNumber itemNumber = new ObsidianFirstContainerItemNumber();

我们接下来看一看这个类。

public class ObsidianFirstContainerItemNumber implements IIntArray {
    int i = 0;

    @Override
    public int get(int index) {
        return i;
    }

    @Override
    public void set(int index, int value) {
        i = value;
    }

    @Override
    public int size() {
        return 1;
    }
}

这个类继承了IIntArray,具体的内容非常的简单,相信大家都能看懂。接下去的问题就是,什么是IntArrayIntArray简单是Container可以自动同步数据的两个类中的一个,另外一个类就是我们之前提及过的InventoryIntArray的作用是给类似于熔炉这样用进度条的GUI来同步Int值用的(熔炉的进度条是根据Int值的大小按比例绘制的)。

然后我们回过头去看TileEntity中的

@Override
public void tick() {
  if (!world.isRemote) {
    this.itemNumber.set(0, this.inventory.getStackInSlot(0).getCount());
  }
}

我们在服务端,每个tick计算inventory中物品的数量,并将其赋值给之前创建的itemNumber

至此方块实体部分的内容已经完成。

接下来就是我们的Container

public class ObsidianFirstContainer extends Container {
    private ObsidianFirstContainerItemNumber intArray;

    protected ObsidianFirstContainer(int id, PlayerInventory playerInventory, BlockPos pos, World world, ObsidianFirstContainerItemNumber intArray) {
        super(ContainerTypeRegistry.obsidianFirstContainer.get(), id);
        this.intArray = intArray;
        trackIntArray(this.intArray);
        ObsidianFirstContainerTileEntity obsidianFirstContainerTileEntity = (ObsidianFirstContainerTileEntity) world.getTileEntity(pos);
        this.addSlot(new Slot(obsidianFirstContainerTileEntity.getInventory(), 0, 80, 32));
        layoutPlayerInventorySlots(playerInventory, 8, 84);
    }

    @Override
    public boolean canInteractWith(PlayerEntity playerIn) {
        return true;
    }

    @Override
    public ItemStack transferStackInSlot(PlayerEntity playerIn, int index) {
        return ItemStack.EMPTY;
    }

    private int addSlotRange(IInventory inventory, int index, int x, int y, int amount, int dx) {
        for (int i = 0; i < amount; i++) {
            addSlot(new Slot(inventory, index, x, y));
            x += dx;
            index++;
        }
        return index;
    }

    private int addSlotBox(IInventory inventory, int index, int x, int y, int horAmount, int dx, int verAmount, int dy) {
        for (int j = 0; j < verAmount; j++) {
            index = addSlotRange(inventory, index, x, y, horAmount, dx);
            y += dy;
        }
        return index;
    }

    private void layoutPlayerInventorySlots(IInventory inventory, int leftCol, int topRow) {
        // Player inventory
        addSlotBox(inventory, 9, leftCol, topRow, 9, 18, 3, 18);

        // Hotbar
        topRow += 58;
        addSlotRange(inventory, 0, leftCol, topRow, 9, 18);
    }

    public IIntArray getIntArray() {
        return intArray;
    }
}

其实Container里的内容相对来说比较简单。

protected ObsidianFirstContainer(int id, PlayerInventory playerInventory, BlockPos pos, World world, ObsidianFirstContainerItemNumber intArray) {
  super(ContainerTypeRegistry.obsidianFirstContainer.get(), id);
  this.intArray = intArray;
  trackIntArray(this.intArray);
  ObsidianFirstContainerTileEntity obsidianFirstContainerTileEntity = (ObsidianFirstContainerTileEntity) world.getTileEntity(pos);
  this.addSlot(new Slot(obsidianFirstContainerTileEntity.getInventory(), 0, 80, 32));
  layoutPlayerInventorySlots(playerInventory, 8, 84);
}

首先就是构造函数。

this.intArray = intArray;
trackIntArray(this.intArray);

在这里,我们获取需要同步的IntArray并且调用trackIntArray方法将其加入同步序列中。如此,当你在客户端修改IntArray的值,服务端就可以自动同步数据了。

接下来就是添加Slot。

this.addSlot(new Slot(obsidianFirstContainerTileEntity.getInventory(), 0, 80, 32));

一个Inventory里可以有很多个Slot,这里我们需要给他添加进我们的GUI中,其中Slot第一个参数是一个Inventory,第二个参数是Inventory中ItemStack的序号,因为我们的Inventory总数只有1,所以这里填入0即可。最后两个坐标是X,Y。这个X,Y的起始点是( (this.width - this.xSize) / 2, (this.height - this.ySize) / 2),这里的xSizeySize我们在之后将Screen的时候会讲到。

最后的layoutPlayerInventorySlots是自定义的函数用来把玩家自带的Inventory添加到Container中,这里大家可以跟着自行计算位置,就不多讲了。

接下来是

@Override
public boolean canInteractWith(PlayerEntity playerIn) {
  return true;
}

这个函数是用来判断玩家能否打开GUI,一般情况下这里都应该做一次玩家和方块的距离计算,用来防止玩家在过远的地方打开GUI,这里我比较懒就不计算了。

@Override
public ItemStack transferStackInSlot(PlayerEntity playerIn, int index) {
  return ItemStack.EMPTY;
}

最后这个函数是用来控制玩家按住Shift键以后点击物品的行为,这里我也是懒得写所以直接返回空了。在默认情况下,你需要实现按住Shift讲物品放入指定空的Slot行为。

因为Container主要的作用就是数据同步,所以Container类中的内容并不多,当然Container自带的方法不止这些,大家可以自行阅读Container的注释。

然后就是我们的Screen。

public class ObsidianFirstContainerScreen extends ContainerScreen<ObsidianFirstContainer> {
    private final ResourceLocation OBSIDIAN_CONTAINER_RESOURCE = new ResourceLocation(Utils.MOD_ID, "textures/gui/container.png");
    private final int textureWidth = 176;
    private final int textureHeight = 166;

    @Override
    public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) {
        renderBackground(matrixStack);
        super.render(matrixStack, mouseX, mouseY, partialTicks);
        renderHoveredTooltip(matrixStack, mouseX, mouseY);
    }

    public ObsidianFirstContainerScreen(ObsidianFirstContainer screenContainer, PlayerInventory inv, ITextComponent titleIn) {
        super(screenContainer, inv, titleIn);
        this.xSize = textureWidth;
        this.ySize = textureHeight;
    }

    @Override
    protected void drawGuiContainerForegroundLayer(MatrixStack matrixStack, int x, int y) {
        drawCenteredString(matrixStack, this.font, Integer.toString(this.getContainer().getIntArray().get(0)), 82, 20, 0xeb0505);
    }

    @Override
    protected void drawGuiContainerBackgroundLayer(MatrixStack matrixStack, float partialTicks, int x, int y) {
        this.renderBackground(matrixStack);
        this.minecraft.getTextureManager().bindTexture(OBSIDIAN_CONTAINER_RESOURCE);
        int i = (this.width - this.xSize) / 2;
        int j = (this.height - this.ySize) / 2;
        blit(matrixStack, i, j, 0, 0, xSize, ySize, this.textureWidth, textureHeight);
    }
}

首先我们继承的是ContainerScreen,这个是Screen类中的一个子类,用来和Container绑定用的。比起Screen,它实现了一些额外的功能,比如按照指定的Container中的数据自动添加组件并渲染出ContainerSlot(再一次提醒渲染工作都是由Screen完成,和Container无关)。

首先我们先来讲xSizeySize,这两个变量时ContainerScreen创建的,用来设定GUI窗口的大小,这里我们就将其设置的和我背景图大小一样。

Screen内主要有两个函数drawGuiContainerBackgroundLayerdrawGuiContainerForegroundLayer,这两个函数中绘制内容时的坐标系不同。

其中drawGuiContainerBackgroundLayer的起始点是(0,0),而drawGuiContainerForegroundLayer的起始点是`

( (this.width - this.xSize)/2 , (this.height - this.ySize)/2 )。

你应该在drawGuiContainerBackgroundLayer渲染你的背景图,而在drawGuiContainerForegroundLayer渲染你的组件(字符串,按钮等)。

如果你已经有了上一节的基础,相信渲染部分的内容并不会难到你,这里就不多加解释了。

这里多出来的是render方法,为了能让我们的GUI显示ToolTip,这里在render方法里调用了renderHoveredToolTip方法,当然这里也不要忘了调用父类的render方法。

@Override
public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) {
  renderBackground(matrixStack);
  super.render(matrixStack, mouseX, mouseY, partialTicks);
  renderHoveredTooltip(matrixStack, mouseX, mouseY);
}

接下来时注册,我们首先需要注册ContainerType

public class ContainerTypeRegistry {
    public static final DeferredRegister<ContainerType<?>> CONTAINERS = DeferredRegister.create(ForgeRegistries.CONTAINERS, Utils.MOD_ID);
    public static final RegistryObject<ContainerType<ObsidianFirstContainer>> obsidianFirstContainer = CONTAINERS.register("obsidian_first_container", () -> IForgeContainerType.create((int windowId, PlayerInventory inv, PacketBuffer data) -> new ObsidianFirstContainer(windowId, inv, data.readBlockPos(), Minecraft.getInstance().world, new ObsidianFirstContainerItemNumber())));
}

可以看见我们调用了IForgeContainerType.create来创建ContainerType,其中第二个参数里所写的创建我们Container的指令,会在客户端收到网络数据包创建Container时执行,其中的windowID 就是用来在两端之间同步Container用的。data.readBlockPos(),可以看到我们从数据包里获取了之前调用NetworkHooks.openGui塞入的值。当然别忘了在你Mod的主类里将CONTAINERS添加进Mod总线中。

接下去你还需要手动把ContainerContainerScreen相绑定,同样别忘了放在enqueueWork中执行。

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModBusEventHandler {
    @SubscribeEvent
    public static void onClientSetupEvent(FMLClientSetupEvent event) {
        event.enqueueWork(() -> {
            ScreenManager.registerFactory(ContainerTypeRegistry.obsidianFirstContainer.get(), ObsidianFirstContainerScreen::new);
        });
    }
}

至此,我们的特殊箱子就已经实现了,打开游戏看看吧。

image-20200508193516742

源代码

相关的资料