2.2 Block 和 BlockState
Part A. 实战
实际上,和 Item
与 ItemStack
的关系一样,Block
并不在游戏中出现,它是以 BlockState
方式存在。
这样的处理不仅可以加速渲染(适当利用享元模式),而且可以保存比 Block
更多的实际信息,例如状态信息。一般的栅栏的连接 状态、火把的朝向、楼梯的摆放状态,等等;
注:一个简单方块通常没有状态,例如上一节我们定义的
obsidian_block
就只有唯一一个状态,或者一个圆石,也只有一个状态。
你可能会问,BlockState
只有与 ItemStack
一样的用途吗?并不是,BlockState
还能够控制方块的模型、材质贴图。下面就在创建一个方块的例子中介绍一下:
2.2.A.1 BlockState
与模型/材质
一般情况下,我们在定义方块模型材质时需要创建如下目录:
resources/
├── META-INF/
│ └── mods.toml
├── assets/
│ ├── mymod/
│ | ├── models/
│ | ├── textures/
│ | ├── blockstates/ # 还需要指定方块状态与模型、贴图的映射关系
| | └── lang/
| |
| └── minecraft/
|
└── pack.mcmeta
在 blockstates
目录中,创建一个与注册方块名称同名的 JSON 文件:
{
"variants": {
"": { "model": "mymod:block/stateful_block_model" }
}
}
并且在 models/block
目录中,创建名字和上面 blockstates
中文件指定名称 stateful_block_model.json
的 JSON 文件表示模型:
{
"parent": "block/cube_all",
"textures": {
"all": "mymod:block/stateful_block_texture"
}
}
block/cube_all
的父级模型是内置模型;"texture" - "all"
表示每个面使用相同材质(32 x 32);
然后在 textures/block
目录下存放名为 stateful_block_texture.png
(上面文件中规定的名称)即可;
不过别忘记我们曾经创建过方块类对应的 item(BlockItem
),我们还需要为这个 item 准备材质。
不过因为 BlockItem
的特殊性,它的模型可以由对应的 block 导出,因此我们不仅可以重用材质,而且模型文件也不需要指定渲染层(layer
):
{
"parent": "mymod:block/stateful_block_model"
}
把它定义在 item 中,文件名与 BlockItem
的注册名相同、模型使用方块的模型即可。
我们再回到前面,blockstates
目录下的 JSON 文件是如何将方块状态映射到不同的模型(以及不同的材质)上的呢?
{
"variants": {
"": { "model": "mymod:block/stateful_block_model" }
}
}
上面这种情况就是 ""
(任何状态)都对应模型 stateful_block_model.json
;
如果 block states JSON 这么写:
{
"variants": {
"face=0": { "model": "mymod:block/stateful_block_model_0" },
"face=1": { "model": "mymod:block/stateful_block_model_1" }
}
}
就是指,当方块状态 face
值为 0 的时候,方块使用 stateful_block_model_0.json
模型及其配套材质;当方块状态 face
值为 1 的时候同理。
这下我们就明白了方块状态可以控制方块的模型以及材质。
注:Block State JSON、Model JSON、Item JSON 都可以用类似 Block Bench、MCreator 的工具定义。
2.2.A.2 定义一个 Stateful Block
上一节的例子中,我们知道如何按照方块状态设定方块材质、模型,现在我们了解一下 如何为一个方块定义状态变量。
我们以状态变量为整型的情况举例:
public class StatefulBlock extends Block {
// 建议使用 public,便于外部 inspect
public static final IntegerProperty STATE = IntegerProperty.create("face", 0, 1);
public StatefulBlock() {
super(Properties.create(Material.ROCK).hardnessAndResistance(5));
this.setDefaultState(this.stateContainer.getBaseState().with(STATE, 1));
// 36.2.42 中应该这么写
// super(Properties.of(Material.STONE).strength(5));
// this.registerDefaultState(this.getStateDefinition().any().setValue(STATE, 1));
}
@Override
protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder) {
builder.add(STATE);
super.fillStateContainer(builder);
}
// 36.2.42 中是下面的方法:
// @Override
// protected void createBlockStateDefinition(StateContainer.Builder<Block, BlockState> builder) {
// builder.add(STATE);
// super.createBlockStateDefinition(builder);
// }
}
和普通方块不一致的是,首先使用 IntegerProperty
的工厂方法定义了私有成员做状态变量(指定最大、最小值);
然后构造函数调用了父类定义的 this.stateContainer
,通过 getBaseState()
拿到 container 中的状态,并且使用 .with()
设置,最终将新的状态设置为默认状态。
此外,我们重写了父类的 fillStateContainer
,作用是在填充 StateContainer
的构建器中,在父类的基础上新添了自定义的状态。
然后,其他所有的代码定义都和普通 block 一致了(注册 block 和对应的 block item)。
Part B. 理论
2.2.B.1 BlockState
的历史
实际上,在 forge 36.2.34 版本(MC 1.16.5)时,对 BlockState
的接口包装比较粗糙,相当多的参数名和属性都是被混淆的状态。直到 MC 1.20+ 后才有所好转。不过幸运的是,我们在使用方块状态的时候也不需要非常了解所有的接口,只需要部分重要接口就能满足绝大部分的需求。
想要理解为什么要有 BlockState
、它的意义、它的最佳实践,我们先谈谈 BlockState
的历史。
在 MC 1.7 或以前的版本,方块是使用一个叫 metadata
(元数据)的属性来存储方块的位置信息、其他状态信息。这个 metadata 仅仅用了一个整型来描述方块朝向、旋转角,甚至可以定义特殊行为。
那读者可以猜一猜,MC 1.7 以前的游戏程序是如何解析这些整型的呢?据官方透露,源码中竟然是 switch - case
加注释!
switch (meta) {
case 0: { ... } // south and on the lower half of the block
case 1: { ... } // south on the upper side of the block
case 2: { ... } // north and on the lower half of the block
case 3: { ... } // north and on the upper half of the block
// ... etc. ...
}
显然一个整型表达如此多的信息,不仅可读性和可维护性很差,而且在 forge 反编译后不会保留源码中的注释!这直接导致了 forge 早期安排控制方块状态的接口非常困难。这肯定不是一个优雅的设计。
于是在 1.8 版本及以后,Mojang 彻底抛弃了使用 metadata 存储方块状态信息的方法,定义了 block state system(方块状态系统)并沿用至今。
这套系统,将一些与方块本身类型无关的状态数据(就是之前提到的,像方块朝向、旋转角、某些简单特殊行为)从 Block
类中抽象出来,每一个状态数据都属于一种类型 Property<?>
或其子类的实例。例如:
- 表达方块击中后发出的音符
EnumProperty<NoteBlockInstrument>
; - 方块朝向
DirectionProperty
; - 是否被红石充能
Property<Boolean>
; - … …
最后,每个方块类中,所有这些属性的键值对(名是属性名,值是 property 实例的值)就以一个全新的类型 BlockState
来包装。
我们只需要知道,在一个 Block
中,已经包装了 StateContainer
用于存放 BlockState
,以及一个方块自己的默认状态 this.defaultState
;
所以我们要添加一个状态,只需完成 3 步:
- 定义私有属性(继承于
Property<?>
或类型模板实例化)作为新状态; - 在构造函数中设置这个状态的默认值(利用
this.setDefaultState()
); - 重写
fillStateContainer
,在执行父类同名方法前向builder
中加入新属性;
2.2.B.2 BlockState
最佳实践
当然我们发现 BlockState
的抽象使得我们对于方块的定义更清晰,但它也不是没有缺点。
由于 BlockState
是可变类型,游戏为了优化性能会在启动时一次性将 BlockState
中所有属性全部排列组合实例化一遍。例如一个包含两个 Property<Boolean>
状态的 BlockState
就需要实例化 2 x 2 = 4 遍。过多的 BlockState
不仅会拖慢游戏的启动性能,而且降低了你的代码可读性。
因此,使用 BlockState
需要遵循以下的最佳实践:
- 不是所有情况都需要
BlockState
,只有该方块类最基本的状态(如朝向、充能)才需要加入; - 其他情况,例如方块有更复杂的自定义行为, 或者方块的状态数过多 / 种类过多,那么要么放在
BlockEntity
中(见 2.4 节),要么拆成另一种方块;
我们以原版 vanilla 台阶为例。橡木台阶的朝向应该定义在
BlockState
中;而橡木台阶和云欢木台阶则应该作为不同的方块类型,而不应该定义在EnumProperty
中。或者说:If it has a different name, it should be a separate block.
2.2.B.3 Property<?>
的使用
Property<?>
的类型模板还有一些接口等待实现(抽象类),所以无法直接实例化,故将创建一个 Property<?>
子类的任务留作习题,读者自行阅读接口来实现自己的 Property<?>
可实例化的子类。
我们这里介绍一些内置的状态属性类型(子类),使用起来更方便:
-
IntegerProperty
,实现了Property<Integer>
,存储一个整型的状态,使用工厂方法构造:IntegerProperty#create(String propertyName, int minimum, int maximum)
-
BooleanProperty
,实现了Property<Boolean>
,存储一个布尔类型的状态,使用工厂方法构造:BooleanProperty#create(String propertyName)
-
EnumProperty<E extends enum<E>>
,实现了Property<E>
,可以以枚举量E
的取值作为状态,使用工厂方法构造:EnumProperty#create(String propertyName, Class<E> enumClass)
-
DirectionProperty
,更简便地实现了EnumProperty<Direction>
,其重载的工厂方法非常多,能帮助我们做很多,例如直接指定方向(东南西北上下):public static final DirectionProperty FACING = DirectionProperty.create("FACING", Direction.SOUTH);
按 predicate 断言定义限制:
public static final DirectionProperty FACING = DirectionProperty.create("FACING", Direction.Plane.HORIZONTAL);
public static final DirectionProperty FACING = DirectionProperty.create("FACING", Direction.Axis.X);
2.2.B.4 理论的实践:放置方块时定义朝向
有了以上知识,我们就可以实现原版许多方块有 的特性:按照玩家站立位置放置方块时,定义方块不同的朝向。
我们知道,如果只定义方块类而不定义 BlockState
的话,放置的方块始终只朝向一个方向。而像箱子一类的方块它们之所以能跟随玩家站立的方向决定朝向,就是因为它们额外定义了 BlockState
以及处理这种逻辑的方法。
这里如果区分方块朝向,我们建议实现 mirror
和 rotate
方法。因为普通的 Block
类型直接使用了 AbstractBlock
的 rotate
和 mirror
方法(现即将弃用),默认行为是不修改方块状态,这在语义上是不合适的。而 HorizontalBlock
(以及其他有方块朝向的方块,如 AbstractFurnaceBlock
)定义了方块朝向的状态属性,并重写了合适的 rotate
以及 mirror
方法:

