01 方块与方块状态
观察下面几组方块:
- 朝东的楼梯与朝西的楼梯;
- 伸出的活塞与未伸出的活塞;
- 能量等级为 0 和能量等级为 15 的红石粉;
- 含水楼梯与普通楼梯。
它们有时看起来完全不同,却仍然属于同一种方块。Minecraft 如何描述这种差异?
答案是:方块与方块状态并不是同一个概念。
1 方块是一种规则,状态是它的当前取值
源码中的 Block 的本质是 **"一类方块的共同规则集合"**。例如,所有橡木楼梯共享同一个 StairsBlock 实例,它定义了:
- 可以有哪些属性(朝向、上半部 / 下半部、形状、是否含水);
- 如何计算放置朝向;
- 具有怎样的碰撞箱;
- 受到 NC 更新时如何调整形状;
- 被破坏时使用什么战利品表。
但是,只知道 "这是橡木楼梯" 仍不足以在世界中还原它。游戏还需要知道它朝向哪里、位于上半部还是下半部、形状是直线还是转角,以及是否含水。
这些当前取值共同组成一个方块状态(Block State)。因此,Block 与 BlockState 的本质关系为:Block 定义 "可以变什么、变了以后规则怎么变";BlockState 记录 "当前取了哪些值"。
2 属性和值
方块状态由一组属性(Property)和值组成。每个属性有三个硬性限制:
- 每个属性只提供有限的允许值集合——
StairsBlock的half只能是top或bottom,不能临时变成别的值。 - 所有属性在所有允许值上的组合都会被预先创建——
StateManager在Block构造时枚举所有组合,为每一种组合创建一个不可变的BlockState对象。 - 已创建的状态不可修改——调用
state.with(property, value)不会原地改变原状态,而是从预建的查找表中返回另一个BlockState。
以楼梯为例,StairsBlock 声明了四个属性:
四个属性的组合总数:4 × 2 × 5 × 2 = 80 种。这 80 个 BlockState 对象在方块注册时就全部创建完毕,此后查找任何一个组合都是 O (1) 的查表操作。
关键规则:不能临时给方块增加新属性。活塞的 extended 只能是 true 或 false;红石粉的 power 只能是 0 至 15。任何超出允许值集合的写入都会在 State.with() 中抛出异常。
3 一个坐标对应一个状态
当游戏查询世界中的某组坐标时,返回值是 BlockState,而不只是 Block。
子区块的 PalettedContainer<BlockState> 直接保存方块状态。这样,读取一组坐标时就能立即知道:
- 它是什么方块(通过
BlockState.getBlock()); - 它的全部状态属性当前取什么值(通过
BlockState.get(Property))。
这带来一个重要性质:世界中大量相同状态的方块可以共同使用同一个预生成的 BlockState 对象。每组坐标不需要维护一个可以随意修改的小对象——它们共享不可变的状态实例。
BlockState 内部持有所属的 Block 和一张不可变的属性表。调用:
不会修改原来的状态,而会从预先建立的状态表中取得另一个状态。如果指定值不在属性允许的集合中,源码会抛出异常。
这意味着世界中大量相同状态的方块可以共同使用预生成的状态对象,而不必让每组坐标都拥有一个随意修改的小对象。
4 子区块与调色板:状态如何被保存
一个世界有数百万个坐标位置,如果每个位置都单独保存一份完整的 BlockState 对象,内存和存储都会迅速膨胀。Minecraft 的解决方案是子区块和调色板。
4.1 子区块
一个 16×16×16 的区域称为一个子区块(ChunkSection)。一个常规区块(Chunk)最多包含 24 个子区块(Y=-64 到 Y=319,每 16 格一层)。每个子区块管理 4096 个坐标位置——但并不是为每个位置单独保存一个 BlockState 对象。
4.2 调色板:用编号代替对象
子区块内部维护一张调色板(Palette),记录 "这个子区块里出现过哪些方块状态":
- 调色板是一个列表,每个条目是一个唯一的
BlockState。 - 子区块中的 4096 个位置各自保存一个很小的编号,指向调色板中的某个条目。
- 需要读取某位置的方块状态时,通过编号查表得到
BlockState。 - 写入新状态时,如果该状态不在调色板中,就把它加入调色板,对应位置保存新条目的编号。
这个过程在源码中由 PalettedContainer<BlockState> 负责。它之所以叫 "Paletted",正是因为其核心是一个调色板(类比图片索引色)和一个索引数组。子区块内的方块数据实际上是 "表 + 4096 个编号",而不是 "4096 个独立的 BlockState 对象"。
4.3 为什么这很重要
这种设计带来了几个直接影响后续机制理解的性质:
- 大量相同状态的方块几乎不占额外空间:一整片石头地面只会在调色板中占一个条目,所有位置共用同一个编号。
- 调色板大小有限:如果子区块内方块状态种类过多,调色板会升级到更大的存储格式(如全局调色板直接映射),但原理相同。
- 写入 = 改编号:改变一个位置的方块状态,本质上是把该位置的编号改为另一种状态的编号。如果新状态已在调色板中,写入就是一次便宜的编号替换。
- 共享实例:因为调色板里的
BlockState是预生成的不可变对象,所有引用该状态的位置实际指向同一个对象。
理解调色板之后,"世界中保存的是什么" 和 "BlockState.with() 做了什么 " 之间的桥梁就架好了:世界保存的是编号,编号指向调色板,调色板指向预生成的不可变 BlockState 对象,BlockState.with() 返回另一个预生成对象。
5 默认状态是构造的起点
每种方块都有一个默认状态。它在 Block 构造的最后被设为 StateManager.getDefaultState()——即第一个被枚举出的状态组合。
具体方块可以在自己的构造器中调整默认属性。例如,活塞将默认状态设为:
默认状态不是说所有新放置的活塞都朝北。放置时,PistonBlock#getPlacementState 会根据玩家视线方向,从默认状态出发,通过 .with() 得到真正要写入世界的状态。默认状态更准确的定位是:构造其他状态的起点——所有状态都是从这个起点通过有限的 .with() 调用可达的。
6 方块状态能保存什么
方块状态适合保存:
- 取值数量有限;
- 经常参与方块行为判断;
- 需要快速读取;
- 通常也需要参与渲染或同步
的信息。
例如朝向、开关、能量等级、年龄和含水状态。
但箱子中的 27 格物品、告示牌文字、漏斗冷却等数据,可能有大量不同取值。若把它们全部做成方块状态,状态组合数量会迅速膨胀。
这类数据由方块实体(Block Entity)保存。方块状态描述 "这个位置现在是哪一种有限状态",方块实体则为某些位置附加更复杂、可独立变化的数据。
6.1 方块实体与实体是两回事
尽管名字里都有 "实体",方块实体(BlockEntity)和实体(Entity)是完全不同的两个系统:
- Entity 有坐标、Motion、碰撞箱,可以移动,主动参与每刻运算。猪、矿车、TNT、物品展示框都属于这个范畴。
- BlockEntity 固定附着在某一组方块坐标上,本身不会移动。它存在的唯一理由是该位置的方块需要保存或运算超出
BlockState能力范围的数据。箱子、漏斗、告示牌、信标都属于这个范畴。
不是每个方块都有方块实体。一个位置是否应该拥有方块实体,首先由该位置的 BlockState 决定——例如箱子方块状态声明了 "我需要方块实体",游戏才会为这个坐标创建箱子方块实体。方块实体的数据保存在区块的方块实体区域中,使用 NBT 格式(见下一节),与子区块中的调色板数据分开存储。
大部分方块实体会参与某种定时运算,具体周期各不相同:漏斗每 8gt 检查传输,信标每 80gt 更新效果,告示牌则不参与任何定时运算。关于方块实体的详细行为,参见方块实体。
7 NBT:复杂数据的通用格式
前面提到,BlockState 只能保存取值有限的信息。那么箱子里的物品、告示牌上的文字、漏斗的冷却时间这些取值众多的数据放在哪里?
答案是 NBT(Named Binary Tag),Minecraft 用来保存复杂结构化数据的树状格式。
7.1 NBT 是什么
NBT 可以理解为 "带类型的 JSON":数据组织成键值对的树状结构,但每个值有明确的类型标签(整数、字符串、列表、复合标签等)。一棵 NBT 树有一个顶层复合标签(Compound Tag),里面嵌套多个命名子标签。
NBT 广泛用于需要保存复杂数据的地方:
- 方块实体:箱子内容、告示牌文字、漏斗冷却、信标效果等。
- 实体:生物属性、装备、药水效果、自定义名称等。
- 物品:附魔、耐久、自定义名称、
BlockStateTag、BlockEntityTag等。 - 区块存储:方块实体数据、实体数据、计划刻队列等。
- 玩家数据:背包、末影箱、进度、统计数据等。
- 关卡数据:世界设置、游戏规则等。
7.2 NBT 与 BlockState 的分工
BlockState 和 NBT 解决的是完全不同的问题:
BlockState解决 "这个位置现在是哪种有限状态"——取值范围已知、数量可控、需要快速查表。- NBT 解决 "这个位置/实体/物品还附着哪些复杂数据"——取值范围开放、可能包含嵌套结构、不需要参与每次方块行为判断。
所以在世界中,同一组坐标上可能同时存在:
- 一个
BlockState(由调色板保证),和 - 一份 NBT 数据(由方块实体持有)。
前者回答 "这是什么方块、朝哪、开还是关",后者回答 "箱子里有什么、告示牌写了什么、漏斗还剩多少冷却"。两者分工明确,互不代替。
8 流体状态在哪里
子区块没有在方块状态旁边再保存一份同样大小的流体数组。
ChunkSection#getFluidState 会先取得该位置的 BlockState,再调用方块状态的 getFluidState。以楼梯为例:
waterlogged=false时返回空流体状态;waterlogged=true时返回静止水的流体状态。
所以在同一组坐标上,“方块状态” 和 “流体状态” 不是两份完全无关的数据。流体状态可以由该坐标的方块状态导出。
9 状态变化为什么重要
当拉下拉杆、打开活板门或改变红石粉能量时,方块种类通常没有变化,变化的是同一个方块的 BlockState。
但是,游戏仍然需要把新状态写回区块。写入操作随后可能引起:
- 模型和碰撞箱改变;
- 光照检查;
- 客户端同步;
- NC 更新;
- 比较器更新;
- 方块实体创建、移除或更新。
因此,“没有换成另一种方块” 不等于 “世界数据没有改变”。下一篇将从一次具体的写入操作出发,观察这些事情如何发生。
10 小结
Block描述一种方块共有的规则。BlockState描述某个位置当前采用的有限状态组合。- 属性拥有有限的允许值,状态管理器会预先创建所有组合。
.with(...)返回另一个预生成状态,不会原地修改旧状态。- 世界中的每组方块坐标对应一个方块状态。
- 复杂且取值众多的数据通常由方块实体保存。
- 流体状态由当前位置的方块状态导出。
11 源码分析
11.1 Block:方块的共同规则
Block 的构造器揭示了方块种类与其状态的关系:
appendProperties 是子类声明 "我这个方块有哪些属性" 的位置。每个子类覆写它来注册自己的属性集合。
11.2 StateManager:枚举所有状态组合
StateManager 的构造器负责为一种方块建立所有可能的状态:
getDefaultState() 返回 this.states.get(0)——列表的第一个状态即默认状态。
11.3 State:状态的不可变查询
State 的 entries 是一张不可变的 ImmutableMap<Property<?>, Comparable<?>>,记录当前状态在各个属性上的取值。get() 方法直接从 entries 取值;with() 方法通过预建的 withTable 实现 O (1) 的状态跳转:
with() 不会修改原有状态——它从预建的状态表里取出另一个 BlockState 并返回。世界中大量相同状态的方块可以共享同一个 BlockState 对象,而不必为每组坐标都保存一份独立的状态实例。
11.4 具体方块示例
** 楼梯(StairsBlock)** 声明四个属性:
FACING 有 4 个水平方向、HALF 有 2 个值、SHAPE 有 5 个值、WATERLOGGED 有 2 个值,共 4 × 2 × 5 × 2 = 80 种状态。
** 活塞(PistonBlock)** 声明两个属性,构造器中调整默认值:
放置时,getPlacementState 从默认状态出发,根据玩家视线方向计算出真正要写入世界的状态。这证明了 "先计算好初始状态、再写入" 的模式。
** 红石粉(RedstoneWireBlock)** 声明五个属性:POWER(0~15)加四个方向的连接属性:
11.5 流体状态由方块状态导出
ChunkSection 的 getFluidState 证明了流体状态并非独立存储:
以楼梯的 getFluidState 为例:
waterlogged=true 时返回水的流体状态,false 时返回空流体状态。所以同一组坐标上的 "方块状态" 和 "流体状态" 是派生关系,不是两份独立数据。
11.6 Block 的标志常量
这些标志在 World#setBlockState 中驱动了写入后的所有后续行为,下一篇将详细展开。
11.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
