又谈工作台类型的设计方法(1.12.2FML)


  • TIS成员

    再次强烈谴责B站专栏没法贴代码块的功能

    最近也有需求,需要设计一个类似原版工作台的方块“注魔台”,支持在消耗一些素材的情况下合成一些物品。在参考了异形龙虾的专栏 cv3031327 和他的一些代码了以后,表示完全搞不懂。后来在铁砧的代码里却成功寻得了解决方案。

    这篇文章虽然较长,实际信息量应该对了解 Java 的人来说并不大;对深谙 Forge 的各种操作的 Modder 来说更是小草一碟。发在这里权当抛砖引玉了。


    因为工作台合成这个东西太庞杂了,不知道怎么看起,决定对需求重新抽象:

    • 方块本身不保存界面中的东西,退出界面后剩余物品自动返回背包
    • 输入物品,输出栏位显示另一个物品,但只有把输出的物品拿走的时候,才会消耗输入的物品
    • 有一定的配方
      根据这些抽象结果,铁砧比工作台看起来更加贴近要求。那么接下来对铁砧的代码进行研究,抽取有用的部分用在自己的模组里。

    铁砧主要由三个类组成:

    • BlockAnvil 负责方块本体各种属性(如透明底,碰撞箱)的配置,
    • ContainerRepair是负责对界面中物品的储存和逻辑判断,
    • GuiRepair负责对客户端的 GUI 进行渲染。

    首先从方块类看起。BlockAnvil重写了onBlockActivated方法,对玩家右键的操作进行响应:

    /**
     * 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 编写中,应该使用 forge 给我们提供的方法openGui()而不是displayGui(),另外,GUI 应该被注册好了,具体注册方法别处都有,恕不赘述。

    方块部分就是这样,很简单,不涉及任何 TileEntity(方块实体)。接下来看逻辑的重中之重ContainerRepair。在类的初始化方法中,可以看到初始化了两个栏位:一个二格的输入和一个一格的输出,对应铁砧的输入输出:

    this.outputSlot = new InventoryCraftResult();
    this.inputSlots = new InventoryBasic("Repair", true, 2) {
        public void markDirty() {
            super.markDirty();
            ContainerRepair.this.onCraftMatrixChanged(this);
        }
    };
    

    这里使用匿名类的方法,重写了输入栏位的markDirty(),让它调用onCraftMatrixChanged()方法。markDirty()方法会在栏位发生任何变化的时候调用,提醒游戏保存一下。

    那么接着看onCraftMatrixChanged()这个方法,它也被重写了。如果输入栏位被改变,就调用一个更新输出栏位的方法:

    public void onCraftMatrixChanged(IInventory inventoryIn) {
        super.onCraftMatrixChanged(inventoryIn);
    
        if (inventoryIn == this.inputSlots) {
            this.updateRepairOutput();
        }
    }
    

    简而言之,通过重写方法,所有输入栏位的变化都会更新输出栏位


    这样一来具体的实现就变得很简单了,另外写一个类,用HashMap统一管理配方,要获得输出就直接从里面查询。记得用ItemStack.areItemsEqual()而不是双等于进行比较。获取到的产物要用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){/**一些重写代码**/});
    

    但是实际情况下,设置成0,1,2会有下标越界错误,不太清楚,但是都改成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){/**一些重写代码**/});
    

    接下来别忘了把玩家背包的栏位也添加进去,这部分代码直接抄就好:

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

    然后在一个统一的方法进行输出栏位的更新,然后搞一个 GUI 的渲染。


    接下来处理一些细节,首先是输出栏位不能放入东西,只能在创造模式或者等级够的时候拿出输出栏位的物品,拿出输出栏位物品会减少输入栏位的物品。这些逻辑在添加栏位的时候重写已有的方法就好,还是拿铁砧举例:

     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()做到
            //一堆复杂的东西,具体就是用玩家的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()就好


    从这篇文章可以看出,大部分玩家想要做出的功能,都可以在原版中找到对应。只要了解 Java,能阅读代码,就能做出不少东西。如果要做背包,那么可以参考末影箱(数据较多,要单独存放)或者潜影盒(数据不多,可以存放在背包的NBT里)。如果要做 GUI 里面的按钮,可以参考附魔台或者信标。要对周围方块实时监测,信标或者附魔台也是不错的例子……


 

友情链接

魔茶国际
Powered by TIS