方块状态

方块状态

开发中经常需要让同一种方块拥有不同状态。例如,小麦有八个生长阶段,为每个阶段单独创建一个方块并不合适;台阶类方块则需要下半、上半和双层三种状态。

方块状态正是用来处理这类情况的。它能够方便地表示同一方块的不同形态,例如作物生长阶段或台阶的放置类型。

方块状态属性

方块状态采用属性系统。一个方块可以有多个不同类型的属性。例如,末地传送门框架有两个属性:是否具有末影之眼(eye,两种取值),以及放置朝向(facing,四种取值)。因此,它共有 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:oak_planks[],也可以简写为 minecraft:oak_planks。

与方块一样,每种 BlockState 在内存中只存在一个实例,因此可以且应当使用 == 比较。BlockState 也是 final 类,不能被继承。所有功能都应写在对应的 Block 类中!

何时使用方块状态

方块状态还是独立方块

一个实用原则是:如果名称不同,就应当是独立方块。以椅子为例,椅子的朝向适合作为状态属性,不同木材制成的椅子则适合使用不同方块。每种木材对应一个椅子方块,每个方块拥有四种朝向状态。

方块状态还是方块实体

基本原则是:**状态数量有限时使用方块状态,状态数量无限或接近无限时使用方块实体。**方块实体可以存储任意规模的数据,但性能开销高于方块状态。

两者也可以结合使用。例如,箱子通过方块状态属性表示朝向、是否含水以及是否组成大箱子;物品栏、当前是否打开,以及与漏斗的交互等,则由方块实体处理。

“多少状态才算过多”没有绝对答案。不过,如果需要超过 8~9 位数据,也就是超过几百种状态,建议改用方块实体。

实现方块状态

要添加状态属性,先在方块类中创建或引用一个 public static final Property<?> 常量。虽然可以自行实现 Property<?>,但原版已提供一些便捷实现,足以覆盖大多数情况:

  • IntegerProperty
    • 实现 Property<Integer>,保存整数值,不支持负数。
    • 通过 IntegerProperty#create(String propertyName, int minimum, int maximum) 创建。
  • BooleanProperty
    • 实现 Property<Boolean>,保存 true 或 false。
    • 通过 BooleanProperty#create(String propertyName) 创建。
  • EnumProperty<E extends Enum<E>>
    • 实现 Property<E>,取值来自某个枚举类。
    • 通过 EnumProperty#create(String propertyName, Class<E> enumClass) 创建。
    • 也可以只使用枚举中的部分值,例如 16 种 DyeColor 中的 4 种,详见 EnumProperty#create 的重载。
  • DirectionProperty
    • 继承 EnumProperty<Direction>,保存一个 Direction。
    • 通过 DirectionProperty#create(String propertyName) 创建。
    • 它提供了若干便捷的谓词。例如,只允许水平四方向时,使用 DirectionProperty.create("<name>", Direction.Plane.HORIZONTAL);只允许 X 轴方向时,使用 DirectionProperty.create("<name>", Direction.Axis.X)。

BlockStateProperties 包含原版共用的属性。应尽可能使用或引用这些属性,而不是重新创建。

定义属性常量后,重写方块类中的 Block#createBlockStateDefinition(StateDefinition$Builder),并在其中调用 StateDefinition.Builder#add(YOUR_PROPERTY);。StateDefinition.Builder#add 使用可变参数,因此可以一次添加多个属性。

每个方块都有默认状态。如果不额外指定,默认状态会采用每个属性的默认值。可以在构造函数中调用 Block#registerDefaultState(BlockState) 修改它。

如果需要决定放置方块时使用哪个 BlockState,可以重写 Block#getStateForPlacement(BlockPlaceContext)。例如,根据放置时玩家的位置或视线方向,设置方块朝向。

下面展示 EndPortalFrameBlock 中的相关部分:

public class EndPortalFrameBlock extends Block {
    // Note: It is possible to directly use the values in BlockStateProperties instead of referencing them here again.
    // However, for the sake of simplicity and readability, it is recommended to add constants like this.
    public static final DirectionProperty FACING = BlockStateProperties.FACING;
    public static final BooleanProperty EYE = BlockStateProperties.EYE;

    public EndPortalFrameBlock(BlockBehaviour.Properties pProperties) {
        super(pProperties);
        // stateDefinition.any() returns a random BlockState from an internal set,
        // we don't care because we're setting all values ourselves anyway
        registerDefaultState(stateDefinition.any()
                .setValue(FACING, Direction.NORTH)
                .setValue(EYE, false)
        );
    }

    @Override
    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> pBuilder) {
        // this is where the properties are actually added to the state
        pBuilder.add(FACING, EYE);
    }

    @Override
    @Nullable
    public BlockState getStateForPlacement(BlockPlaceContext pContext) {
        // code that determines which state will be used when
        // placing down this block, depending on the BlockPlaceContext
    }
}

使用方块状态

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

调用 BlockState#getValue(Property<?>),传入对应属性,即可读取它的值。继续以末地传送门框架为例:

// EndPortalFrameBlock.FACING is a DirectionProperty and thus can be used to obtain a Direction from the BlockState
Direction direction = endPortalFrameBlockState.getValue(EndPortalFrameBlock.FACING);

要获取具有不同属性值的 BlockState,在已有状态上调用 BlockState#setValue(Property<T>, T),传入属性与新值即可:

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

提示 BlockState 是不可变对象。调用 #setValue(Property<T>, T) 不会修改原有状态,而是通过内部查找,返回具有所需属性值组合的唯一状态实例。因此,单独调用 state#setValue 而不保存返回值(例如重新赋给 state),不会产生实际效果。

要读取世界某个位置的 BlockState,使用 Level#getBlockState(BlockPos)。

Level#setBlock

使用 Level#setBlock(BlockPos, BlockState, int) 可以设置世界中的方块状态。

其中的 int 参数是更新标记,需要额外说明。Block 提供了一组以 UPDATE_ 开头的整数常量,帮助正确设置这些标记。可以通过按位或组合多个标记,例如 Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS。

  • Block.UPDATE_NEIGHBORS:向相邻方块发送更新。具体会调用 Block#neighborChanged,进而调用一系列主要与红石相关的方法。
  • 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:仅供活塞代码使用,表示方块被活塞移动,主要用于延迟光照引擎更新。
  • Block.UPDATE_ALL:等同于 Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS。
  • Block.UPDATE_ALL_IMMEDIATE:等同于 Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS | Block.UPDATE_IMMEDIATE。
  • Block.NONE:等同于 Block.UPDATE_INVISIBLE。

还提供了便捷方法 Level#setBlockAndUpdate(BlockPos pos, BlockState state),它内部会调用 setBlock(pos, state, Block.UPDATE_ALL)。


来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交 4a680cdd4d15。