用户:MashKJo/1.12.2模组开发教程/10.BlockState系统及Forge BlockStates V1模型格式

MashKJo留言 | 贡献2024年12月22日 (日) 23:42的版本 →‎给方块附加属性:​ // Edit via Wikiplus

和Item一样,我们和世界中的方块进行交互时,实际上交互的也并非Block本身,而是它的某个方块状态(BlockState)。BlockState的存在允许我们为世界中实际存在的某个方块附加一些属性:一个或多个IProperty<T extends Comparable<T>>。方块状态对象都是接口IBlockState的实例。

很多教程都会说ItemStack和IBlockState是相似的,其实不然:一来,ItemStack的使用是随建随用,而IBlockState甚至一定程度上遵循享元设计模式;二来,同一种物品对应的ItemStack有无数多种,而同一种方块的方块状态种类是可以被穷尽的。

原版中有很多多BlockState的方块,如熔炉就有8种方块状态——朝向有4种,工作状态2种:熄灭/燃烧,乘起来就是8种。

在本节教程中,笔者将完整地讲述创建一个多BlockState方块的全过程。

给方块附加属性

首先我们需要在对应的方块类中添加相应的静态IProperty<T extends Comparable<T>>字段。实际上我们完全不需要重新实现这个接口,用Minecraft提供的一系列默认实现就好了。默认实现一共有3种:PropertyInteger、PropertyBool和PropertyEnum<T extends Enum & IStringSerializable>。顾名思义,它们分别用于描述整型、布尔型和枚举型的属性。值得注意的是,它们的构造方法都是protected修饰的,那么我们如何构造它们呢?答案是利用它们的静态方法create。

3种IProperty的默认实现类的对象的构造,都要求一个String参数,用于设定该属性的名称,这个参数只能由小写字母和下划线组成,我们会在方块的blockstates JSON文件中看到它的身影。对于PropertyInteger,我们还需要指定该属性的最大值和最小值。对于PropertyEnum,我们需要一个实现了IStringSerializable接口的枚举类(这个接口只有一个方法:String getName(),也是为blockstates JSON文件服务的);该属性会默认接受该枚举类的所有枚举值,但我们也可以显式指定哪些值会被允许,如传入一个数组或Collection,甚至一个Predicate,都是可以的。

此外,PropertyEnum还有一个针对方块朝向的特化版:PropertyDirection,本质上是一个PropertyEnum<EnumFacing>。实际上关于这个类,你可以找到一些可以直接拿来用的公开字段,如BlockHorizontal.FACING,这个属性只允许水平方向上的EnumFacing——即东、南、西、北四个方向。

弄好了你所需要的的IProperty字段后,就要通过覆写Block类下的一个方法来告知Minecraft方块状态系统:这个方块的BlockState可以包含这些IProperty:

@Override
public BlockStateContainer createBlockState(){
    return new BlockStateContainer(this, property1, property2, ...);
}

BlockStateContainer类构造方法中有个变长参数,以用于传入我们声明好的IProperty。实际上这个方法的返回值,会在Block类构造方法中,被传入一个名为blockState的字段。

然后我们就该声明该方块的默认方块状态了(Default BlockState),通过setDefaultState来实现。对于BlockState中没有IProperty的方块,方块状态只有一种,因此我们也不需要设定默认的方块状态了——Minecraft已经帮我们设定好了。但是对于多BlockState方块,我们必须手动设定。

那有读者就问了:我们如何能拿到一个带有特定属性且属性值被显式指定的一个IBlockState呢?所以首先我们先来讲讲IBlockState中的一些常用方法。

说是一些,其实也就三个,其中有两个是修改属性值的:withProperty和cycleProperty。前者用于显式指定某个IBlockState中的某一属性的值,后者用于循环某个IBlockState中某一属性所有可能的值。最常用的方法是withProperty。两个方法均符合链式调用规则。此外还有一个:getValue,其作用显而易见。

然后我们就可以设定该方块默认的方块状态了:

