又談工作臺類型的設(shè)計方法(1.12.2FML)

再次強烈譴責(zé)專欄沒法貼代碼塊的功能
前往TIS論壇,感受更好的 Markdown?渲染體驗:https://forum.tis.world/topic/137/又談工作臺類型的設(shè)計方法-1-12-2FML
啟
最近也有需求,需要設(shè)計一個類似原版工作臺的方塊“注魔臺”,支持在消耗一些素材的情況下合成一些物品。在參考了?異形龍蝦?的專欄 CV3031327 和他的一些代碼了以后,表示完全搞不懂。后來轉(zhuǎn)而在鐵砧的代碼里尋得了解決方案。

這篇文章雖然較長,實際信息量應(yīng)該對了解 Java 的人來說并不大;對深諳 Forge 的各種操作的 Modder 來說更是小菜一碟。部分代碼自己還未完全吃透,發(fā)在這里權(quán)當(dāng)拋磚引玉了。

因為工作臺合成這個東西太龐雜了,不知道怎么看起,決定對需求重新抽象:
方塊本身不保存界面中的東西,退出界面后剩余物品自動返回背包
輸入物品,輸出欄位顯示另一個物品,但只有把輸出的物品拿走的時候,才會消耗輸入的物品
有一定的配方 根據(jù)這些抽象結(jié)果,鐵砧比工作臺看起來更加貼近要求。那么接下來對鐵砧的代碼進(jìn)行研究,抽取有用的部分用在自己的模組里。
鐵砧主要由三個類組成:
BlockAnvil
負(fù)責(zé)方塊本體各種屬性(如透明底,碰撞箱)的配置,ContainerRepair
是負(fù)責(zé)對界面中物品的儲存和邏輯判斷,GuiRepair
負(fù)責(zé)對客戶端的 GUI 進(jìn)行渲染。
首先從方塊類看起。BlockAnvil
重寫了onBlockActivated
方法,對玩家右鍵的操作進(jìn)行響應(yīng):
/**
* Called when the block is right clicked by a player.
*/?
public boolean onBlockActivated(World worldIn, BlockPos pos, IBlockState state, EntityPlayer playerIn, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) {
? ?if (!worldIn.isRemote) {
? ? ? ?playerIn.displayGui(new BlockAnvil.Anvil(worldIn, pos));
? ?}
? ?return true;?
}
這里使用了一個額外的類Anvil
來顯示 GUI ,但是在實際的 mod 編寫中,應(yīng)該使用 forge 給我們提供的方法openGui()
而不是displayGui()
,另外,GUI 應(yīng)該被注冊好了,具體注冊方法別處都有,恕不贅述。
方塊部分就是這樣,很簡單,不涉及任何 TileEntity(方塊實體)。接下來看邏輯的重中之重ContainerRepair
。在類的初始化方法中,可以看到初始化了兩個欄位:一個二格的輸入和一個一格的輸出,對應(yīng)鐵砧的輸入輸出:
this.outputSlot = new InventoryCraftResult();?
this.inputSlots = new InventoryBasic("Repair", true, 2) {
? ?public void markDirty() {
? ? ? ?super.markDirty();
? ? ? ?ContainerRepair.this.onCraftMatrixChanged(this);
? ?}
};
這里使用匿名類的方法,重寫了輸入欄位的markDirty()
,讓它調(diào)用onCraftMatrixChanged()
方法。markDirty()
方法會在欄位發(fā)生任何變化的時候調(diào)用,提醒游戲保存一下。
那么接著看onCraftMatrixChanged()
這個方法,它也被重寫了。如果輸入欄位被改變,就調(diào)用一個更新輸出欄位的方法:
public void onCraftMatrixChanged(IInventory inventoryIn) {
? ?super.onCraftMatrixChanged(inventoryIn);
? ?if (inventoryIn == this.inputSlots) {
? ? ? ?this.updateRepairOutput();
? ?}?
}
簡而言之,通過重寫方法,所有輸入欄位的變化都會更新輸出欄位

