天天动画片 > 八卦谈 > Forge模组开发-容器类详解

Forge模组开发-容器类详解

八卦谈 佚名 2023-09-11 07:10:20


引言


首先仍是了解什么是容器类,容器并非物品栏,而是将物品栏呈现给玩家的一个工具:

注册容器:例如ContainerType<ChestContainer> GENERIC_9X1这是箱子容器类型,也就是上面9个格子,下面玩家背包的容器类型,使用容器类ChestContainer注册生成用以判断类别和参与游戏功能;

容器类:是用于生成容器实例的类,例如,ChestContainer extends Container,容器实例最主要功能是操作物品栏(IInventory)物品增删改查,将物品栏以界面形式展现在玩家面前方便操作;

物品栏:实现了IInventory接口的类所生成的实例,是真正储存ItemStack对象的地方,容器类操作的就是该对象。

 

本文要介绍的内容包括Container类基本要求和有用内容、Slot类基本要求和有用内容,IInventory的某些典型实现的分析,在后续讲完TileEntity后,我会讲出他们之间的关系以及为什么Inventory是接口。

 

Container类


构造器:protected Container(@Nullable ContainerType<?> type, int id)

容器类构造器内容很少,但不意味着我们继承后写的构造器也这么简单,我们的构造器需要至少两个,且满足几个步骤:

 

第一个构造器,称为注册构造器,CustomContainer(int id, PlayerInventory playerInventory),传入id、玩家物品栏,意味着这两个参数必需需要我们外部获取,其内部功能只需要调用第二个构造器,将第二个构造器需要的参数合理new出即可,不需要有实际意义。

 

第二个构造器,称为实例构造器,CustomContainer(int id, PlayerInventory playerInventory , IInventory inventory, IIntArray intarry):

第一步,super调用父类构造器,也就是上述构造器,将注册容器type、分配的注册id传入,分别写入对应成员变量,所以,我们的构造器需要从容器类型注册类获取用我们这个容器类注册的容器类型type,看起来有点首尾衔接死循环了,实际上是分部分错开使用的,故而并不会出错,不必深究,在注册解析区会进行详细说明,而第二个参数id则是由Forge容器管理系统分配的,也不必深究,此处只需知道我们使用时都可以获得即可;

第二步,校验形参合法性,例如assertInventorySize(inventory, 8);意思是校验传入的物品栏inventory是否至少有8个位置可以存放物品,除了前三个基本形参外,还可以传入如intarry这样数字数组类型的信息对象的额外信息参数,方便实现更多功能,同样,都需要校验合理性;

第三步,将校验好的物品栏对象、数字数组信息对象放入我们写的对应的成员变量中例如private final IIntArray data;与this.data = intarry;;

第四步,由于forge(mojang?)只提供了IIntArray 的同步器,所以额外信息对象一般是数字类型的,所以我们第三步是将该信息对象所在的成员变量使用trackIntArray(this.data);开始进行同步;

第五步,也是格外重要一步,为容器添加物品槽,物品槽构造参数分为(物品栏,物品栏索引物品,x、y在容器的相对位置,其他需要的参数...),后面细讲物品槽;

 

这两个构造器何时使用我们在讲方块实体和容器注册时会说明。

 

必要覆写方法


ItemStack transferStackInSlot(@NotNull PlayerEntity playerIn, int index)

在玩家使用该容器在背包inventory和目标inventory之间转移物品时调用,例如箱子界面中,shift点击上方物品槽,该物品槽内物品应当执行怎样的移动逻辑,查看ChestContainer的该方法可以轻易了解如何编写,注意实际发生物品转移后,要记得将对应inventory标记为待更新;

boolean canInteractWith(@NotNull PlayerEntity playerIn)

根据需要不同,重写此方法,可以借此上容器锁,也可以实现其他用处;

 

非必要但覆写有用方法


onContainerClosed(PlayerEntity playerIn)

在容器关闭时执行的方法,重写时应当注意要super调用父方法,防止正常游戏线出现问题

onCraftMatrixChanged(IInventory inventoryIn)

容器内出现合成时间后调用,需要检测变化,执行对应功能并同步数据

