Skip to main content
Version: 1.21.4

方块状态(Blockstates)

在开发过程中,你经常会遇到需要表示方块不同状态的场景。例如,小麦作物有八个生长阶段,如果为每个阶段都创建一个独立的方块,显然是不合理的。再比如,半砖(slab)或类似半砖的方块——它有底部状态、顶部状态以及上下都有的状态。

这时候,方块状态(blockstates)就派上用场了。方块状态是一种简单的方式,用于表示方块可能拥有的不同状态,比如生长阶段或半砖的放置类型。

方块状态属性(Blockstate Properties)

方块状态采用属性(property)系统。一个方块可以拥有多个不同类型的属性。例如,末地传送门框架(end portal frame)有两个属性:是否有眼睛(eye,2 种选项)以及放置的朝向(facing,4 种选项)。因此,末地传送门框架总共有 8 种(2 * 4)不同的方块状态:

minecraft:end_portal_frame[facing=north,eye=false]
minecraft:end_portal_frame[facing=east,eye=false]
minecraft:end_portal_frame[facing=south,eye=false]
minecraft:end_portal_frame[facing=west,eye=false]
minecraft:end_portal_frame[facing=north,eye=true]
minecraft:end_portal_frame[facing=east,eye=true]
minecraft:end_portal_frame[facing=south,eye=true]
minecraft:end_portal_frame[facing=west,eye=true]

blockid[property1=value1,property2=value,...] 这种记法是用来以文本形式标准化表示方块状态的方法,并且在原版 Minecraft 的某些地方(比如命令中)会用到。

如果你的方块没有定义任何方块状态属性,它依然只有一个方块状态——就是那个没有任何属性的状态,因为没有属性需要指定。可以表示为 minecraft:oak_planks[],也可以简写为 minecraft:oak_planks

和方块类似,每一个 BlockState 在内存中都只存在一份。这意味着可以并且应该使用 == 来比较不同的 BlockStateBlockState 也是一个 final 类,意味着它不能被继承。所有功能都应该放在对应的 Block 类中!

何时使用方块状态(When to Use Blockstates)

方块状态 vs. 独立方块(Blockstates vs. Separate Blocks)

一个很好的经验法则是:如果它有不同的名字,那就应该是不同的方块。 例如制作椅子方块时,椅子的朝向应该作为一个属性,而不同种类的木材则应当分成不同的方块。所以每种木材会有一个椅子方块,每个椅子方块有四种方块状态(对应四个方向)。

方块状态 vs. 方块实体(Blockstates vs. Block Entities

在这里,经验法则是:如果状态数量有限,用方块状态;如果状态数量无限或接近无限,用方块实体。 方块实体(block entity)可以存储任意数量的数据,但它们的效率比方块状态低。 方块状态(Blockstates)和方块实体(block entities)可以结合使用。例如,箱子(chest)使用方块状态属性(blockstate properties)来表示方向(direction)、是否充满水(waterlogged)、是否为双箱(double chest)等信息,而存储物品栏(inventory)、当前是否打开、与漏斗(hoppers)的交互等则由方块实体处理。

对于“方块状态到底能有多少状态才算太多?”这个问题,并没有绝对的答案。但我们建议,如果你需要的数据超过 8-9 位(也就是几百种状态以上),那么应该考虑使用方块实体来实现。

实现方块状态(Implementing Blockstates)

要实现方块状态属性(blockstate property),需要在你的方块类中创建或引用一个 public static final Property<?> 常量。你可以自定义 Property<?> 实现,但原版代码已经提供了几种常用的实现,基本可以满足大多数需求:

  • IntegerProperty
    • 实现了 Property<Integer>。定义一个保存整数值的属性。注意:不支持负数。
    • 通过调用 IntegerProperty#create(String propertyName, int minimum, int maximum) 创建。
  • BooleanProperty
    • 实现了 Property<Boolean>。定义一个保存 truefalse 的属性。
    • 通过调用 BooleanProperty#create(String propertyName) 创建。
  • EnumProperty<E extends Enum<E>>
    • 实现了 Property<E>。定义一个可以取枚举类(Enum)值的属性。
    • 通过调用 EnumProperty#create(String propertyName, Class<E> enumClass) 创建。
    • 你也可以只使用枚举类的一部分值(比如 16 种 DyeColor 只用其中 4 种),可参考 EnumProperty#create 的重载方法。

BlockStateProperties 类包含了一些原版共享的属性,建议在可能的情况下优先使用或引用这些属性,而不是自己新建。

有了属性常量后,需要在你的方块类中重写 Block#createBlockStateDefinition(StateDefinition$Builder) 方法。在这个方法里,调用 StateDefinition.Builder#add(YOUR_PROPERTY);StateDefinition.Builder#add 支持可变参数(vararg),如果你有多个属性,可以一次性全部添加。

每个方块都会有一个默认状态。如果没有特别指定,默认状态会使用每个属性的默认值。你可以在构造函数中调用 Block#registerDefaultState(BlockState) 方法来修改默认状态。

如果你希望在放置方块时自定义使用哪个 BlockState,可以重写 Block#getStateForPlacement(BlockPlaceContext)。这样可以根据玩家站立或观察的位置来设置方块的朝向等信息。

为了更好地说明,下面是 EndPortalFrameBlock 类相关部分的示例代码:

public class EndPortalFrameBlock extends Block {
// 注意:其实可以直接使用 BlockStateProperties 中的值,而不必在这里再次引用。
// 但为了简单和易读,推荐像这样添加常量。
public static final EnumProperty<Direction> FACING = BlockStateProperties.FACING;
public static final BooleanProperty EYE = BlockStateProperties.EYE;

public EndPortalFrameBlock(BlockBehaviour.Properties pProperties) {
super(pProperties);
// stateDefinition.any() 会返回内部集合中的一个随机 BlockState,
// 但我们不需要关心,因为我们会自行设置所有属性值
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(EYE, false)
);
}

@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> pBuilder) {
// 这里实际上是把属性添加到状态中
pBuilder.add(FACING, EYE);
}