這樣一來具體的實現(xiàn)就變得很簡單了,另外寫一個類,用HashMap
統(tǒng)一管理配方,要獲得輸出就直接從里面查詢。記得用ItemStack.areItemsEqual()
而不是雙等于進(jìn)行比較。獲取到的產(chǎn)物要用copy()
來防止引用問題,修改到不該修改的東西。
將欄位加入容器的過程,Minecraft原本的是:
this.addSlotToContainer(new Slot(this.inputSlots, 0, 27, 47));
this.addSlotToContainer(new Slot(this.inputSlots, 1, 76, 47));?
this.addSlotToContainer(new Slot(this.outputSlot, 2, 134, 47){/**一些重寫代碼**/});
但是實際情況下,設(shè)置成0,1,2會有下標(biāo)越界錯誤,不太清楚,但是都改成0就好了:
this.addSlotToContainer(new Slot(this.powderSlot, 0, 80, 53) {/**一些重寫代碼**/});?
this.addSlotToContainer(new Slot(this.inputSlot, 0, 48, 26));?
this.addSlotToContainer(new Slot(this.outputSlot, 0, 111, 26){/**一些重寫代碼**/});
接下來別忘了把玩家背包的欄位也添加進(jìn)去,這部分代碼直接抄就好:
for (int i = 0; i < 3; ++i) {
? ?for (int j = 0; j < 9; ++j) {
? ? ? ?this.addSlotToContainer(new Slot(playerInventory, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
? ?}
}?
for (int k = 0; k < 9; ++k) {
? ?this.addSlotToContainer(new Slot(playerInventory, k, 8 + k * 18, 142));?
}
然后在一個統(tǒng)一的方法進(jìn)行輸出欄位的更新,然后搞一個 GUI 的渲染。

接下來處理一些細(xì)節(jié),首先是輸出欄位不能放入東西,只能在創(chuàng)造模式或者等級夠的時候拿出輸出欄位的物品,拿出輸出欄位物品會減少輸入欄位的物品。這些邏輯在添加欄位的時候重寫已有的方法就好,還是拿鐵砧舉例:
this.addSlotToContainer(new Slot(this.outputSlot, 2, 134, 47) {
?? ?public boolean isItemValid(ItemStack stack) {
?? ? ? ?return false;
? ?}
? ?public boolean canTakeStack(EntityPlayer playerIn) {
? ? ? ?return (playerIn.capabilities.isCreativeMode || playerIn.experienceLevel >= ContainerRepair.this.maximumCost) && ContainerRepair.this.maximumCost > 0 && this.getHasStack();
? ?}
?? public ItemStack onTake(EntityPlayer thePlayer, ItemStack stack){
? ? ? ?if (!thePlayer.capabilities.isCreativeMode) {
? ? ? ? ? ?thePlayer.addExperienceLevel(-ContainerRepair.this.maximumCost);
? ? ? ?}
? ? ? ?//減少輸入物品可以用setInventorySlotContents()或者decrStackSize()做到
? ? ? ?//一堆復(fù)雜的東西,具體就是用玩家的rng計算鐵砧會不會壞,還有播放音效之類的。各位rng大法師可以試試操控
?? ? ? ?return stack;
?? ?}
});
如果要控制輸入欄位只能塞入特定的物品,也是重寫isItemValid
方法。
在退出界面時自動把欄位的物品彈到玩家身上或地上:
public void onContainerClosed(EntityPlayer playerIn) {
? ?super.onContainerClosed(playerIn);
? ?if (!this.world.isRemote) {
? ? ? ?this.clearContainer(playerIn, this.world, this.inputSlots);
? ?}
}
玩家用SHIFT點擊的情況需要特殊處理,不過這里全部照抄鐵砧的transferStackInSlot()
就好
成品展示
感謝?XeKr?做的材質(zhì)和模型,XKnb!。

我給注魔臺上面的蛋糕模型做了一個旋轉(zhuǎn)+起伏的效果,這里是需要 TileEntity 和 TileEntitySpecialRenderer?的


結(jié)
從這篇文章可以看出,大部分玩家想要做出的功能,都可以在原版中找到對應(yīng)。只要了解 Java,能閱讀代碼,就能做出不少東西。如果要做背包,那么可以參考末影箱(數(shù)據(jù)較多,要單獨存放)或者潛影盒(數(shù)據(jù)不多,可以存放在背包的NBT里)。如果要做 GUI 里面的按鈕,可以參考附魔臺或者信標(biāo)。要對周圍方塊實時監(jiān)測,信標(biāo)或者附魔臺也是不錯的例子……