概述
在本部分教程中我们可以做到:
- 控制GUI界面中的按钮点击行为
- 控制物品槽的物品进出是否被允许
- 控制物品槽的物品发生变动时的行为
- 控制按下Shift后物品槽中的物品变动
也就是这张图中的③和④两部分:
如果读者还有印象的话,在上一节中我们新添加了四个物品槽。这里作为演示,我们为物品槽添加一些设定:
- 第一个物品槽只能放进去金锭,而且最多只能放进去十六个
- 第二个物品槽放着一些钻石,然而玩家就是取不出来
- 第三个物品槽只能放进去绿宝石,通过放进去绿宝石的数目可以调整钻石的数目,使两者加在一起总是64
- 第四个物品槽放着一些铁锭,玩家虽然取不出来,但是可以通过其旁边的按钮调整铁锭的数量
然后我们再解决按下Shift后物品槽中的物品变动问题。还记得下面这张图吗?
解决按下Shift后物品槽中的物品变动问题需要用到它,也就是上面的数字索引。
第一个物品槽
我们先把四个物品槽作为字段存储起来,并且使用匿名内部类的方式扩展:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
protected Slot goldSlot;
protected Slot diamondSlot;
protected Slot emeraldSlot;
protected Slot ironSlot;
this.addSlotToContainer(this.goldSlot = new SlotItemHandler(items, 0, 38 + 0 * 32, 20)
{
// TODO
});
this.addSlotToContainer(this.diamondSlot = new SlotItemHandler(items, 1, 38 + 1 * 32, 20)
{
// TODO
});
this.addSlotToContainer(this.emeraldSlot = new SlotItemHandler(items, 2, 38 + 2 * 32, 20)
{
// TODO
});
this.addSlotToContainer(this.ironSlot = new SlotItemHandler(items, 3, 38 + 3 * 32, 20)
{
// TODO
});
然后我们从第一个物品槽开始,因为比较简单,这里先直接给出代码:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
this.addSlotToContainer(this.goldSlot = new SlotItemHandler(items, 0, 38 + 0 * 32, 20)
{
@Override
public boolean isItemValid(ItemStack stack)
{
return stack != null && stack.getItem() == Items.gold_ingot && super.isItemValid(stack);
}
@Override
public int getItemStackLimit(ItemStack stack)
{
return 16;
}
});
isItemValid
方法用于判定物品是否可以放进去,getItemStackLimit
方法用于判定物品最多可以放进去多少,这两个方法也没有什么好说的。
第二个物品槽
同样直接上代码:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
this.addSlotToContainer(this.diamondSlot = new SlotItemHandler(items, 1, 38 + 1 * 32, 20)
{
{
this.putStack(new ItemStack(Items.diamond, 64));
}
@Override
public boolean canTakeStack(EntityPlayer playerIn)
{
return false;
}
});
第二个物品槽会预先放进去钻石,所以我们先在构造方法中设置一组钻石:
{
this.putStack(new ItemStack(Items.diamond, 64));
}
然后我们覆写canTakeStack
方法用于判定物品是否可以拿出来。
@Override
public boolean canTakeStack(EntityPlayer playerIn)
{
return false;
}
第三个物品槽
首先,我们设置其只可以放出绿宝石:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
this.addSlotToContainer(this.emeraldSlot = new SlotItemHandler(items, 2, 38 + 2 * 32, 20)
{
@Override
public boolean isItemValid(ItemStack stack)
{
return stack != null && stack.getItem() == Items.emerald && super.isItemValid(stack);
}
});
然后我们需要做到物品槽发生变动时,旁边的钻石物品槽同步更新,这时我们需要覆写Slot
类的onSlotChanged
(注意Slot
类还有一个名称极其类似的方法名为onSlotChange
,请不要加以混淆)方法:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
this.addSlotToContainer(this.emeraldSlot = new SlotItemHandler(items, 2, 38 + 2 * 32, 20)
{
@Override
public boolean isItemValid(ItemStack stack)
{
return stack != null && stack.getItem() == Items.emerald && super.isItemValid(stack);
}
@Override
public void onSlotChanged()
{
ItemStack stack = this.getStack();
int amount = stack == null ? 64 : 64 - stack.stackSize;
ContainerDemo.this.diamondSlot.putStack(amount == 0 ? null : new ItemStack(Items.diamond, amount));
super.onSlotChanged();
}
});
这里的onSlotChanged
方法会在物品槽中的物品发生变动时触发,进而设置旁边钻石物品槽的物品。
第四个物品槽
我们首先设置其放着一些不能取出的物品,并将其初始值设置为一组铁锭:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
this.addSlotToContainer(this.ironSlot = new SlotItemHandler(items, 3, 38 + 3 * 32, 20)
{
{
this.putStack(new ItemStack(Items.iron_ingot, 64));
}
@Override
public boolean canTakeStack(EntityPlayer playerIn)
{
return false;
}
});
然后我们想要按钮参与交互,该怎么做呢?这该轮到GuiContainerDemo
类大显身手的时候了。为了方便,我们先设置一个方法用于获取ironSlot
:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
public Slot getIronSlot()
{
return this.ironSlot;
}
然后我们在GuiContainerDemo
类的构造方法中获取到这个物品槽并设置进私有字段中:
src/main/java/com/github/ustc_zzzz/fmltutor/client/gui/GuiContainerDemo.java(部分):
private Slot ironSlot;
public GuiContainerDemo(ContainerDemo inventorySlotsIn)
{
super(inventorySlotsIn);
this.xSize = 176;
this.ySize = 133;
this.ironSlot = inventorySlotsIn.getIronSlot();
}
然后我们就需要监听按钮点击事件的,在之前的代码中我们设置了两个按钮,并分别在其构造方法的第一个参数中传入了ID:
private static final int BUTTON_UP = 0;
private static final int BUTTON_DOWN = 1;
现在我们需要覆写GuiContainerDemo
类的actionPerformed
方法,因为这一方法会在按钮按下时调用:
src/main/java/com/github/ustc_zzzz/fmltutor/client/gui/GuiContainerDemo.java(部分):
@Override
protected void actionPerformed(GuiButton button) throws IOException
{
ItemStack stack = this.ironSlot.getStack();
int amount = stack == null ? 0 : stack.stackSize;
switch (button.id)
{
case BUTTON_DOWN:
amount = (amount + 64) % 65;
break;
case BUTTON_UP:
amount = (amount + 1) % 65;
break;
default:
super.actionPerformed(button);
return;
}
this.ironSlot.putStack(amount == 0 ? null : new ItemStack(Items.iron_ingot, amount));
}
我们根据按钮的ID来判断是哪个按钮,然后执行对应的操作。
GUI关闭后的操作
一般情况下,如果你的GUI是和一个TileEntity相连接,那么关闭GUI时你是可以什么都不用做的。不过对于没有和TileEntity相关联的GUI,比如工作台等,事情便并不是这样——你需要把其中的部分物品以掉落物的方式返给玩家。Container
类提供了一个名为onContainerClosed
的方法,我们直接覆写它就可以了:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
@Override
public void onContainerClosed(EntityPlayer playerIn)
{
super.onContainerClosed(playerIn);
if (playerIn.isServerWorld())
{
ItemStack goldStack = this.goldSlot.getStack();
if (goldStack != null)
{
playerIn.dropPlayerItemWithRandomChoice(goldStack, false);
this.goldSlot.putStack(null);
}
ItemStack emeraldStack = this.emeraldSlot.getStack();
if (emeraldStack != null)
{
playerIn.dropPlayerItemWithRandomChoice(emeraldStack, false);
this.emeraldSlot.putStack(null);
}
}
}
我们首先检查玩家是否处于服务端,因为客户端的数据会从服务端中同步得到:
if (playerIn.isServerWorld())
然后我们从两个需要掉落的物品栏中获取物品,并判断其是否为空:
ItemStack goldStack = this.goldSlot.getStack();
if (goldStack != null)
ItemStack emeraldStack = this.emeraldSlot.getStack();
if (emeraldStack != null)
如果非空,我们通过调用EntityPlayer
的dropPlayerItemWithRandomChoice
方法以生成掉落物,并把物品槽设置为空:
playerIn.dropPlayerItemWithRandomChoice(goldStack, false);
this.goldSlot.putStack(null);
playerIn.dropPlayerItemWithRandomChoice(emeraldStack, false);
this.emeraldSlot.putStack(null);
大功告成!
按下Shift后的物品槽
这里我们需要覆写的是transferStackInSlot
方法,在上一节中作者曾说过,如果想要什么都不做,这个方法必须返回null。在这一节我们详细讲一讲这一方法返回值的含义。
想象一下一个背包中每个格子里都有63块木炭,现在你在熔炉中烧好了半组木块,也就是生成了32块木炭,你想要按住Shift直接放入物品槽中,那么最后的结果是要有足足32个物品槽受到影响。
设计一种一劳永逸的方式去解决这个问题显然不够现实,不过更重要的是因为Mojang独特的设计方式——Minecraft采取的办法是先进行一步,也就是试着把一个ItemStack
的部分放入第一个可用的物品槽,如果没有可放入的物品槽则返回null,如果仍然可能有可放入的物品槽,则返回旧的ItemStack
。
一般情况下的transferStackInSlot
方法是这样子的:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
@Override
public ItemStack transferStackInSlot(EntityPlayer playerIn, int index)
{
Slot slot = inventorySlots.get(index);
if (slot == null || !slot.getHasStack())
{
return null;
}
ItemStack newStack = slot.getStack(), oldStack = newStack.copy();
boolean isMerged = false;
// TODO
if (!isMerged)
{
return null;
}
if (newStack.stackSize == 0)
{
slot.putStack(null);
}
else
{
slot.onSlotChanged();
}
slot.onPickupFromSlot(playerIn, newStack);
return oldStack;
}
我们首先获取对应的物品槽,然后判断对应的物品槽或者里面的物品是否存在,如果不存在,自然不需要放入其他的物品槽了:
Slot slot = inventorySlots.get(index);
if (slot == null || !slot.getHasStack())
{
return null;
}
然后我们获取到对应的ItemStack
,也就是newStack
,等待进一步处理,同时为了作为返回值,我们需要复制一份,也就是oldStack
。
// TODO
注释的部分就是真正尝试把物品的部分放入第一个可用的物品槽的部分,我们同时新添加了isMerged
布尔值用于判定是否被放入其他物品槽。接下来:
if (!isMerged)
{
return null;
}
如果所有情况下都没有将物品放入其他的物品槽,那么就代表没有可用的物品槽用于放入了,此时我们返回null。
如果成功尝试将物品放入了一个物品槽,那么我们就需要告知游戏,对应的物品槽产生了更新:
if (newStack.stackSize == 0)
{
slot.putStack(null);
}
else
{
slot.onSlotChanged();
}
因为Mojang独特的设计方式,有的物品槽还会覆写onPickupFromSlot
方法进行处理,为了对其进行支持,我们同时调用了这个方法:
slot.onPickupFromSlot(playerIn, newStack);
最后返回旧的ItemStack
:
return oldStack;
然后是// TODO
的部分:
src/main/java/com/github/ustc_zzzz/fmltutor/inventory/ContainerDemo.java(部分):
boolean isMerged = false;
if (index == 0 || index == 2)
{
isMerged = mergeItemStack(newStack, 4, 40, true);
}
else if (index >= 4 && index < 31)
{
isMerged = !goldSlot.getHasStack() && newStack.stackSize <= 16 && mergeItemStack(newStack, 0, 1, false)
|| !emeraldSlot.getHasStack() && mergeItemStack(newStack, 2, 3, false)
|| mergeItemStack(newStack, 31, 40, false);
}
else if (index >= 31 && index < 40)
{
isMerged = !goldSlot.getHasStack() && newStack.stackSize <= 16 && mergeItemStack(newStack, 0, 1, false)
|| !emeraldSlot.getHasStack() && mergeItemStack(newStack, 2, 3, false)
|| mergeItemStack(newStack, 4, 31, false);
}
if (!isMerged)
{
return null;
}
这里我们首先需要判定物品槽的序号,这个序号是什么呢?本节开头的一张图片已经告诉我们了:
- 0-3分别为GUI中4个物品槽的对应ID
- 4-30分别为GUI中玩家背包中的27个物品槽的对应ID
- 31-39分别为GUI中玩家背包中可直接使用的9个物品槽的对应ID
我们先从第一个代码块开始:
if (index == 0 || index == 2)
{
isMerged = mergeItemStack(newStack, 4, 40, true);
}
如果是金块和绿宝石块对应的物品槽,那么我们要调用Container
类的mergeItemStack
方法,以试图把物品的部分放入第一个可用的物品槽:
- 该方法的第一个参数传入想要更改的
ItemStack
- 该方法的第二个参数和第三个参数传入想要放入的物品槽的开始ID(包含)和结束ID(不包含),在这里也就是4-39,即玩家背包中的36个物品槽
- 该方法的最后一个参数传入是正向查找第一个可用物品槽(4-39),还是反向查找(39-4),当然如果等放入的物品槽只有一个(后面会遇到)那么这两者是没有区别的
对于正向还是反向查找,游戏有一个约定:通常情况下,如果从非玩家背包的GUI物品槽中试图将物品放入玩家物品槽,那么使用反向查找,如果从玩家背包中试图将物品放入玩家背包或非玩家背包物品槽,则使用正向查找。
mergeItemStack
方法的返回值用于标识是否成功把物品的部分放入了一个可用的物品槽,如果成功放入了,则返回真,否则返回假。
下面我们来看第二个代码块:
else if (index >= 4 && index < 31)
{
isMerged = !goldSlot.getHasStack() && newStack.stackSize <= 16 && mergeItemStack(newStack, 0, 1, false)
|| !emeraldSlot.getHasStack() && mergeItemStack(newStack, 2, 3, false)
|| mergeItemStack(newStack, 31, 40, false);
}
我们先试着把ID为4-30的27个物品槽中对应的物品槽中的物品放入金锭对应的ID为0的物品槽中,前提是该物品槽中没有物品,并且试图放入的不超过16个(!goldSlot.getHasStack() && newStack.stackSize <= 16
)。如果没有放入成功(执行||
后的语句),则试图放入绿宝石槽,同理,如果还是没有放入成功,则试图放入9个剩下的物品槽。
第三个代码块也是同理,此处不再赘述:
else if (index >= 31 && index < 40)
{
isMerged = !goldSlot.getHasStack() && newStack.stackSize <= 16 && mergeItemStack(newStack, 0, 1, false)
|| !emeraldSlot.getHasStack() && mergeItemStack(newStack, 2, 3, false)
|| mergeItemStack(newStack, 4, 31, false);
}
好了,本节的所有内容都已经讲完了,读者可以打开游戏试试了。