03 方块与方块状态
观察下面几组方块:
- 朝东的楼梯与朝西的楼梯;
- 伸出的活塞与未伸出的活塞;
- 能量等级为 0 和能量等级为 15 的红石粉;
- 含水楼梯与普通楼梯。
它们有时看起来完全不同,却仍然属于同一种方块。Minecraft 如何描述这种差异?
答案是:方块与方块状态并不是同一个概念。
1 方块是一种规则
源码中的 Block 更接近 “一类方块的共同规则”。例如,所有橡木楼梯共享同一个 StairsBlock:
- 它可以有哪些属性;
- 如何计算放置朝向;
- 具有怎样的碰撞箱;
- 受到 NC 更新时如何调整形状;
- 被破坏时使用什么战利品表。
但是,只知道 “这是橡木楼梯” 仍不足以在维度中还原它。游戏还需要知道它朝向哪里、位于方块上半部还是下半部、形状是直线还是转角,以及是否含水。
这些当前取值共同组成一个方块状态(Block State)。
2 属性和值
方块状态由一组属性(Property)和值组成。
以楼梯为例,StairsBlock 声明了四个属性:
属性不是一段可以任意填写的文字。每个 Property 都提供一个有限的允许值集合。StateManager 会枚举这些属性值的所有组合,为该方块建立完整的状态集合。
因此,方块状态只能在方块预先声明的范围内变化。活塞的 extended 只能是 true 或 false;红石粉的 power 只能是 0 至 15;你不能临时给活塞增加一个名为 color 的状态属性。
3 一个坐标对应一个状态
当游戏查询维度中的某组坐标时,返回值是 BlockState,而不只是 Block。
子区块的 PalettedContainer<BlockState> 直接保存方块状态。这样,读取一组坐标时就能立即知道:
- 它是什么方块;
- 它的全部状态属性当前取什么值。
BlockState 内部持有所属的 Block 和一张不可变的属性表。调用:
不会修改原来的状态,而会从预先建立的状态表中取得另一个状态。如果指定值不在属性允许的集合中,源码会抛出异常。
这意味着维度中大量相同状态的方块可以共同使用预生成的状态对象,而不必让每组坐标都拥有一个随意修改的小对象。
4 默认状态
每种方块都有一个默认状态。
Block 构造时会收集 appendProperties 提供的属性,创建 StateManager,然后选出默认状态。具体方块可以在自己的构造器中调整默认属性。
例如,活塞将默认状态设为:
默认状态不是说所有新放置的活塞都朝北。放置时,PistonBlock#getPlacementState 会根据玩家视线方向,从默认状态得到真正要写入维度的状态。默认状态更像是构造其他状态时的起点。
5 方块状态能保存什么
方块状态适合保存:
- 取值数量有限;
- 经常参与方块行为判断;
- 需要快速读取;
- 通常也需要参与渲染或同步
的信息。
例如朝向、开关、能量等级、年龄和含水状态。
但箱子中的 27 格物品、告示牌文字、漏斗冷却等数据,可能有大量不同取值。若把它们全部做成方块状态,状态组合数量会迅速膨胀。
这类数据由方块实体(Block Entity)保存。方块状态描述 “这个位置现在是哪一种有限状态”,方块实体则为某些位置附加更复杂、可独立变化的数据。关于方块实体的详细行为,参见。
6 流体状态在哪里
子区块没有在方块状态旁边再保存一份同样大小的流体数组。
ChunkSection#getFluidState 会先取得该位置的 BlockState,再调用方块状态的 getFluidState。以楼梯为例:
waterlogged=false时返回空流体状态;waterlogged=true时返回静止水的流体状态。
所以在同一组坐标上,“方块状态” 和 “流体状态” 不是两份完全无关的数据。流体状态可以由该坐标的方块状态导出。
7 状态变化为什么重要
当拉下拉杆、打开活板门或改变红石粉能量时,方块种类通常没有变化,变化的是同一个方块的 BlockState。
但是,游戏仍然需要把新状态写回区块。写入操作随后可能引起:
- 模型和碰撞箱改变;
- 光照检查;
- 客户端同步;
- NC 更新;
- 比较器更新;
- 方块实体创建、移除或更新。
因此,“没有换成另一种方块” 不等于 “维度数据没有改变”。下一篇将从一次具体的写入操作出发,观察这些事情如何发生。
8 小结
Block描述一种方块共有的规则。BlockState描述某个位置当前采用的有限状态组合。- 属性拥有有限的允许值,状态管理器会预先创建所有组合。
.with(...)返回另一个预生成状态,不会原地修改旧状态。- 维度中的每组方块坐标对应一个方块状态。
- 复杂且取值众多的数据通常由方块实体保存。
- 流体状态由当前位置的方块状态导出。
9 源码分析
9.1 Block:方块的共同规则
Block 的构造器揭示了方块种类与其状态的关系:
appendProperties 是子类声明 "我这个方块有哪些属性" 的位置。每个子类覆写它来注册自己的属性集合。
9.2 StateManager:枚举所有状态组合
StateManager 的构造器负责为一种方块建立所有可能的状态:
getDefaultState() 返回 this.states.get(0)—— 列表的第一个状态即默认状态。
9.3 State:状态的不可变查询
State 的 entries 是一张不可变的 ImmutableMap<Property<?>, Comparable<?>>,记录当前状态在各个属性上的取值。get() 方法直接从 entries 取值;with() 方法通过预建的 withTable 实现 O (1) 的状态跳转:
with() 不会修改原有状态 —— 它从预建的状态表里取出另一个 BlockState 并返回。维度中大量相同状态的方块可以共享同一个 BlockState 对象,而不必为每组坐标都保存一份独立的状态实例。
9.4 具体方块示例
** 楼梯(StairsBlock)** 声明四个属性:
FACING 有 4 个水平方向、HALF 有 2 个值、SHAPE 有 5 个值、WATERLOGGED 有 2 个值,共 4 × 2 × 5 × 2 = 80 种状态。
** 活塞(PistonBlock)** 声明两个属性,构造器中调整默认值:
放置时,getPlacementState 从默认状态出发,根据玩家视线方向计算出真正要写入维度的状态。这证明了 "先计算好初始状态、再写入" 的模式。
** 红石粉(RedstoneWireBlock)** 声明五个属性:POWER(0~15)加四个方向的连接属性:
9.5 流体状态由方块状态导出
ChunkSection 的 getFluidState 证明了流体状态并非独立存储:
以楼梯的 getFluidState 为例:
waterlogged=true 时返回水的流体状态,false 时返回空流体状态。所以同一组坐标上的 "方块状态" 和 "流体状态" 是派生关系,不是两份独立数据。
9.6 Block 的标志常量
这些标志在 World#setBlockState 中驱动了写入后的所有后续行为,下一篇将详细展开。
9.7 参考类列表
net.minecraft.block.Blocknet.minecraft.block.BlockStatenet.minecraft.state.Statenet.minecraft.state.StateManagernet.minecraft.state.property.Propertynet.minecraft.block.StairsBlocknet.minecraft.block.PistonBlocknet.minecraft.block.RedstoneWireBlocknet.minecraft.world.chunk.ChunkSection