@Override
@Nullable
public BlockState getStateForPlacement(BlockPlaceContext pContext) {
// 这里的代码用于决定在放置该方块时
// 根据 BlockPlaceContext 使用哪个状态
}
}

使用方块状态(Blockstates)

要从 Block 获取 BlockState,可以调用 Block#defaultBlockState()。默认的方块状态可以通过 Block#registerDefaultState 修改,如上文所述。

你可以通过调用 BlockState#getValue(Property<?>) 并传入你想获取的属性,来获取某个属性的值。继续以末地传送门框为例,代码如下:

// EndPortalFrameBlock.FACING 是一个 EnumProperty<Direction>,因此可以用它从 BlockState 获取一个 Direction
Direction direction = endPortalFrameBlockState.getValue(EndPortalFrameBlock.FACING);

如果你想要一个具有不同属性值的 BlockState,只需在已有的方块状态上调用 BlockState#setValue(Property<T>, T),传入属性和对应的值即可。以杠杆(lever)为例,代码如下:

endPortalFrameBlockState = endPortalFrameBlockState.setValue(EndPortalFrameBlock.FACING, Direction.SOUTH);
note

BlockState 是不可变的。这意味着当你调用 #setValue(Property<T>, T) 时,实际上并没有修改原有的方块状态。相反,内部会进行一次查找,并返回你请求的那个方块状态对象,该对象是唯一一个拥有这些确切属性值的实例。因此,单纯调用 state#setValue 而不将结果保存到变量(比如重新赋值给 state)是没有任何效果的。

要从世界(level)中获取一个 BlockState,可以使用 Level#getBlockState(BlockPos)

Level#setBlock

要在世界中设置一个 BlockState,可以使用 Level#setBlock(BlockPos, BlockState, int)int 参数值得额外解释一下,因为它的含义并不是一目了然。它代表所谓的更新标志(update flags)。

为了帮助你正确设置这些更新标志,Block 类中提供了一些以 UPDATE_ 为前缀的 int 常量。如果你希望组合使用这些标志,可以通过位或操作(bitwise-OR)将它们合并,例如 Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS

  • Block.UPDATE_NEIGHBORS 会向相邻方块发送一次更新。更具体地说,它会调用 Block#neighborChanged,该方法又会调用一系列方法,大多数与红石(redstone)机制有关。
  • Block.UPDATE_CLIENTS 会将方块的更新同步到客户端。
  • Block.UPDATE_INVISIBLE 明确表示不会在客户端进行更新。它还会覆盖 Block.UPDATE_CLIENTS,导致更新不会被同步。但服务器端总是会更新该方块。
  • Block.UPDATE_IMMEDIATE 会强制在客户端主线程上重新渲染该方块。
  • Block.UPDATE_KNOWN_SHAPE 用于阻止邻居更新的递归调用。
  • Block.UPDATE_SUPPRESS_DROPS 会禁用该位置原有方块的掉落物。
  • Block.UPDATE_MOVE_BY_PISTON 仅由活塞(piston)相关代码使用,用于标记方块是被活塞移动的。它主要用于延迟光照引擎的更新。
  • Block.UPDATE_SKIP_SHAPE_UPDATE_ON_WIREExperimentalRedstoneWireEvaluator 使用,用于指示是否应跳过形状更新。只有当电力强度的改变不是由放置引起,或信号的原始来源不是当前红石线(wire)时,才会设置该标志。
  • Block.UPDATE_ALLBlock.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS 的别名(alias)。
  • Block.UPDATE_ALL_IMMEDIATEBlock.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS | Block.UPDATE_IMMEDIATE 的别名。
  • Block.UPDATE_NONEBlock.UPDATE_INVISIBLE 的别名。

此外,还有一个便捷方法 Level#setBlockAndUpdate(BlockPos pos, BlockState state),它内部会调用 setBlock(pos, state, Block.UPDATE_ALL)