方块状态

方块状态

开发中经常需要让一个方块具有不同状态。例如,小麦有八个生长阶段,为每个阶段单独创建方块并不合适;又如台阶或类似方块,既可以放在下半部,也可以放在上半部,还可以合并为双层台阶。

方块状态(blockstate)用于表示方块可能具有的这些状态,例如生长阶段或台阶的放置类型。

方块状态属性

方块状态使用属性系统。一个方块可以拥有多个不同类型的属性。例如,末地传送门框架有两个属性:是否含有末影之眼(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:oak_planks[],也可以简写为 minecraft:oak_planks。

与方块相同,每个 BlockState 在内存中都只存在一个实例。因此,比较 BlockState 时可以、也应该使用 ==。BlockState 还是 final 类,不能继承。所有功能都应放在对应的 Block 类中!

何时使用方块状态

方块状态与独立方块

一个实用原则是:名称不同的东西应当是不同的方块。例如制作椅子时,朝向应作为属性,而不同木材的椅子应当分别创建方块。这样,每种木材对应一个椅子方块,每个方块又有四种方块状态,分别对应四个朝向。

方块状态与方块实体

这里的实用原则是:状态数量有限时使用方块状态;状态数量无限或接近无限时使用方块实体。 方块实体可以存储任意数量的数据,但速度比方块状态慢。

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

“多少种状态算太多”没有统一答案。不过,如果数据超过 8–9 位(即超过几百种状态),建议改用方块实体。

实现方块状态

要实现方块状态属性,在方块类中创建或引用一个 public static final Property<?> 常量。虽然可以自行实现 Property<?>,但原版已经提供了几个便捷实现,能够满足大多数需求:

  • IntegerProperty
    • 实现 Property<Integer>,存储整数值,不支持负数。
    • 通过 IntegerProperty#create(String name, int min, int max) 创建。
  • BooleanProperty
    • 实现 Property<Boolean>,存储 true 或 false。
    • 通过 BooleanProperty#create(String name) 创建。
  • EnumProperty<E extends Enum<E>>
    • 实现 Property<E>,取值来自一个枚举类。
    • 通过 EnumProperty#create(String name, Class<E> enumClass) 创建。
    • 也可以只使用枚举值的一个子集,例如 16 种 DyeColor 中的 4 种;参见 EnumProperty#create 的重载。

BlockStateProperties 类包含原版共用的属性。应尽可能复用或引用这些属性,而不是自行创建。

定义好属性常量后,在方块类中重写 Block#createBlockStateDefinition(StateDefinition.Builder),并调用 StateDefinition.Builder#add(YOUR_PROPERTY);。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 EnumProperty<Direction> FACING = BlockStateProperties.FACING;
    public static final BooleanProperty EYE = BlockStateProperties.EYE;

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

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

    @Override
    @Nullable
    public BlockState getStateForPlacement(BlockPlaceContext ctx) {
        // 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 an EnumPropery<Direction> 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)不会产生效果。

使用 Level#getBlockState(BlockPos) 可读取世界中某个位置的方块状态。

Level#setBlock

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

最后的 int 参数表示更新标志(update flags),它的含义需要进一步说明。

Block 提供了一组以 UPDATE_ 开头的 int 常量,帮助正确设置这些标志。需要组合标志时,可以按位或,例如 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_SKIP_SHAPE_UPDATE_ON_WIRE 供 ExperimentalRedstoneWireEvaluator 标记是否跳过形状更新。只有在非放置操作引起信号强度变化,或者信号的原始来源不是当前红石线时才设置。
  • Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS 阻止调用 BlockEntity#preRemoveSideEffects,通常用于阻止方块实体清空其内容。
  • Block.UPDATE_SKIP_ON_PLACE 阻止调用 Block#onPlace,通常用于阻止方块执行初始行为,例如让铁轨连接其他方块或生成傀儡。
  • Block.UPDATE_NONE 是 Block.UPDATE_INVISIBLE | Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS 的别名。
  • Block.UPDATE_ALL 是 Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS 的别名。
  • Block.UPDATE_ALL_IMMEDIATE 是 Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS | Block.UPDATE_IMMEDIATE 的别名。
  • Block.UPDATE_SKIP_ALL_SIDEEFFECTS 是 Block.UPDATE_SKIP_ON_PLACE | Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS | Block.UPDATE_SUPPRESS_DROPS | Block.UPDATE_KNOWN_SHAPE 的别名。

另有便捷方法 Level#setBlockAndUpdate(BlockPos pos, BlockState state),它在内部调用 setBlock(pos, state, Block.UPDATE_ALL)。


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