注意,文档中的 deprecated 指
IBlockState.withRotation
的调用会被弃用,实现 / 重写是不会弃用的。解析:
rot.rotate(Direction)
指DirectionProperty
的值被rot
旋转后最终的值。上图的
mirror
实现中AbstractBlockState#rotate(Rotation)
已经弃用。我们直接借鉴rotate
一样写就行,例如:@Override
public BlockState mirror(BlockState state, Mirror mirroIn) {
Direction originalFacingState = state.get(FACING);
return state.with(FACING, mirroIn.toRotation(originalFacingState).rotate(originalFacingState));
}
那么首先一定是定义一个状态属性:
public static final DirectionProperty FACING = HorizontalBlock.FACING;
这里直接借用了 HorizotalBlock
的 FACING
状态属性的定义;

注 1:predicate 限制
Direction.Plane.HORIZONTAL
意味着方块的该状态属性不能为UP / DOWN
,只能是东南西北四者之一。注 2:
BlockStateProperties
类型存放着大多数能用到的BlockState
的状态属性的预先定义。
然后构造函数中定义默认值:
public TestBlock() {
super(Properties.create(Material.ROCK).hardnessAndResistance(5));
// 默认北向
this.setDefaultState(this.stateContainer.getBaseState().with(FACING, Direction.NORTH));
}
接着重写 fillStateContainer
:
@Override
protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder) {
builder.add(FACING);
super.fillStateContainer(builder);
}
结束了吗?如果这是普通状态属性,那么结束了。但是这是控制方块朝向的状态,因此需要将这个状态与模型贴图、玩家摆放方式联系起来。
我们 先将方块朝向和玩家摆放方式联系,这需要重写 Block
类的方法 getStateForPlacement
:
@Override
public BlockState getStateForPlacement(BlockItemUseContext context) {
return this.defaultBlockState().setValue(FACING, context.getPlacementHorizontalFacing().getOpposite());
}
解释一下,getStateForPlacement
会当玩家放置方块时被调用,主循环会获取返回的 BlockState
,作为最终方块放下去的状态。
传入的参数是当前用户放置方块时的上下文信息 BlockItemUseContext
。这个类型继承于 ItemUseContext
,用于存储游戏中所有 Item
被右键使用的上下文信息。我们将在 2.4 节讨 论 Item
右击行为的时候在深入了解,现在只要知道它是做什么的就行。
这个 BlockItemUseContext
有方法 getPlacementHorizontalFacing()
,用于获取当前用户放置方块时,水平方向所面向的方向。类型为 Direction
枚举。
Direction
枚举类型中有一个常用的方法 getOpposite()
,获取当前实例方向的反方向。
来个脑筋急转弯,为什么放置方块设置的 BlockState
中的朝向需要是玩家面向的反方向?嘿嘿,如果不这样的话那方块不就背对着玩家了吗 ~
至此,其实想要 “放置方块时定义朝向” 的任务已经完成,但是现在方块各个面都是相同的,看不出什么东西,所以我们按照朝向的状态来定义方块的模型和材质,让方块不同面的材质不一样,这样看的更明显。
我们去定义这个多面方块的 blockstate 的配置 JSON:
{
"variants": {
"facing=east": {
"model": "mymod:block/test_block",
"y": 90
},
"facing=north": {
"model": "mymod:block/test_block"
},
"facing=south": {
"model": "mymod:block/test_block",
"y": 180
},
"facing=west": {
"model": "mymod:block/test_block",
"y": 270
}
}
}
因为我们定义的是一个 horizontal block,所以这样的做 法可以让方块在不同状态下 模型整体绕 y 轴旋转一定角度,这样模型 + 贴图会呈现出面向不同方位的效果。
注:在 MC 中 y 轴表高度轴,也就是垂直于
Direction.Plane.HORIZONTAL
的轴;
模型文件还是与正常方块类似,不过为了区分不同朝向的贴图,我们定义 model JSON 时使用新的 parent
,minecraft:block/orientable
:
{
"parent": "minecraft:block/orientable",
"textures": {
"top": "minecraft:block/sand",
"front": "mymod:blocks/test_block",
"side": "minecraft:block/tnt_side"
}
}
我们定义材质时,故意把顶部面使用 原版沙子材质、侧面使用 TNT 材质,前面使用自定义的材质。
这样在从不同方向放置该方块时,就能看到不同朝向的方块了。