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