方块状态
开发中经常需要让同一种方块拥有不同状态。例如,小麦有八个生长阶段,为每个阶段单独创建一个方块并不合适;台阶类方块则需要下半、上半和双层三种状态。
方块状态正是用来处理这类情况的。它能够方便地表示同一方块的不同形态,例如作物生长阶段或台阶的放置类型。
方块状态属性
方块状态采用属性系统。一个方块可以有多个不同类型的属性。例如,末地传送门框架有两个属性:是否具有末影之眼(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。