this.setDefaultState(this.blockState.getBaseState().withProperty(property1, defaultValue1).withProperty(property2, defaultValue2)....;

然后Block类下的protected字段:defaultState就会把传入的这个IBlockState缓存下来,需要的时候用getDefaultState获取即可。所以我们可以看到,一个方块的默认状态,也是满足重复利用实例的原则的。但是非默认的方块状态便不一样了,用到它们的时候一般是在defaultState的基础上再次withProperty或者cycleProperty,因此其他方块状态,便不能用==运算符号判断是否相等了——只有默认的方块状态可以这么做。

在什么情况下,给方块设定什么样的状态,都是没有定式的,全看你自己的需求。但特别地,如果你的方块有一个PropertyDirection属性,且你希望这个属性随着玩家放置方块时的的朝向变化,那么一般你只需要覆写Block#getStateForPlacement方法即可,具体的实现如何,可以去参考原版的一些有方向属性的方块在这一块的代码。

那么,工作是不是已经做完了呢?当然没有,我们毕竟还要写相应的JSON文件嘛,不过读者可能会觉得,仅仅是为了测试的话,没必要立刻就写JSON文件,可以直接顶着紫黑块的压力,按F3查看它的属性值!好的,这么做确实可以,然而注册完方块,runClient之后读者会发现代码根本跑不起来,会崩溃——且抛出一个IllegalArgumentException:"Don't know how to convert [state] back into data..."。

好的,所以笔者还需要讲一个很重要的概念:方块的meta值。是不是很眼熟?ItemStack就有meta值啊。只是在ItemStack中,meta值兼任损害值和类型序数两个概念,由hasSubtypes字段控制它的具体含义。但对于方块而言,没损害值这一说,所以方块的meta值就代表它的一个类型。这么说,方块的meta岂不是和它的BlockState作用重合了?是这样的,在Minecraft 1.8版本之前,BlockState系统还不存在,那时候方块被附加上不同属性后得到的“状态”就是用方块的meta值来描述的——玩家在世界中和方块交互时,以及Minecraft内部机制序列化、反序列化地图时,用的都是方块的meta值。但BlockState系统加入后,meta值便只被用于序列化、反序列化存档地图了。显然我们需要给一个方块的所有BlockState都各自指定一个meta值,否则Minecraft根本就不知道如何序列化或反序列化这种多BlockState方块。我们覆写Block#getStateFromMetaBlock#getMetaFromState这两个方法,即可,runClient后,游戏便能正常进入了。

和ItemStack不同,方块的meta值只能是0 - 15这个范围内的整数——最多只有16种可能;而在99%的情况下,所有BlockState和所有meta值之间的关系都是一一对应的,因此通常情况下,一个方块的BlockState最多只有16种。但也有例外——比如栅栏、红石粉、火焰这些方块,它们实际上有着成百上千种BlockState,那么这是如何做到的呢?它们实际上都只指定了部分BlockState的meta值,同时覆写了Block#getActualState方法,该方法能根据周围的方块的方块状态,计算出它本身该是什么方块,这一部分算出来的BlockState实际上是不参与序列化和反序列化的。这个手段实际上很有局限性,为何?能这么做的方块,它的特点也很明确:它本身的BlockState确实会随着它周围的方块的BlockState的变化而变化,那对于箱子这种能存储物品(实际上是ItemStack)的方块呢?就没辙了,毕竟你总不可能不打开箱子,根据它周围的方块状态就确定它里面装的到底都有啥吧(笑),而且因为ItemStack本身有附加NBT,因此可以这么说,同一种物品的ItemStack有无限种可能。所以,指望着BlockState系统来完成这件事是没辙了。

那有没有解决办法呢?废话,当然有。解决方案是给世界上的每一个箱子方块都创建一个方块实体(TileEntity),这会在之后的教程中讲到,TileEntity并不是享元对象,因此我们可以通过给对应的TileEntity类追加实例变量的方式来存储理论上无限的数据。一个方块拥有了TileEntity不意味着它就不需要多种方块状态了,实际上箱子的朝向这种信息还是通过IProperty字段来定义的。TileEntity也有其缺点:内存和性能开销比较大。

除非你真的在写类似于原版的栅栏、红石粉这些方块的方块,否则不要用getActualState,它实际上是个hack,很不好用,也很复杂。该用TileEntity就用TileEntity。另外,不是所有方块的属性都必须以IProperty的形式呈现。如:箱子有普通材质和圣诞节材质两种材质,这难道也应该是一个PropertyBool吗?大错特错。实际上这个是在相关的渲染代码中根据当前系统时间被临时判断出来的,因为这个真的只影响视觉,不影响任何(反)序列化过程、游戏逻辑。只有当方块的某个特性影响到了实际的游戏逻辑,或造成了存档内数据的改变,该特性才应该被作为一个IProperty对待。否则,你只需临时作个判断就好了。

模型文件的书写