calcRedstone(@Nullable TileEntity te)与calcRedstoneFromInventory(@Nullable IInventory inv)

红石比较器比较容器时调用,至于为什么第一个传入了方块实体,讲到方块实体再说

 

还有其他能重写的方法,但是用处不大,需要自行研究吧。

 

Slot类


构造器:public Slot(IInventory inventoryIn, int index, int xPosition, int yPosition)

this.inventory = inventoryIn;

this.slotIndex = index;

this.xPos = xPosition;

this.yPos = yPosition;

没啥好讲,初始化参数,对应容器类构造器二的第五步,每个物品槽都要具体设置,不论是循环还是其他方法。一般情况下一个槽对应一个物品栏inventory的一个索引位和索引位内的非空物品(AIR空气也是物品)。

 

建议重写方法


boolean isItemValid(ItemStack stack)

返回true,代表传入的stack允许置入,例如容器的产物槽,不论输入什么都会返回false

ItemStack onTake(PlayerEntity thePlayer, ItemStack stack)

在取物品后执行的方法,一般用来写统计信息,例如获得了xxx物品数量

int getSlotStackLimit()

槽位堆叠限制,与物品栏堆叠限制同时作用

boolean canTakeStack(PlayerEntity playerIn)

是否能从槽位取走物品,例如创造物品栏的物品就无法取出

 

物品槽最实用的方法是isItemValid,而且槽位实例允许你储存一些信息,例如我在CustomSlot构造器中增加一个形参Item item,在容器构造器中指定为Items.BOOK,然后在该方法中判断传入的物品是否是构造器中传入的item,来判断能不能塞入;

 

所以我们可以看到,容器构造器二的第五步是例如箱子:

for(int i1 = 0; i1 < 9; ++i1) {

   this.addSlot(new Slot(playerInventoryIn, i1, 8 + i1 * 18, 161 + i));

}

使用了一个循环,将玩家背包的手持物品栏(九个)分别绑定到了九个new Slot上。

 

IInventory类


为了统一调用,提供了一个IInventory接口供实现物品栏类,其中最典型的实现则是Inventory,但是此处并不是讲的Inventory,而是IInventory;

接口自然没有什么构造器要讲,但接口方法也就要求了具体实现的构造器的功能;

 

接口方法


int getSizeInventory();

获取容器大小,一般对应物品栏实际储存物品的非空物品堆list大小

boolean isEmpty();

物品栏是否为空

ItemStack getStackInSlot(int index);

获取索物品栏引位置物品堆

ItemStack decrStackSize(int index, int count);

将槽位中的物品堆分堆,形成两个ItemStack实例,返回需要的实例,一般调用ItemStackHelper.getAndSplit方法即可

ItemStack removeStackFromSlot(int index)

从物品栏索引位index移除物品堆,返回该被移除的物品堆

setInventorySlotContents(int index, @NotNull ItemStack stack)

设置物品栏对应索引位index的物品堆为stack

int getInventoryStackLimit()

返回物品栏的每个物品堆最大堆叠

void markDirty();

标记为待更新,用于从服务端同步数据到客户端,需要自行在合适位置调用,防止更新不及时导致客户端动效或者信息不更新或者滞后

boolean isUsableByPlayer(PlayerEntity player);

是否可用,建议不在这写,当然根据自己需要写

void openInventory

并非是真正的打开物品栏方法,物品栏其实是打不开的,只能借助容器才能“打开”,这个方法是用来执行一些信息统计、改变、更新,调起对应事件用的。

void closeInventory(PlayerEntity player)

同上,应当逆序打开的信息改变或者自行设计

boolean isItemValidForSlot(int index, ItemStack stack)

后检测,在slot校验合法通过过还要执行此方法校验,判断要存入的物品是否能存入,到此部分,一般是在塞入物品关闭容器后扔出物品而不是点击不进去物品

int count(Item itemIn)

查询某类型物品数量

boolean hasAny(Set<Item> set)

是否有某类型物品数量

 

为什么将IInventory设计为接口,是为了任何实现他的实例都可以被认作物品栏,故而很多时候,会让方块实体实现IInventory接口,这样他既是一个功能实体,又可以很方便地储存、调用想要的物品实现他的功能,方块实体章节细讲;

 

Inventory则是IInventory的标准实现,如果没有额外需求,直接new出一个该对象即可新建一个物品栏,也可以研究他的具体方法实现,来编写自己的物品栏,为什么不讲其他实现的物品栏类型呢,因为我们发现,物品栏一般是依附方块或者实体的,单独将属实没意义,所以我们后续再讲。

 

有关容器注册


从讲容器构造器得知我们写了两个构造器,一个是注册用,一个是实例生成用,显然,我们这时候要用构造器一,这时候该构造器只是创建了一个空壳容器对象。

第一步 仍然要新建一个容器注册类,写一静态成员:

public static final DeferredRegister<ContainerType<?>> CONTAINERS = DeferredRegister.create(ForgeRegistries.CONTAINERS,MODID);

第二步 进行注册

public static final RegistryObject<ContainerType<容器类>> 容器类型名称=

CONTAINERS.register("类型键名", () ->

            IForgeContainerType.create(

                    (int windowId, PlayerInventory inv, PacketBuffer data) ->{

                        return new 容器类(windowId, inv);

}));

其中windowId是分配好的数字id,可以用此让服务端发包,指挥客户端打开界面,方便同步两端写入物品栏数据并展示,inv是留空的玩家物品栏,容器的打开者必然是一个玩家在操作,物品栏是要传入,但是你也可以不用,用的话例如箱子,将玩家物品栏渲染在下方,不用的话例如自己写,data按道理是服务端发送打开容器包时附带的信息,例如容器位置啊等,实际上哥们没使用成功过,所以哥们本人不会,但是吧,你可以自己写网络通信代替这部分,效果是一样的(亲测可行)。

 

有关容器使用


使用是建立在注册成功上的,我在这先只讲虚空弹出容器界面的方法,至于该在什么地方弹出,我会在方块实体部分讲一种用法,这只讲怎么弹;

首先起点是服务端代码,你要有的参数是:服务端玩家对象、要打开的容器实例

仅此二者即可,其中服务端玩家对象就是对应的要打开容器的玩家,看到该界面的玩家就是对应的客户端玩家实例,这个很容易理解,那么要打开的容器实例看下面;

要打开的容器实例需要new出,使用的则是我们写的第二个构造器,构造参要求的id先不说,玩家物品栏,自然是传入该玩家或者你想的话其他玩家也行;物品栏,例如我获取了一个箱子的物品栏(虚空)就可以传入;数据,自行发挥此处示例不传。

这样我们就有了一个有意义的容器,我们肯定要包装我们自己的打开物品栏方法的:

public void openExtraInventory(ServerPlayerEntity spe,IInventory inv) {

if (spe.openContainer != spe.container) {

     spe.closeScreen();

}

     spe.getNextWindowId();

     spe.openContainer = new CustomChestContainer(spe.currentWindowId, spe.inventory, inv);

     spe.connection.sendPacket(new SOpenWindowPacket(spe.currentWindowId, ContainerTypeRegistry.customchestContainer.get(), new TranslationTextComponent("额外背包")));

     spe.sendContainerToPlayer(spe.openContainer);

}

上述方法,步骤分为:

界面检测,如果有其他界面,进行关闭

窗口Id自增,表示新的窗口要来了,方便同步数据

设置服务端玩家要打开的容器,容器是new出的,这时候我们就有了刚刚没讲的id了

获取玩家链接向客户端玩家发送打开窗口包,指定id,容器类型,以及我们预设好的容器界面的文字框内展示的文字

然后立马将对应容器实例发过去,同步数据,该方法有两个数据包,分别是物品堆信息和槽位信息,这样我们发现,我们发过去的三个包很契合我们本文讲的三个内容,容器、物品栏、槽位,至此,方法结束,本文也就结束了。

 

至于这个方法什么时候调用,调用限制,这都属于自由发挥,等cv模组策划完善后,或许出一节额外背包教程,读者就会了解到安全的额外背包了。

 


本文标题:Forge模组开发-容器类详解 - 八卦谈
本文地址:www.ttdhp.com/article/38899.html

天天动画片声明:登载此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述。
扫码关注我们